diff options
Diffstat (limited to 'app/assets/javascripts/design_management_new/components')
20 files changed, 0 insertions, 2538 deletions
diff --git a/app/assets/javascripts/design_management_new/components/app.vue b/app/assets/javascripts/design_management_new/components/app.vue deleted file mode 100644 index 98240aef810..00000000000 --- a/app/assets/javascripts/design_management_new/components/app.vue +++ /dev/null @@ -1,3 +0,0 @@ -<template> - <router-view /> -</template> diff --git a/app/assets/javascripts/design_management_new/components/delete_button.vue b/app/assets/javascripts/design_management_new/components/delete_button.vue deleted file mode 100644 index 77e1b97a227..00000000000 --- a/app/assets/javascripts/design_management_new/components/delete_button.vue +++ /dev/null @@ -1,81 +0,0 @@ -<script> -import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; -import { uniqueId } from 'lodash'; -import { s__ } from '~/locale'; - -export default { - name: 'DeleteButton', - components: { - GlButton, - GlModal, - }, - directives: { - GlModalDirective, - }, - props: { - isDeleting: { - type: Boolean, - required: false, - default: false, - }, - buttonClass: { - type: String, - required: false, - default: '', - }, - buttonVariant: { - type: String, - required: false, - default: 'info', - }, - buttonSize: { - type: String, - required: false, - default: 'medium', - }, - hasSelectedDesigns: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - modalId: uniqueId('design-deletion-confirmation-'), - }; - }, - modal: { - title: s__('DesignManagement|Delete designs confirmation'), - actionPrimary: { - text: s__('Delete'), - attributes: { variant: 'danger' }, - }, - actionCancel: { - text: s__('Cancel'), - }, - }, -}; -</script> - -<template> - <div class="gl-display-flex gl-align-items-center gl-h-full"> - <gl-modal - :modal-id="modalId" - :title="$options.modal.title" - :action-primary="$options.modal.actionPrimary" - :action-cancel="$options.modal.actionCancel" - @ok="$emit('deleteSelectedDesigns')" - > - <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p> - </gl-modal> - <gl-button - v-gl-modal-directive="modalId" - :variant="buttonVariant" - :size="buttonSize" - :class="buttonClass" - :disabled="isDeleting || !hasSelectedDesigns" - > - <slot></slot> - </gl-button> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_destroyer.vue b/app/assets/javascripts/design_management_new/components/design_destroyer.vue deleted file mode 100644 index 7ae569216f0..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_destroyer.vue +++ /dev/null @@ -1,67 +0,0 @@ -<script> -import { ApolloMutation } from 'vue-apollo'; -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import destroyDesignMutation from '../graphql/mutations/destroy_design.mutation.graphql'; -import { updateStoreAfterDesignsDelete } from '../utils/cache_update'; - -export default { - components: { - ApolloMutation, - }, - props: { - filenames: { - type: Array, - required: true, - }, - }, - inject: { - projectPath: { - default: '', - }, - iid: { - from: 'issueIid', - defaut: '', - }, - }, - computed: { - projectQueryBody() { - return { - query: getDesignListQuery, - variables: { fullPath: this.projectPath, iid: this.iid, atVersion: null }, - }; - }, - }, - methods: { - updateStoreAfterDelete( - store, - { - data: { designManagementDelete }, - }, - ) { - updateStoreAfterDesignsDelete( - store, - designManagementDelete, - this.projectQueryBody, - this.filenames, - ); - }, - }, - destroyDesignMutation, -}; -</script> - -<template> - <apollo-mutation - #default="{ mutate, loading, error }" - :mutation="$options.destroyDesignMutation" - :variables="{ - filenames, - projectPath, - iid, - }" - :update="updateStoreAfterDelete" - v-on="$listeners" - > - <slot v-bind="{ mutate, loading, error }"></slot> - </apollo-mutation> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_note_pin.vue b/app/assets/javascripts/design_management_new/components/design_note_pin.vue deleted file mode 100644 index 0811397fbad..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_note_pin.vue +++ /dev/null @@ -1,61 +0,0 @@ -<script> -import { __, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; - -export default { - name: 'DesignNotePin', - components: { - Icon, - }, - props: { - position: { - type: Object, - required: true, - }, - label: { - type: Number, - required: false, - default: null, - }, - repositioning: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - isNewNote() { - return this.label === null; - }, - pinStyle() { - return this.repositioning ? { ...this.position, cursor: 'move' } : this.position; - }, - pinLabel() { - return this.isNewNote - ? __('Comment form position') - : sprintf(__("Comment '%{label}' position"), { label: this.label }); - }, - }, -}; -</script> - -<template> - <button - :style="pinStyle" - :aria-label="pinLabel" - :class="{ - 'btn-transparent comment-indicator': isNewNote, - 'js-image-badge badge badge-pill': !isNewNote, - }" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center" - type="button" - @mousedown="$emit('mousedown', $event)" - @mouseup="$emit('mouseup', $event)" - @click="$emit('click', $event)" - > - <icon v-if="isNewNote" name="image-comment-dark" /> - <template v-else> - {{ label }} - </template> - </button> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management_new/components/design_notes/design_discussion.vue deleted file mode 100644 index 4aaf43e3a5b..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_notes/design_discussion.vue +++ /dev/null @@ -1,297 +0,0 @@ -<script> -import { ApolloMutation } from 'vue-apollo'; -import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import allVersionsMixin from '../../mixins/all_versions'; -import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql'; -import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql'; -import getDesignQuery from '../../graphql/queries/get_design.query.graphql'; -import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql'; -import DesignNote from './design_note.vue'; -import DesignReplyForm from './design_reply_form.vue'; -import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; -import ToggleRepliesWidget from './toggle_replies_widget.vue'; - -export default { - components: { - ApolloMutation, - DesignNote, - ReplyPlaceholder, - DesignReplyForm, - GlIcon, - GlLoadingIcon, - GlLink, - ToggleRepliesWidget, - TimeAgoTooltip, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - mixins: [allVersionsMixin], - props: { - discussion: { - type: Object, - required: true, - }, - noteableId: { - type: String, - required: true, - }, - designId: { - type: String, - required: true, - }, - markdownPreviewPath: { - type: String, - required: false, - default: '', - }, - resolvedDiscussionsExpanded: { - type: Boolean, - required: true, - }, - discussionWithOpenForm: { - type: String, - required: true, - }, - }, - apollo: { - activeDiscussion: { - query: activeDiscussionQuery, - result({ data }) { - const discussionId = data.activeDiscussion.id; - if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) { - return; - } - // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists - // We don't want scrollIntoView to be triggered from the discussion click itself - if ( - discussionId && - data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin && - discussionId === this.discussion.notes[0].id - ) { - this.$el.scrollIntoView({ - behavior: 'smooth', - inline: 'start', - }); - } - }, - }, - }, - data() { - return { - discussionComment: '', - isFormRendered: false, - activeDiscussion: {}, - isResolving: false, - shouldChangeResolvedStatus: false, - areRepliesCollapsed: this.discussion.resolved, - }; - }, - computed: { - mutationPayload() { - return { - noteableId: this.noteableId, - body: this.discussionComment, - discussionId: this.discussion.id, - }; - }, - designVariables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - filenames: [this.$route.params.id], - atVersion: this.designsVersion, - }; - }, - isDiscussionHighlighted() { - return this.discussion.notes[0].id === this.activeDiscussion.id; - }, - resolveCheckboxText() { - return this.discussion.resolved - ? s__('DesignManagement|Unresolve thread') - : s__('DesignManagement|Resolve thread'); - }, - firstNote() { - return this.discussion.notes[0]; - }, - discussionReplies() { - return this.discussion.notes.slice(1); - }, - areRepliesShown() { - return !this.discussion.resolved || !this.areRepliesCollapsed; - }, - resolveIconName() { - return this.discussion.resolved ? 'check-circle-filled' : 'check-circle'; - }, - isRepliesWidgetVisible() { - return this.discussion.resolved && this.discussionReplies.length > 0; - }, - isReplyPlaceholderVisible() { - return this.areRepliesShown || !this.discussionReplies.length; - }, - isFormVisible() { - return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id; - }, - }, - methods: { - addDiscussionComment( - store, - { - data: { createNote }, - }, - ) { - updateStoreAfterAddDiscussionComment( - store, - createNote, - getDesignQuery, - this.designVariables, - this.discussion.id, - ); - }, - onDone() { - this.discussionComment = ''; - this.hideForm(); - if (this.shouldChangeResolvedStatus) { - this.toggleResolvedStatus(); - } - }, - onCreateNoteError(err) { - this.$emit('createNoteError', err); - }, - hideForm() { - this.isFormRendered = false; - this.discussionComment = ''; - }, - showForm() { - this.$emit('openForm', this.discussion.id); - this.isFormRendered = true; - }, - toggleResolvedStatus() { - this.isResolving = true; - this.$apollo - .mutate({ - mutation: toggleResolveDiscussionMutation, - variables: { id: this.discussion.id, resolve: !this.discussion.resolved }, - }) - .then(({ data }) => { - if (data.errors?.length > 0) { - this.$emit('resolveDiscussionError', data.errors[0]); - } - }) - .catch(err => { - this.$emit('resolveDiscussionError', err); - }) - .finally(() => { - this.isResolving = false; - }); - }, - }, - createNoteMutation, -}; -</script> - -<template> - <div class="design-discussion-wrapper"> - <div - class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center" - :class="{ resolved: discussion.resolved }" - type="button" - > - {{ discussion.index }} - </div> - <ul - class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none" - data-qa-selector="design_discussion_content" - > - <design-note - :note="firstNote" - :markdown-preview-path="markdownPreviewPath" - :is-resolving="isResolving" - :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" - @error="$emit('updateNoteError', $event)" - > - <template v-if="discussion.resolvable" #resolveDiscussion> - <button - v-gl-tooltip - :class="{ 'is-active': discussion.resolved }" - :title="resolveCheckboxText" - :aria-label="resolveCheckboxText" - type="button" - class="line-resolve-btn note-action-button gl-mr-3" - data-testid="resolve-button" - @click.stop="toggleResolvedStatus" - > - <gl-icon v-if="!isResolving" :name="resolveIconName" data-testid="resolve-icon" /> - <gl-loading-icon v-else inline /> - </button> - </template> - <template v-if="discussion.resolved" #resolvedStatus> - <p class="gl-text-gray-700 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message"> - {{ __('Resolved by') }} - <gl-link - class="gl-text-gray-700 gl-text-decoration-none gl-font-sm link-inherit-color" - :href="discussion.resolvedBy.webUrl" - target="_blank" - >{{ discussion.resolvedBy.name }}</gl-link - > - <time-ago-tooltip :time="discussion.resolvedAt" tooltip-placement="bottom" /> - </p> - </template> - </design-note> - <toggle-replies-widget - v-if="isRepliesWidgetVisible" - :collapsed="areRepliesCollapsed" - :replies="discussionReplies" - @toggle="areRepliesCollapsed = !areRepliesCollapsed" - /> - <design-note - v-for="note in discussionReplies" - v-show="areRepliesShown" - :key="note.id" - :note="note" - :markdown-preview-path="markdownPreviewPath" - :is-resolving="isResolving" - :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" - @error="$emit('updateNoteError', $event)" - /> - <li v-show="isReplyPlaceholderVisible" class="reply-wrapper"> - <reply-placeholder - v-if="!isFormVisible" - class="qa-discussion-reply" - :button-text="__('Reply...')" - @onClick="showForm" - /> - <apollo-mutation - v-else - #default="{ mutate, loading }" - :mutation="$options.createNoteMutation" - :variables="{ - input: mutationPayload, - }" - :update="addDiscussionComment" - @done="onDone" - @error="onCreateNoteError" - > - <design-reply-form - v-model="discussionComment" - :is-saving="loading" - :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="hideForm" - > - <template v-if="discussion.resolvable" #resolveCheckbox> - <label data-testid="resolve-checkbox"> - <input v-model="shouldChangeResolvedStatus" type="checkbox" /> - {{ resolveCheckboxText }} - </label> - </template> - </design-reply-form> - </apollo-mutation> - </li> - </ul> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue b/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue deleted file mode 100644 index 172e61920ef..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue +++ /dev/null @@ -1,156 +0,0 @@ -<script> -import { ApolloMutation } from 'vue-apollo'; -import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import DesignReplyForm from './design_reply_form.vue'; -import { findNoteId } from '../../utils/design_management_utils'; -import { hasErrors } from '../../utils/cache_update'; - -export default { - components: { - UserAvatarLink, - TimelineEntryItem, - TimeAgoTooltip, - DesignReplyForm, - ApolloMutation, - GlIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - note: { - type: Object, - required: true, - }, - markdownPreviewPath: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - noteText: this.note.body, - isEditing: false, - }; - }, - computed: { - author() { - return this.note.author; - }, - noteAnchorId() { - return findNoteId(this.note.id); - }, - isNoteLinked() { - return this.$route.hash === `#note_${this.noteAnchorId}`; - }, - mutationPayload() { - return { - id: this.note.id, - body: this.noteText, - }; - }, - isEditButtonVisible() { - return !this.isEditing && this.note.userPermissions.adminNote; - }, - }, - mounted() { - if (this.isNoteLinked) { - this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' }); - } - }, - methods: { - hideForm() { - this.isEditing = false; - this.noteText = this.note.body; - }, - onDone({ data }) { - this.hideForm(); - if (hasErrors(data.updateNote)) { - this.$emit('error', data.errors[0]); - } - }, - }, - updateNoteMutation, -}; -</script> - -<template> - <timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form"> - <user-avatar-link - :link-href="author.webUrl" - :img-src="author.avatarUrl" - :img-alt="author.username" - :img-size="40" - /> - <div class="d-flex justify-content-between"> - <div> - <a - v-once - :href="author.webUrl" - class="js-user-link" - :data-user-id="author.id" - :data-username="author.username" - > - <span class="note-header-author-name bold">{{ author.name }}</span> - <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> - <span class="note-headline-light">@{{ author.username }}</span> - </a> - <span class="note-headline-light note-headline-meta"> - <span class="system-note-message"> <slot></slot> </span> - <template v-if="note.createdAt"> - <span class="system-note-separator"></span> - <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`"> - <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> - </a> - </template> - </span> - </div> - <div class="gl-display-flex"> - <slot name="resolveDiscussion"></slot> - <button - v-if="isEditButtonVisible" - v-gl-tooltip - type="button" - :title="__('Edit comment')" - class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" - @click="isEditing = true" - > - <gl-icon name="pencil" class="link-highlight" /> - </button> - </div> - </div> - <template v-if="!isEditing"> - <div - class="note-text js-note-text md" - data-qa-selector="note_content" - v-html="note.bodyHtml" - ></div> - <slot name="resolvedStatus"></slot> - </template> - <apollo-mutation - v-else - #default="{ mutate, loading }" - :mutation="$options.updateNoteMutation" - :variables="{ - input: mutationPayload, - }" - @error="$emit('error', $event)" - @done="onDone" - > - <design-reply-form - v-model="noteText" - :is-saving="loading" - :markdown-preview-path="markdownPreviewPath" - :is-new-comment="false" - class="mt-5" - @submitForm="mutate" - @cancelForm="hideForm" - /> - </apollo-mutation> - </timeline-entry-item> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management_new/components/design_notes/design_reply_form.vue deleted file mode 100644 index 969034909f2..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_notes/design_reply_form.vue +++ /dev/null @@ -1,141 +0,0 @@ -<script> -import { GlDeprecatedButton, GlModal } from '@gitlab/ui'; -import MarkdownField from '~/vue_shared/components/markdown/field.vue'; -import { s__ } from '~/locale'; - -export default { - name: 'DesignReplyForm', - components: { - MarkdownField, - GlDeprecatedButton, - GlModal, - }, - props: { - markdownPreviewPath: { - type: String, - required: false, - default: '', - }, - value: { - type: String, - required: true, - }, - isSaving: { - type: Boolean, - required: true, - }, - isNewComment: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - formText: this.value, - }; - }, - computed: { - hasValue() { - return this.value.trim().length > 0; - }, - modalSettings() { - if (this.isNewComment) { - return { - title: s__('DesignManagement|Cancel comment confirmation'), - okTitle: s__('DesignManagement|Discard comment'), - cancelTitle: s__('DesignManagement|Keep comment'), - content: s__('DesignManagement|Are you sure you want to cancel creating this comment?'), - }; - } - return { - title: s__('DesignManagement|Cancel comment update confirmation'), - okTitle: s__('DesignManagement|Cancel changes'), - cancelTitle: s__('DesignManagement|Keep changes'), - content: s__('DesignManagement|Are you sure you want to cancel changes to this comment?'), - }; - }, - buttonText() { - return this.isNewComment - ? s__('DesignManagement|Comment') - : s__('DesignManagement|Save comment'); - }, - }, - mounted() { - this.focusInput(); - }, - methods: { - submitForm() { - if (this.hasValue) this.$emit('submitForm'); - }, - cancelComment() { - if (this.hasValue && this.formText !== this.value) { - this.$refs.cancelCommentModal.show(); - } else { - this.$emit('cancelForm'); - } - }, - focusInput() { - this.$refs.textarea.focus(); - }, - }, -}; -</script> - -<template> - <form class="new-note common-note-form" @submit.prevent> - <markdown-field - :markdown-preview-path="markdownPreviewPath" - :can-attach-file="false" - :enable-autocomplete="true" - :textarea-value="value" - markdown-docs-path="/help/user/markdown" - class="bordered-box" - > - <template #textarea> - <textarea - ref="textarea" - :value="value" - class="note-textarea js-gfm-input js-autosize markdown-area" - dir="auto" - data-supports-quick-actions="false" - data-qa-selector="note_textarea" - :aria-label="__('Description')" - :placeholder="__('Write a comment…')" - @input="$emit('input', $event.target.value)" - @keydown.meta.enter="submitForm" - @keydown.ctrl.enter="submitForm" - @keyup.esc.stop="cancelComment" - > - </textarea> - </template> - </markdown-field> - <slot name="resolveCheckbox"></slot> - <div class="note-form-actions gl-display-flex gl-justify-content-space-between"> - <gl-deprecated-button - ref="submitButton" - :disabled="!hasValue || isSaving" - variant="success" - type="submit" - data-track-event="click_button" - data-qa-selector="save_comment_button" - @click="$emit('submitForm')" - > - {{ buttonText }} - </gl-deprecated-button> - <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{ - __('Cancel') - }}</gl-deprecated-button> - </div> - <gl-modal - ref="cancelCommentModal" - ok-variant="danger" - :title="modalSettings.title" - :ok-title="modalSettings.okTitle" - :cancel-title="modalSettings.cancelTitle" - modal-id="cancel-comment-modal" - @ok="$emit('cancelForm')" - >{{ modalSettings.content }} - </gl-modal> - </form> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_notes/toggle_replies_widget.vue b/app/assets/javascripts/design_management_new/components/design_notes/toggle_replies_widget.vue deleted file mode 100644 index 46c73e3eea8..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_notes/toggle_replies_widget.vue +++ /dev/null @@ -1,70 +0,0 @@ -<script> -import { GlIcon, GlButton, GlLink } from '@gitlab/ui'; -import { __, n__ } from '~/locale'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; - -export default { - name: 'ToggleNotesWidget', - components: { - GlIcon, - GlButton, - GlLink, - TimeAgoTooltip, - }, - props: { - collapsed: { - type: Boolean, - required: true, - }, - replies: { - type: Array, - required: true, - }, - }, - computed: { - lastReply() { - return this.replies[this.replies.length - 1]; - }, - iconName() { - return this.collapsed ? 'chevron-right' : 'chevron-down'; - }, - toggleText() { - return this.collapsed - ? `${this.replies.length} ${n__('reply', 'replies', this.replies.length)}` - : __('Collapse replies'); - }, - }, -}; -</script> - -<template> - <li - class="toggle-comments gl-bg-gray-50 gl-display-flex gl-align-items-center gl-py-3" - :class="{ expanded: !collapsed }" - data-testid="toggle-comments-wrapper" - > - <gl-icon :name="iconName" class="gl-ml-3" @click.stop="$emit('toggle')" /> - <gl-button - variant="link" - class="toggle-comments-button gl-ml-2 gl-mr-2" - @click.stop="$emit('toggle')" - > - {{ toggleText }} - </gl-button> - <template v-if="collapsed"> - <span class="gl-text-gray-700">{{ __('Last reply by') }}</span> - <gl-link - :href="lastReply.author.webUrl" - target="_blank" - class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2" - > - {{ lastReply.author.name }} - </gl-link> - <time-ago-tooltip - :time="lastReply.createdAt" - tooltip-placement="bottom" - class="gl-text-gray-700" - /> - </template> - </li> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_overlay.vue b/app/assets/javascripts/design_management_new/components/design_overlay.vue deleted file mode 100644 index 926e7c74802..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_overlay.vue +++ /dev/null @@ -1,287 +0,0 @@ -<script> -import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql'; -import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql'; -import DesignNotePin from './design_note_pin.vue'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants'; - -export default { - name: 'DesignOverlay', - components: { - DesignNotePin, - }, - props: { - dimensions: { - type: Object, - required: true, - }, - position: { - type: Object, - required: true, - }, - notes: { - type: Array, - required: false, - default: () => [], - }, - currentCommentForm: { - type: Object, - required: false, - default: null, - }, - disableCommenting: { - type: Boolean, - required: false, - default: false, - }, - resolvedDiscussionsExpanded: { - type: Boolean, - required: true, - }, - }, - apollo: { - activeDiscussion: { - query: activeDiscussionQuery, - }, - }, - data() { - return { - movingNoteNewPosition: null, - movingNoteStartPosition: null, - activeDiscussion: {}, - }; - }, - computed: { - overlayStyle() { - const cursor = this.disableCommenting ? 'unset' : undefined; - - return { - cursor, - width: `${this.dimensions.width}px`, - height: `${this.dimensions.height}px`, - ...this.position, - }; - }, - isMovingCurrentComment() { - return Boolean(this.movingNoteStartPosition && !this.movingNoteStartPosition.noteId); - }, - currentCommentPositionStyle() { - return this.isMovingCurrentComment && this.movingNoteNewPosition - ? this.getNotePositionStyle(this.movingNoteNewPosition) - : this.getNotePositionStyle(this.currentCommentForm); - }, - }, - methods: { - setNewNoteCoordinates({ x, y }) { - this.$emit('openCommentForm', { x, y }); - }, - getNoteRelativePosition(position) { - const { x, y, width, height } = position; - const widthRatio = this.dimensions.width / width; - const heightRatio = this.dimensions.height / height; - return { - left: Math.round(x * widthRatio), - top: Math.round(y * heightRatio), - }; - }, - getNotePositionStyle(position) { - const { left, top } = this.getNoteRelativePosition(position); - return { - left: `${left}px`, - top: `${top}px`, - }; - }, - getMovingNotePositionDelta(e) { - let deltaX = 0; - let deltaY = 0; - - if (this.movingNoteStartPosition) { - const { clientX, clientY } = this.movingNoteStartPosition; - deltaX = e.clientX - clientX; - deltaY = e.clientY - clientY; - } - - return { - deltaX, - deltaY, - }; - }, - isMovingNote(noteId) { - const movingNoteId = this.movingNoteStartPosition?.noteId; - return Boolean(movingNoteId && movingNoteId === noteId); - }, - canMoveNote(note) { - const { userPermissions } = note; - const { adminNote } = userPermissions || {}; - - return Boolean(adminNote); - }, - isPositionInOverlay(position) { - const { top, left } = this.getNoteRelativePosition(position); - const { height, width } = this.dimensions; - - return top >= 0 && top <= height && left >= 0 && left <= width; - }, - onNewNoteMove(e) { - if (!this.isMovingCurrentComment) return; - - const { deltaX, deltaY } = this.getMovingNotePositionDelta(e); - const x = this.currentCommentForm.x + deltaX; - const y = this.currentCommentForm.y + deltaY; - - const movingNoteNewPosition = { - x, - y, - width: this.dimensions.width, - height: this.dimensions.height, - }; - - if (!this.isPositionInOverlay(movingNoteNewPosition)) { - this.onNewNoteMouseup(); - return; - } - - this.movingNoteNewPosition = movingNoteNewPosition; - }, - onExistingNoteMove(e) { - const note = this.notes.find(({ id }) => id === this.movingNoteStartPosition.noteId); - if (!note || !this.canMoveNote(note)) return; - - const { position } = note; - const { width, height } = position; - const widthRatio = this.dimensions.width / width; - const heightRatio = this.dimensions.height / height; - - const { deltaX, deltaY } = this.getMovingNotePositionDelta(e); - const x = position.x * widthRatio + deltaX; - const y = position.y * heightRatio + deltaY; - - const movingNoteNewPosition = { - x, - y, - width: this.dimensions.width, - height: this.dimensions.height, - }; - - if (!this.isPositionInOverlay(movingNoteNewPosition)) { - this.onExistingNoteMouseup(); - return; - } - - this.movingNoteNewPosition = movingNoteNewPosition; - }, - onNewNoteMouseup() { - if (!this.movingNoteNewPosition) return; - - const { x, y } = this.movingNoteNewPosition; - this.setNewNoteCoordinates({ x, y }); - }, - onExistingNoteMouseup(note) { - if (!this.movingNoteStartPosition || !this.movingNoteNewPosition) { - this.updateActiveDiscussion(note.id); - this.$emit('closeCommentForm'); - return; - } - - const { x, y } = this.movingNoteNewPosition; - this.$emit('moveNote', { - noteId: this.movingNoteStartPosition.noteId, - discussionId: this.movingNoteStartPosition.discussionId, - coordinates: { x, y }, - }); - }, - onNoteMousedown({ clientX, clientY }, note) { - this.movingNoteStartPosition = { - noteId: note?.id, - discussionId: note?.discussion.id, - clientX, - clientY, - }; - }, - onOverlayMousemove(e) { - if (!this.movingNoteStartPosition) return; - - if (this.isMovingCurrentComment) { - this.onNewNoteMove(e); - } else { - this.onExistingNoteMove(e); - } - }, - onNoteMouseup(note) { - if (!this.movingNoteStartPosition) return; - - if (this.isMovingCurrentComment) { - this.onNewNoteMouseup(); - } else { - this.onExistingNoteMouseup(note); - } - - this.movingNoteStartPosition = null; - this.movingNoteNewPosition = null; - }, - onAddCommentMouseup({ offsetX, offsetY }) { - if (this.disableCommenting) return; - if (this.activeDiscussion.id) { - this.updateActiveDiscussion(); - } - - this.setNewNoteCoordinates({ x: offsetX, y: offsetY }); - }, - updateActiveDiscussion(id) { - this.$apollo.mutate({ - mutation: updateActiveDiscussionMutation, - variables: { - id, - source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin, - }, - }); - }, - isNoteInactive(note) { - return this.activeDiscussion.id && this.activeDiscussion.id !== note.id; - }, - designPinClass(note) { - return { inactive: this.isNoteInactive(note), resolved: note.resolved }; - }, - }, -}; -</script> - -<template> - <div - class="position-absolute image-diff-overlay frame" - :style="overlayStyle" - @mousemove="onOverlayMousemove" - @mouseleave="onNoteMouseup" - > - <button - v-show="!disableCommenting" - type="button" - class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button" - data-qa-selector="design_image_button" - @mouseup="onAddCommentMouseup" - ></button> - <template v-for="note in notes"> - <design-note-pin - v-if="resolvedDiscussionsExpanded || !note.resolved" - :key="note.id" - :label="note.index" - :repositioning="isMovingNote(note.id)" - :position=" - isMovingNote(note.id) && movingNoteNewPosition - ? getNotePositionStyle(movingNoteNewPosition) - : getNotePositionStyle(note.position) - " - :class="designPinClass(note)" - @mousedown.stop="onNoteMousedown($event, note)" - @mouseup.stop="onNoteMouseup(note)" - /> - </template> - - <design-note-pin - v-if="currentCommentForm" - :position="currentCommentPositionStyle" - :repositioning="isMovingCurrentComment" - @mousedown.stop="onNoteMousedown" - @mouseup.stop="onNoteMouseup" - /> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_presentation.vue b/app/assets/javascripts/design_management_new/components/design_presentation.vue deleted file mode 100644 index 84dbb2809d9..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_presentation.vue +++ /dev/null @@ -1,322 +0,0 @@ -<script> -import { throttle } from 'lodash'; -import DesignImage from './image.vue'; -import DesignOverlay from './design_overlay.vue'; - -const CLICK_DRAG_BUFFER_PX = 2; - -export default { - components: { - DesignImage, - DesignOverlay, - }, - props: { - image: { - type: String, - required: false, - default: '', - }, - imageName: { - type: String, - required: false, - default: '', - }, - discussions: { - type: Array, - required: true, - }, - isAnnotating: { - type: Boolean, - required: false, - default: false, - }, - scale: { - type: Number, - required: false, - default: 1, - }, - resolvedDiscussionsExpanded: { - type: Boolean, - required: true, - }, - }, - data() { - return { - overlayDimensions: null, - overlayPosition: null, - currentAnnotationPosition: null, - zoomFocalPoint: { - x: 0, - y: 0, - width: 0, - height: 0, - }, - initialLoad: true, - lastDragPosition: null, - isDraggingDesign: false, - }; - }, - computed: { - discussionStartingNotes() { - return this.discussions.map(discussion => ({ - ...discussion.notes[0], - index: discussion.index, - })); - }, - currentCommentForm() { - return (this.isAnnotating && this.currentAnnotationPosition) || null; - }, - presentationStyle() { - return { - cursor: this.isDraggingDesign ? 'grabbing' : undefined, - }; - }, - }, - beforeDestroy() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return; - - presentationViewport.removeEventListener('scroll', this.scrollThrottled, false); - }, - mounted() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return; - - this.scrollThrottled = throttle(() => { - this.shiftZoomFocalPoint(); - }, 400); - - presentationViewport.addEventListener('scroll', this.scrollThrottled, false); - }, - methods: { - syncCurrentAnnotationPosition() { - if (!this.currentAnnotationPosition) return; - - const widthRatio = this.overlayDimensions.width / this.currentAnnotationPosition.width; - const heightRatio = this.overlayDimensions.height / this.currentAnnotationPosition.height; - const x = this.currentAnnotationPosition.x * widthRatio; - const y = this.currentAnnotationPosition.y * heightRatio; - - this.currentAnnotationPosition = this.getAnnotationPositon({ x, y }); - }, - setOverlayDimensions(overlayDimensions) { - this.overlayDimensions = overlayDimensions; - - // every time we set overlay dimensions, we need to - // update the current annotation as well - this.syncCurrentAnnotationPosition(); - }, - setOverlayPosition() { - if (!this.overlayDimensions) { - this.overlayPosition = {}; - } - - const { presentationViewport } = this.$refs; - if (!presentationViewport) return; - - // default to center - this.overlayPosition = { - left: `calc(50% - ${this.overlayDimensions.width / 2}px)`, - top: `calc(50% - ${this.overlayDimensions.height / 2}px)`, - }; - - // if the overlay overflows, then don't center - if (this.overlayDimensions.width > presentationViewport.offsetWidth) { - this.overlayPosition.left = '0'; - } - if (this.overlayDimensions.height > presentationViewport.offsetHeight) { - this.overlayPosition.top = '0'; - } - }, - /** - * Return a point that represents the center of an - * overflowing child element w.r.t it's parent - */ - getViewportCenter() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return {}; - - // get height of scroll bars (i.e. the max values for scrollTop, scrollLeft) - const scrollBarWidth = presentationViewport.scrollWidth - presentationViewport.offsetWidth; - const scrollBarHeight = presentationViewport.scrollHeight - presentationViewport.offsetHeight; - - // determine how many child pixels have been scrolled - const xScrollRatio = - presentationViewport.scrollLeft > 0 ? presentationViewport.scrollLeft / scrollBarWidth : 0; - const yScrollRatio = - presentationViewport.scrollTop > 0 ? presentationViewport.scrollTop / scrollBarHeight : 0; - const xScrollOffset = - (presentationViewport.scrollWidth - presentationViewport.offsetWidth - 0) * xScrollRatio; - const yScrollOffset = - (presentationViewport.scrollHeight - presentationViewport.offsetHeight - 0) * yScrollRatio; - - const viewportCenterX = presentationViewport.offsetWidth / 2; - const viewportCenterY = presentationViewport.offsetHeight / 2; - const focalPointX = viewportCenterX + xScrollOffset; - const focalPointY = viewportCenterY + yScrollOffset; - - return { - x: focalPointX, - y: focalPointY, - }; - }, - /** - * Scroll the viewport such that the focal point is positioned centrally - */ - scrollToFocalPoint() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return; - - const scrollX = this.zoomFocalPoint.x - presentationViewport.offsetWidth / 2; - const scrollY = this.zoomFocalPoint.y - presentationViewport.offsetHeight / 2; - - presentationViewport.scrollTo(scrollX, scrollY); - }, - scaleZoomFocalPoint() { - const { x, y, width, height } = this.zoomFocalPoint; - const widthRatio = this.overlayDimensions.width / width; - const heightRatio = this.overlayDimensions.height / height; - - this.zoomFocalPoint = { - x: Math.round(x * widthRatio * 100) / 100, - y: Math.round(y * heightRatio * 100) / 100, - ...this.overlayDimensions, - }; - }, - shiftZoomFocalPoint() { - this.zoomFocalPoint = { - ...this.getViewportCenter(), - ...this.overlayDimensions, - }; - }, - onImageResize(imageDimensions) { - this.setOverlayDimensions(imageDimensions); - this.setOverlayPosition(); - - this.$nextTick(() => { - if (this.initialLoad) { - // set focal point on initial load - this.shiftZoomFocalPoint(); - this.initialLoad = false; - } else { - this.scaleZoomFocalPoint(); - this.scrollToFocalPoint(); - } - }); - }, - getAnnotationPositon(coordinates) { - const { x, y } = coordinates; - const { width, height } = this.overlayDimensions; - return { - x: Math.round(x), - y: Math.round(y), - width: Math.round(width), - height: Math.round(height), - }; - }, - openCommentForm(coordinates) { - this.currentAnnotationPosition = this.getAnnotationPositon(coordinates); - this.$emit('openCommentForm', this.currentAnnotationPosition); - }, - closeCommentForm() { - this.currentAnnotationPosition = null; - this.$emit('closeCommentForm'); - }, - moveNote({ noteId, discussionId, coordinates }) { - const position = this.getAnnotationPositon(coordinates); - this.$emit('moveNote', { noteId, discussionId, position }); - }, - onPresentationMousedown({ clientX, clientY }) { - if (!this.isDesignOverflowing()) return; - - this.lastDragPosition = { - x: clientX, - y: clientY, - }; - }, - getDragDelta(clientX, clientY) { - return { - deltaX: this.lastDragPosition.x - clientX, - deltaY: this.lastDragPosition.y - clientY, - }; - }, - exceedsDragThreshold(clientX, clientY) { - const { deltaX, deltaY } = this.getDragDelta(clientX, clientY); - - return Math.abs(deltaX) > CLICK_DRAG_BUFFER_PX || Math.abs(deltaY) > CLICK_DRAG_BUFFER_PX; - }, - shouldDragDesign(clientX, clientY) { - return ( - this.lastDragPosition && - (this.isDraggingDesign || this.exceedsDragThreshold(clientX, clientY)) - ); - }, - onPresentationMousemove({ clientX, clientY }) { - const { presentationViewport } = this.$refs; - if (!presentationViewport || !this.shouldDragDesign(clientX, clientY)) return; - - this.isDraggingDesign = true; - - const { scrollLeft, scrollTop } = presentationViewport; - const { deltaX, deltaY } = this.getDragDelta(clientX, clientY); - presentationViewport.scrollTo(scrollLeft + deltaX, scrollTop + deltaY); - - this.lastDragPosition = { - x: clientX, - y: clientY, - }; - }, - onPresentationMouseup() { - this.lastDragPosition = null; - this.isDraggingDesign = false; - }, - isDesignOverflowing() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return false; - - return ( - presentationViewport.scrollWidth > presentationViewport.offsetWidth || - presentationViewport.scrollHeight > presentationViewport.offsetHeight - ); - }, - }, -}; -</script> - -<template> - <div - ref="presentationViewport" - class="h-100 w-100 p-3 overflow-auto position-relative" - :style="presentationStyle" - @mousedown="onPresentationMousedown" - @mousemove="onPresentationMousemove" - @mouseup="onPresentationMouseup" - @mouseleave="onPresentationMouseup" - @touchstart="onPresentationMousedown" - @touchmove="onPresentationMousemove" - @touchend="onPresentationMouseup" - @touchcancel="onPresentationMouseup" - > - <div class="h-100 w-100 d-flex align-items-center position-relative"> - <design-image - v-if="image" - :image="image" - :name="imageName" - :scale="scale" - @resize="onImageResize" - /> - <design-overlay - v-if="overlayDimensions && overlayPosition" - :dimensions="overlayDimensions" - :position="overlayPosition" - :notes="discussionStartingNotes" - :current-comment-form="currentCommentForm" - :disable-commenting="isDraggingDesign" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - @openCommentForm="openCommentForm" - @closeCommentForm="closeCommentForm" - @moveNote="moveNote" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_scaler.vue b/app/assets/javascripts/design_management_new/components/design_scaler.vue deleted file mode 100644 index 55dee74bef5..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_scaler.vue +++ /dev/null @@ -1,65 +0,0 @@ -<script> -import { GlIcon } from '@gitlab/ui'; - -const SCALE_STEP_SIZE = 0.2; -const DEFAULT_SCALE = 1; -const MIN_SCALE = 1; -const MAX_SCALE = 2; - -export default { - components: { - GlIcon, - }, - data() { - return { - scale: DEFAULT_SCALE, - }; - }, - computed: { - disableReset() { - return this.scale <= MIN_SCALE; - }, - disableDecrease() { - return this.scale === DEFAULT_SCALE; - }, - disableIncrease() { - return this.scale >= MAX_SCALE; - }, - }, - methods: { - setScale(scale) { - if (scale < MIN_SCALE) { - return; - } - - this.scale = Math.round(scale * 100) / 100; - this.$emit('scale', this.scale); - }, - incrementScale() { - this.setScale(this.scale + SCALE_STEP_SIZE); - }, - decrementScale() { - this.setScale(this.scale - SCALE_STEP_SIZE); - }, - resetScale() { - this.setScale(DEFAULT_SCALE); - }, - }, -}; -</script> - -<template> - <div class="design-scaler btn-group" role="group"> - <button class="btn" :disabled="disableDecrease" @click="decrementScale"> - <span class="d-flex-center gl-icon s16"> - – - </span> - </button> - <button class="btn" :disabled="disableReset" @click="resetScale"> - <gl-icon name="redo" /> - </button> - <button class="btn" :disabled="disableIncrease" @click="incrementScale"> - <gl-icon name="plus" /> - </button> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/design_sidebar.vue b/app/assets/javascripts/design_management_new/components/design_sidebar.vue deleted file mode 100644 index 333ad2557e8..00000000000 --- a/app/assets/javascripts/design_management_new/components/design_sidebar.vue +++ /dev/null @@ -1,178 +0,0 @@ -<script> -import { s__ } from '~/locale'; -import Cookies from 'js-cookie'; -import { parseBoolean } from '~/lib/utils/common_utils'; -import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui'; -import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql'; -import { extractDiscussions, extractParticipants } from '../utils/design_management_utils'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants'; -import DesignDiscussion from './design_notes/design_discussion.vue'; -import Participants from '~/sidebar/components/participants/participants.vue'; - -export default { - components: { - DesignDiscussion, - Participants, - GlCollapse, - GlButton, - GlPopover, - }, - props: { - design: { - type: Object, - required: true, - }, - resolvedDiscussionsExpanded: { - type: Boolean, - required: true, - }, - markdownPreviewPath: { - type: String, - required: true, - }, - }, - data() { - return { - isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)), - discussionWithOpenForm: '', - }; - }, - computed: { - discussions() { - return extractDiscussions(this.design.discussions); - }, - issue() { - return { - ...this.design.issue, - webPath: this.design.issue.webPath.substr(1), - }; - }, - discussionParticipants() { - return extractParticipants(this.issue.participants); - }, - resolvedDiscussions() { - return this.discussions.filter(discussion => discussion.resolved); - }, - unresolvedDiscussions() { - return this.discussions.filter(discussion => !discussion.resolved); - }, - resolvedCommentsToggleIcon() { - return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right'; - }, - }, - methods: { - handleSidebarClick() { - this.isResolvedCommentsPopoverHidden = true; - Cookies.set(this.$options.cookieKey, 'true', { expires: 365 * 10 }); - this.updateActiveDiscussion(); - }, - updateActiveDiscussion(id) { - this.$apollo.mutate({ - mutation: updateActiveDiscussionMutation, - variables: { - id, - source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion, - }, - }); - }, - closeCommentForm() { - this.comment = ''; - this.$emit('closeCommentForm'); - }, - updateDiscussionWithOpenForm(id) { - this.discussionWithOpenForm = id; - }, - }, - resolveCommentsToggleText: s__('DesignManagement|Resolved Comments'), - cookieKey: 'hide_design_resolved_comments_popover', -}; -</script> - -<template> - <div class="image-notes" @click="handleSidebarClick"> - <h2 class="gl-font-weight-bold gl-mt-0"> - {{ issue.title }} - </h2> - <a - class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block" - :href="issue.webUrl" - >{{ issue.webPath }}</a - > - <participants - :participants="discussionParticipants" - :show-participant-label="false" - class="gl-mb-4" - /> - <h2 - v-if="unresolvedDiscussions.length === 0" - class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4" - data-testid="new-discussion-disclaimer" - > - {{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }} - </h2> - <design-discussion - v-for="discussion in unresolvedDiscussions" - :key="discussion.id" - :discussion="discussion" - :design-id="$route.params.id" - :noteable-id="design.id" - :markdown-preview-path="markdownPreviewPath" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - :discussion-with-open-form="discussionWithOpenForm" - data-testid="unresolved-discussion" - @createNoteError="$emit('onDesignDiscussionError', $event)" - @updateNoteError="$emit('updateNoteError', $event)" - @resolveDiscussionError="$emit('resolveDiscussionError', $event)" - @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" - @openForm="updateDiscussionWithOpenForm" - /> - <template v-if="resolvedDiscussions.length > 0"> - <gl-button - id="resolved-comments" - data-testid="resolved-comments" - :icon="resolvedCommentsToggleIcon" - variant="link" - class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4" - @click="$emit('toggleResolvedComments')" - >{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }}) - </gl-button> - <gl-popover - v-if="!isResolvedCommentsPopoverHidden" - :show="!isResolvedCommentsPopoverHidden" - target="resolved-comments" - container="popovercontainer" - placement="top" - :title="s__('DesignManagement|Resolved Comments')" - > - <p> - {{ - s__( - 'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below', - ) - }} - </p> - <a href="#" rel="noopener noreferrer" target="_blank">{{ - s__('DesignManagement|Learn more about resolving comments') - }}</a> - </gl-popover> - <gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3"> - <design-discussion - v-for="discussion in resolvedDiscussions" - :key="discussion.id" - :discussion="discussion" - :design-id="$route.params.id" - :noteable-id="design.id" - :markdown-preview-path="markdownPreviewPath" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - :discussion-with-open-form="discussionWithOpenForm" - data-testid="resolved-discussion" - @error="$emit('onDesignDiscussionError', $event)" - @updateNoteError="$emit('updateNoteError', $event)" - @openForm="updateDiscussionWithOpenForm" - @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" - /> - </gl-collapse> - </template> - <slot name="replyForm"></slot> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/image.vue b/app/assets/javascripts/design_management_new/components/image.vue deleted file mode 100644 index 91b7b576e0c..00000000000 --- a/app/assets/javascripts/design_management_new/components/image.vue +++ /dev/null @@ -1,110 +0,0 @@ -<script> -import { throttle } from 'lodash'; -import { GlIcon } from '@gitlab/ui'; - -export default { - components: { - GlIcon, - }, - props: { - image: { - type: String, - required: false, - default: '', - }, - name: { - type: String, - required: false, - default: '', - }, - scale: { - type: Number, - required: false, - default: 1, - }, - }, - data() { - return { - baseImageSize: null, - imageStyle: null, - imageError: false, - }; - }, - watch: { - scale(val) { - this.zoom(val); - }, - }, - beforeDestroy() { - window.removeEventListener('resize', this.resizeThrottled, false); - }, - mounted() { - this.onImgLoad(); - - this.resizeThrottled = throttle(() => { - // NOTE: if imageStyle is set, then baseImageSize - // won't change due to resize. We must still emit a - // `resize` event so that the parent can handle - // resizes appropriately (e.g. for design_overlay) - this.setBaseImageSize(); - }, 400); - window.addEventListener('resize', this.resizeThrottled, false); - }, - methods: { - onImgLoad() { - requestIdleCallback(this.setBaseImageSize, { timeout: 1000 }); - }, - onImgError() { - this.imageError = true; - }, - setBaseImageSize() { - const { contentImg } = this.$refs; - if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return; - - this.baseImageSize = { - height: contentImg.offsetHeight, - width: contentImg.offsetWidth, - }; - this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height }); - }, - onResize({ width, height }) { - this.$emit('resize', { width, height }); - }, - zoom(amount) { - if (amount === 1) { - this.imageStyle = null; - this.$nextTick(() => { - this.setBaseImageSize(); - }); - return; - } - const width = this.baseImageSize.width * amount; - const height = this.baseImageSize.height * amount; - - this.imageStyle = { - width: `${width}px`, - height: `${height}px`, - }; - - this.onResize({ width, height }); - }, - }, -}; -</script> - -<template> - <div class="m-auto js-design-image"> - <gl-icon v-if="imageError" class="text-secondary-100" name="media-broken" :size="48" /> - <img - v-show="!imageError" - ref="contentImg" - class="mh-100" - :src="image" - :alt="name" - :style="imageStyle" - :class="{ 'img-fluid': !imageStyle }" - @error="onImgError" - @load="onImgLoad" - /> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/list/item.vue b/app/assets/javascripts/design_management_new/components/list/item.vue deleted file mode 100644 index b19aef9c22d..00000000000 --- a/app/assets/javascripts/design_management_new/components/list/item.vue +++ /dev/null @@ -1,174 +0,0 @@ -<script> -import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; -import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; -import { n__, __ } from '~/locale'; -import { DESIGN_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - GlLoadingIcon, - GlIntersectionObserver, - GlIcon, - Icon, - Timeago, - }, - props: { - id: { - type: [Number, String], - required: true, - }, - event: { - type: String, - required: true, - }, - notesCount: { - type: Number, - required: true, - }, - image: { - type: String, - required: true, - }, - filename: { - type: String, - required: true, - }, - updatedAt: { - type: String, - required: false, - default: null, - }, - isUploading: { - type: Boolean, - required: false, - default: true, - }, - imageV432x230: { - type: String, - required: false, - default: null, - }, - }, - data() { - return { - imageLoading: true, - imageError: false, - wasInView: false, - }; - }, - computed: { - icon() { - const normalizedEvent = this.event.toLowerCase(); - const icons = { - creation: { - name: 'file-addition-solid', - classes: 'text-success-500', - tooltip: __('Added in this version'), - }, - modification: { - name: 'file-modified-solid', - classes: 'text-primary-500', - tooltip: __('Modified in this version'), - }, - deletion: { - name: 'file-deletion-solid', - classes: 'text-danger-500', - tooltip: __('Deleted in this version'), - }, - }; - - return icons[normalizedEvent] ? icons[normalizedEvent] : {}; - }, - notesLabel() { - return n__('%d comment', '%d comments', this.notesCount); - }, - imageLink() { - return this.wasInView ? this.imageV432x230 || this.image : ''; - }, - showLoadingSpinner() { - return this.imageLoading || this.isUploading; - }, - showImageErrorIcon() { - return this.wasInView && this.imageError; - }, - showImage() { - return !this.showLoadingSpinner && !this.showImageErrorIcon; - }, - }, - methods: { - onImageLoad() { - this.imageLoading = false; - this.imageError = false; - }, - onImageError() { - this.imageLoading = false; - this.imageError = true; - }, - onAppear() { - // do nothing if image has previously - // been in view - if (this.wasInView) { - return; - } - - this.wasInView = true; - this.imageLoading = true; - }, - }, - DESIGN_ROUTE_NAME, -}; -</script> - -<template> - <router-link - :to="{ - name: $options.DESIGN_ROUTE_NAME, - params: { id: filename }, - query: $route.query, - }" - class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" - > - <div class="card-body p-0 d-flex-center overflow-hidden position-relative"> - <div v-if="icon.name" class="design-event position-absolute"> - <span :title="icon.tooltip" :aria-label="icon.tooltip"> - <icon :name="icon.name" :size="18" :class="icon.classes" /> - </span> - </div> - <gl-intersection-observer @appear="onAppear"> - <gl-loading-icon v-if="showLoadingSpinner" size="md" /> - <gl-icon - v-else-if="showImageErrorIcon" - name="media-broken" - class="text-secondary" - :size="32" - /> - <img - v-show="showImage" - :src="imageLink" - :alt="filename" - class="block mx-auto mw-100 mh-100 design-img" - data-qa-selector="design_image" - @load="onImageLoad" - @error="onImageError" - /> - </gl-intersection-observer> - </div> - <div class="card-footer d-flex w-100"> - <div class="d-flex flex-column str-truncated-100"> - <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{ - filename - }}</span> - <span v-if="updatedAt" class="str-truncated-100"> - {{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" /> - </span> - </div> - <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary"> - <icon name="comments" class="ml-1" /> - <span :aria-label="notesLabel" class="ml-1"> - {{ notesCount }} - </span> - </div> - </div> - </router-link> -</template> diff --git a/app/assets/javascripts/design_management_new/components/toolbar/index.vue b/app/assets/javascripts/design_management_new/components/toolbar/index.vue deleted file mode 100644 index 0b51035e83e..00000000000 --- a/app/assets/javascripts/design_management_new/components/toolbar/index.vue +++ /dev/null @@ -1,124 +0,0 @@ -<script> -import { GlDeprecatedButton } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; -import timeagoMixin from '~/vue_shared/mixins/timeago'; -import Pagination from './pagination.vue'; -import DeleteButton from '../delete_button.vue'; -import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql'; -import { DESIGNS_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - Icon, - Pagination, - DeleteButton, - GlDeprecatedButton, - }, - mixins: [timeagoMixin], - props: { - id: { - type: String, - required: true, - }, - isDeleting: { - type: Boolean, - required: true, - }, - filename: { - type: String, - required: false, - default: '', - }, - updatedAt: { - type: String, - required: false, - default: null, - }, - updatedBy: { - type: Object, - required: false, - default: () => ({}), - }, - isLatestVersion: { - type: Boolean, - required: true, - }, - image: { - type: String, - required: true, - }, - }, - data() { - return { - permissions: { - createDesign: false, - }, - }; - }, - inject: { - projectPath: { - default: '', - }, - issueIid: { - default: '', - }, - }, - apollo: { - permissions: { - query: permissionsQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - }; - }, - update: data => data.project.issue.userPermissions, - }, - }, - computed: { - updatedText() { - return sprintf(__('Updated %{updated_at} by %{updated_by}'), { - updated_at: this.timeFormatted(this.updatedAt), - updated_by: this.updatedBy.name, - }); - }, - canDeleteDesign() { - return this.permissions.createDesign; - }, - }, - DESIGNS_ROUTE_NAME, -}; -</script> - -<template> - <header class="d-flex p-2 bg-white align-items-center js-design-header"> - <router-link - :to="{ - name: $options.DESIGNS_ROUTE_NAME, - query: $route.query, - }" - :aria-label="s__('DesignManagement|Go back to designs')" - data-testid="close-design" - class="mr-3 text-plain d-flex justify-content-center align-items-center" - > - <icon :size="18" name="close" /> - </router-link> - <div class="overflow-hidden d-flex align-items-center"> - <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2> - <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small> - </div> - <pagination :id="id" class="ml-auto flex-shrink-0" /> - <gl-deprecated-button :href="image" class="mr-2"> - <icon :size="18" name="download" /> - </gl-deprecated-button> - <delete-button - v-if="isLatestVersion && canDeleteDesign" - :is-deleting="isDeleting" - button-variant="danger" - @deleteSelectedDesigns="$emit('delete')" - > - <icon :size="18" name="remove" /> - </delete-button> - </header> -</template> diff --git a/app/assets/javascripts/design_management_new/components/toolbar/pagination.vue b/app/assets/javascripts/design_management_new/components/toolbar/pagination.vue deleted file mode 100644 index bf62a8f66a6..00000000000 --- a/app/assets/javascripts/design_management_new/components/toolbar/pagination.vue +++ /dev/null @@ -1,83 +0,0 @@ -<script> -/* global Mousetrap */ -import 'mousetrap'; -import { s__, sprintf } from '~/locale'; -import PaginationButton from './pagination_button.vue'; -import allDesignsMixin from '../../mixins/all_designs'; -import { DESIGN_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - PaginationButton, - }, - mixins: [allDesignsMixin], - props: { - id: { - type: String, - required: true, - }, - }, - computed: { - designsCount() { - return this.designs.length; - }, - currentIndex() { - return this.designs.findIndex(design => design.filename === this.id); - }, - paginationText() { - return sprintf(s__('DesignManagement|%{current_design} of %{designs_count}'), { - current_design: this.currentIndex + 1, - designs_count: this.designsCount, - }); - }, - previousDesign() { - if (!this.designsCount) return null; - - return this.designs[this.currentIndex - 1]; - }, - nextDesign() { - if (!this.designsCount) return null; - - return this.designs[this.currentIndex + 1]; - }, - }, - mounted() { - Mousetrap.bind('left', () => this.navigateToDesign(this.previousDesign)); - Mousetrap.bind('right', () => this.navigateToDesign(this.nextDesign)); - }, - beforeDestroy() { - Mousetrap.unbind(['left', 'right'], this.navigateToDesign); - }, - methods: { - navigateToDesign(design) { - if (design) { - this.$router.push({ - name: DESIGN_ROUTE_NAME, - params: { id: design.filename }, - query: this.$route.query, - }); - } - }, - }, -}; -</script> - -<template> - <div v-if="designsCount" class="d-flex align-items-center"> - {{ paginationText }} - <div class="btn-group ml-3 mr-3"> - <pagination-button - :design="previousDesign" - :title="s__('DesignManagement|Go to previous design')" - icon-name="angle-left" - class="js-previous-design" - /> - <pagination-button - :design="nextDesign" - :title="s__('DesignManagement|Go to next design')" - icon-name="angle-right" - class="js-next-design" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management_new/components/toolbar/pagination_button.vue deleted file mode 100644 index f00ecefca01..00000000000 --- a/app/assets/javascripts/design_management_new/components/toolbar/pagination_button.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -import Icon from '~/vue_shared/components/icon.vue'; -import { DESIGN_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - Icon, - }, - props: { - design: { - type: Object, - required: false, - default: null, - }, - title: { - type: String, - required: true, - }, - iconName: { - type: String, - required: true, - }, - }, - computed: { - designLink() { - if (!this.design) return {}; - - return { - name: DESIGN_ROUTE_NAME, - params: { id: this.design.filename }, - query: this.$route.query, - }; - }, - }, -}; -</script> - -<template> - <router-link - :to="designLink" - :disabled="!design" - :class="{ disabled: !design }" - :aria-label="title" - class="btn btn-default" - > - <icon :name="iconName" /> - </router-link> -</template> diff --git a/app/assets/javascripts/design_management_new/components/upload/button.vue b/app/assets/javascripts/design_management_new/components/upload/button.vue deleted file mode 100644 index de8a38334ac..00000000000 --- a/app/assets/javascripts/design_management_new/components/upload/button.vue +++ /dev/null @@ -1,59 +0,0 @@ -<script> -import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants'; - -export default { - components: { - GlButton, - GlLoadingIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - isSaving: { - type: Boolean, - required: true, - }, - }, - methods: { - openFileUpload() { - this.$refs.fileUpload.click(); - }, - onFileUploadChange(e) { - this.$emit('upload', e.target.files); - }, - }, - VALID_DESIGN_FILE_MIMETYPE, -}; -</script> - -<template> - <div> - <gl-button - v-gl-tooltip.hover - :title=" - s__( - 'DesignManagement|Adding a design with the same filename replaces the file in a new version.', - ) - " - :disabled="isSaving" - variant="success" - size="small" - @click="openFileUpload" - > - {{ s__('DesignManagement|Upload designs') }} - <gl-loading-icon v-if="isSaving" inline class="ml-1" /> - </gl-button> - - <input - ref="fileUpload" - type="file" - name="design_file" - :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype" - class="hide" - multiple - @change="onFileUploadChange" - /> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue deleted file mode 100644 index 7b829d63330..00000000000 --- a/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue +++ /dev/null @@ -1,136 +0,0 @@ -<script> -import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; -import createFlash from '~/flash'; -import uploadDesignMutation from '../../graphql/mutations/upload_design.mutation.graphql'; -import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages'; -import { isValidDesignFile } from '../../utils/design_management_utils'; -import { VALID_DATA_TRANSFER_TYPE, VALID_DESIGN_FILE_MIMETYPE } from '../../constants'; - -export default { - components: { - GlIcon, - GlLink, - GlSprintf, - }, - props: { - hasDesigns: { - type: Boolean, - required: true, - }, - }, - data() { - return { - dragCounter: 0, - isDragDataValid: false, - }; - }, - computed: { - dragging() { - return this.dragCounter !== 0; - }, - }, - methods: { - isValidUpload(files) { - return files.every(isValidDesignFile); - }, - isValidDragDataType({ dataTransfer }) { - return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE)); - }, - ondrop({ dataTransfer = {} }) { - this.dragCounter = 0; - // User already had feedback when dropzone was active, so bail here - if (!this.isDragDataValid) { - return; - } - - const { files } = dataTransfer; - if (!this.isValidUpload(Array.from(files))) { - createFlash(UPLOAD_DESIGN_INVALID_FILETYPE_ERROR); - return; - } - - this.$emit('change', files); - }, - ondragenter(e) { - this.dragCounter += 1; - this.isDragDataValid = this.isValidDragDataType(e); - }, - ondragleave() { - this.dragCounter -= 1; - }, - openFileUpload() { - this.$refs.fileUpload.click(); - }, - onDesignInputChange(e) { - this.$emit('change', e.target.files); - }, - }, - uploadDesignMutation, - VALID_DESIGN_FILE_MIMETYPE, -}; -</script> - -<template> - <div - class="w-100 position-relative" - @dragstart.prevent.stop - @dragend.prevent.stop - @dragover.prevent.stop - @dragenter.prevent.stop="ondragenter" - @dragleave.prevent.stop="ondragleave" - @drop.prevent.stop="ondrop" - > - <slot> - <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3" - @click="openFileUpload" - > - <div - :class="{ 'gl-flex-direction-column': hasDesigns }" - class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center" - data-testid="dropzone-area" - > - <gl-icon name="upload" :size="24" :class="hasDesigns ? 'gl-mb-2' : 'gl-mr-4'" /> - <p class="gl-font-weight-bold gl-mb-0"> - <gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} Designs to attach')"> - <template #link="{ content }"> - <gl-link class="gl-font-weight-normal" @click.stop="openFileUpload"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> - </p> - </div> - </button> - - <input - ref="fileUpload" - type="file" - name="design_file" - :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype" - class="hide" - multiple - @change="onDesignInputChange" - /> - </slot> - <transition name="design-dropzone-fade"> - <div - v-show="dragging" - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - > - <div v-show="!isDragDataValid" class="mw-50 text-center"> - <h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Oh no!') }}</h3> - <span>{{ - __( - 'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.', - ) - }}</span> - </div> - <div v-show="isDragDataValid" class="mw-50 text-center"> - <h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Incoming!') }}</h3> - <span>{{ __('Drop your designs to start your upload.') }}</span> - </div> - </div> - </transition> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management_new/components/upload/design_version_dropdown.vue deleted file mode 100644 index 5299d0ce09e..00000000000 --- a/app/assets/javascripts/design_management_new/components/upload/design_version_dropdown.vue +++ /dev/null @@ -1,76 +0,0 @@ -<script> -import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import allVersionsMixin from '../../mixins/all_versions'; -import { findVersionId } from '../../utils/design_management_utils'; - -export default { - components: { - GlNewDropdown, - GlNewDropdownItem, - }, - mixins: [allVersionsMixin], - computed: { - queryVersion() { - return this.$route.query.version; - }, - currentVersionIdx() { - if (!this.queryVersion) return 0; - - const idx = this.allVersions.findIndex( - version => this.findVersionId(version.node.id) === this.queryVersion, - ); - - // if the currentVersionId isn't a valid version (i.e. not in allVersions) - // then return the latest version (index 0) - return idx !== -1 ? idx : 0; - }, - currentVersionId() { - if (this.queryVersion) return this.queryVersion; - - const currentVersion = this.allVersions[this.currentVersionIdx]; - return this.findVersionId(currentVersion.node.id); - }, - dropdownText() { - if (this.isLatestVersion) { - return __('Showing Latest Version'); - } - // allVersions is sorted in reverse chronological order (latest first) - const currentVersionNumber = this.allVersions.length - this.currentVersionIdx; - - return sprintf(__('Showing Version #%{versionNumber}'), { - versionNumber: currentVersionNumber, - }); - }, - }, - methods: { - findVersionId, - }, -}; -</script> - -<template> - <gl-new-dropdown :text="dropdownText" size="small" class="design-version-dropdown"> - <gl-new-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id"> - <router-link - class="d-flex js-version-link" - :to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }" - > - <div class="flex-grow-1 ml-2"> - <div> - <strong - >{{ __('Version') }} {{ allVersions.length - index }} - <span v-if="findVersionId(version.node.id) === latestVersionId" - >({{ __('latest') }})</span - > - </strong> - </div> - </div> - <i - v-if="findVersionId(version.node.id) === currentVersionId" - class="fa fa-check pull-right" - ></i> - </router-link> - </gl-new-dropdown-item> - </gl-new-dropdown> -</template> |