debian-mirror-gitlab/app/assets/javascripts/design_management/pages/design/index.vue
2020-05-24 23:13:21 +05:30

400 lines
12 KiB
Vue

<script>
import { ApolloMutation } from 'vue-apollo';
import Mousetrap from 'mousetrap';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import allVersionsMixin from '../../mixins/all_versions';
import Toolbar from '../../components/toolbar/index.vue';
import DesignDiscussion from '../../components/design_notes/design_discussion.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignDestroyer from '../../components/design_destroyer.vue';
import DesignScaler from '../../components/design_scaler.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
import DesignPresentation from '../../components/design_presentation.vue';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import appDataQuery from '../../graphql/queries/appData.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
import updateImageDiffNoteMutation from '../../graphql/mutations/updateImageDiffNote.mutation.graphql';
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
import {
extractDiscussions,
extractDesign,
extractParticipants,
updateImageDiffNoteOptimisticResponse,
} from '../../utils/design_management_utils';
import {
updateStoreAfterAddImageDiffNote,
updateStoreAfterUpdateImageDiffNote,
} from '../../utils/cache_update';
import {
ADD_DISCUSSION_COMMENT_ERROR,
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
DESIGN_NOT_FOUND_ERROR,
DESIGN_VERSION_NOT_EXIST_ERROR,
UPDATE_NOTE_ERROR,
designDeletionError,
} from '../../utils/error_messages';
import { trackDesignDetailView } from '../../utils/tracking';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
export default {
components: {
ApolloMutation,
DesignPresentation,
DesignDiscussion,
DesignScaler,
DesignDestroyer,
Toolbar,
DesignReplyForm,
GlLoadingIcon,
GlAlert,
Participants,
},
mixins: [allVersionsMixin],
props: {
id: {
type: String,
required: true,
},
},
data() {
return {
design: {},
comment: '',
annotationCoordinates: null,
projectPath: '',
errorMessage: '',
issueIid: '',
scale: 1,
};
},
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
},
design: {
query: getDesignQuery,
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
variables() {
return this.designVariables;
},
update: data => extractDesign(data),
result(res) {
this.onDesignQueryResult(res);
},
error() {
this.onQueryError(DESIGN_NOT_FOUND_ERROR);
},
},
},
computed: {
isFirstLoading() {
// We only want to show spinner on initial design load (when opened from a deep link to design)
// If we already have cached a design, loading shouldn't be indicated to user
return this.$apollo.queries.design.loading && !this.design.filename;
},
discussions() {
return extractDiscussions(this.design.discussions);
},
discussionParticipants() {
return extractParticipants(this.design.issue.participants);
},
markdownPreviewPath() {
return `/${this.projectPath}/preview_markdown?target_type=Issue`;
},
isSubmitButtonDisabled() {
return this.comment.trim().length === 0;
},
renderDiscussions() {
return this.discussions.length || this.annotationCoordinates;
},
designVariables() {
return {
fullPath: this.projectPath,
iid: this.issueIid,
filenames: [this.$route.params.id],
atVersion: this.designsVersion,
};
},
mutationPayload() {
const { x, y, width, height } = this.annotationCoordinates;
return {
noteableId: this.design.id,
body: this.comment,
position: {
headSha: this.design.diffRefs.headSha,
baseSha: this.design.diffRefs.baseSha,
startSha: this.design.diffRefs.startSha,
x,
y,
width,
height,
paths: {
newPath: this.design.fullPath,
},
},
};
},
issue() {
return {
...this.design.issue,
webPath: this.design.issue.webPath.substr(1),
};
},
isAnnotating() {
return Boolean(this.annotationCoordinates);
},
},
mounted() {
Mousetrap.bind('esc', this.closeDesign);
},
beforeDestroy() {
Mousetrap.unbind('esc', this.closeDesign);
},
methods: {
addImageDiffNoteToStore(
store,
{
data: { createImageDiffNote },
},
) {
updateStoreAfterAddImageDiffNote(
store,
createImageDiffNote,
getDesignQuery,
this.designVariables,
);
},
updateImageDiffNoteInStore(
store,
{
data: { updateImageDiffNote },
},
) {
return updateStoreAfterUpdateImageDiffNote(
store,
updateImageDiffNote,
getDesignQuery,
this.designVariables,
);
},
onMoveNote({ noteId, discussionId, position }) {
const discussion = this.discussions.find(({ id }) => id === discussionId);
const note = discussion.notes.find(
({ discussion: noteDiscussion }) => noteDiscussion.id === discussionId,
);
const mutationPayload = {
optimisticResponse: updateImageDiffNoteOptimisticResponse(note, {
position,
}),
variables: {
input: {
id: noteId,
position,
},
},
mutation: updateImageDiffNoteMutation,
update: this.updateImageDiffNoteInStore,
};
return this.$apollo.mutate(mutationPayload).catch(e => this.onUpdateImageDiffNoteError(e));
},
onDesignQueryResult({ data, loading }) {
// On the initial load with cache-and-network policy data is undefined while loading is true
// To prevent throwing an error, we don't perform any logic until loading is false
if (loading) {
return;
}
if (!data || !extractDesign(data)) {
this.onQueryError(DESIGN_NOT_FOUND_ERROR);
} else if (this.$route.query.version && !this.hasValidVersion) {
this.onQueryError(DESIGN_VERSION_NOT_EXIST_ERROR);
}
},
onQueryError(message) {
// because we redirect user to /designs (the issue page),
// we want to create these flashes on the issue page
createFlash(message);
this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME });
},
onError(message, e) {
this.errorMessage = message;
throw e;
},
onCreateImageDiffNoteError(e) {
this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e);
},
onUpdateNoteError(e) {
this.onError(UPDATE_NOTE_ERROR, e);
},
onDesignDiscussionError(e) {
this.onError(ADD_DISCUSSION_COMMENT_ERROR, e);
},
onUpdateImageDiffNoteError(e) {
this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
},
onDesignDeleteError(e) {
this.onError(designDeletionError({ singular: true }), e);
},
openCommentForm(annotationCoordinates) {
this.annotationCoordinates = annotationCoordinates;
},
closeCommentForm() {
this.comment = '';
this.annotationCoordinates = null;
},
closeDesign() {
this.$router.push({
name: this.$options.DESIGNS_ROUTE_NAME,
query: this.$route.query,
});
},
trackEvent() {
// TODO: This needs to be made aware of referers, or if it's rendered in a different context than a Issue
trackDesignDetailView(
'issue-design-collection',
'issue',
this.$route.query.version || this.latestVersionId,
this.isLatestVersion,
);
},
updateActiveDiscussion(id) {
this.$apollo.mutate({
mutation: updateActiveDiscussionMutation,
variables: {
id,
source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
},
});
},
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.trackEvent();
});
},
beforeRouteUpdate(to, from, next) {
this.trackEvent();
this.closeCommentForm();
// We need to reset the active discussion when opening a new design
this.updateActiveDiscussion();
next();
},
beforeRouteLeave(to, from, next) {
// We need to reset the active discussion when moving to design list view
this.updateActiveDiscussion();
next();
},
createImageDiffNoteMutation,
DESIGNS_ROUTE_NAME,
};
</script>
<template>
<div
class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
>
<gl-loading-icon v-if="isFirstLoading" size="xl" class="align-self-center" />
<template v-else>
<div class="d-flex overflow-hidden flex-grow-1 flex-column position-relative">
<design-destroyer
:filenames="[design.filename]"
:project-path="projectPath"
:iid="issueIid"
@done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })"
@error="onDesignDeleteError"
>
<template #default="{ mutate, loading }">
<toolbar
:id="id"
:is-deleting="loading"
:is-latest-version="isLatestVersion"
v-bind="design"
@delete="mutate"
/>
</template>
</design-destroyer>
<div v-if="errorMessage" class="p-3">
<gl-alert variant="danger" @dismiss="errorMessage = null">
{{ errorMessage }}
</gl-alert>
</div>
<design-presentation
:image="design.image"
:image-name="design.filename"
:discussions="discussions"
:is-annotating="isAnnotating"
:scale="scale"
@openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm"
@moveNote="onMoveNote"
/>
<div class="design-scaler-wrapper position-absolute mb-4 d-flex-center">
<design-scaler @scale="scale = $event" />
</div>
</div>
<div class="image-notes" @click="updateActiveDiscussion()">
<h2 class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0">
{{ issue.title }}
</h2>
<a class="text-tertiary text-decoration-none mb-3 d-block" :href="issue.webUrl">{{
issue.webPath
}}</a>
<participants
:participants="discussionParticipants"
:show-participant-label="false"
class="mb-4"
/>
<template v-if="renderDiscussions">
<design-discussion
v-for="(discussion, index) in discussions"
:key="discussion.id"
:discussion="discussion"
:design-id="id"
:noteable-id="design.id"
:discussion-index="index + 1"
:markdown-preview-path="markdownPreviewPath"
@error="onDesignDiscussionError"
@updateNoteError="onUpdateNoteError"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
/>
<apollo-mutation
v-if="annotationCoordinates"
#default="{ mutate, loading }"
:mutation="$options.createImageDiffNoteMutation"
:variables="{
input: mutationPayload,
}"
:update="addImageDiffNoteToStore"
@done="closeCommentForm"
@error="onCreateImageDiffNoteError"
>
<design-reply-form
v-model="comment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
@submitForm="mutate"
@cancelForm="closeCommentForm"
/>
</apollo-mutation>
</template>
<h2 v-else class="new-discussion-disclaimer gl-font-base m-0">
{{ __("Click the image where you'd like to start a new discussion") }}
</h2>
</div>
</template>
</div>
</template>