summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/design_management/pages/design/index.vue
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/design_management/pages/design/index.vue')
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue400
1 files changed, 400 insertions, 0 deletions
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
new file mode 100644
index 00000000000..7ff3271394d
--- /dev/null
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -0,0 +1,400 @@
+<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>