diff options
159 files changed, 451 insertions, 11153 deletions
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue index 09d7c0329a9..7b53020fc49 100644 --- a/app/assets/javascripts/clusters_list/components/clusters.vue +++ b/app/assets/javascripts/clusters_list/components/clusters.vue @@ -5,7 +5,7 @@ import { GlLink, GlLoadingIcon, GlPagination, - GlSkeletonLoading, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlTable, } from '@gitlab/ui'; diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js index 20c9cacf83f..1a87dd38137 100644 --- a/app/assets/javascripts/design_management/index.js +++ b/app/assets/javascripts/design_management/index.js @@ -4,7 +4,7 @@ import App from './components/app.vue'; import apolloProvider from './graphql'; export default () => { - const el = document.querySelector('.js-design-management-new'); + const el = document.querySelector('.js-design-management'); const { issueIid, projectPath, issuePath } = el.dataset; const router = createRouter(issuePath); diff --git a/app/assets/javascripts/design_management_legacy/components/app.vue b/app/assets/javascripts/design_management_legacy/components/app.vue deleted file mode 100644 index 98240aef810..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/app.vue +++ /dev/null @@ -1,3 +0,0 @@ -<template> - <router-view /> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/delete_button.vue b/app/assets/javascripts/design_management_legacy/components/delete_button.vue deleted file mode 100644 index 1fd902c9ed7..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/delete_button.vue +++ /dev/null @@ -1,64 +0,0 @@ -<script> -import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui'; -import { uniqueId } from 'lodash'; - -export default { - name: 'DeleteButton', - components: { - GlDeprecatedButton, - GlModal, - }, - directives: { - GlModalDirective, - }, - props: { - isDeleting: { - type: Boolean, - required: false, - default: false, - }, - buttonClass: { - type: String, - required: false, - default: '', - }, - buttonVariant: { - type: String, - required: false, - default: '', - }, - hasSelectedDesigns: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - modalId: uniqueId('design-deletion-confirmation-'), - }; - }, -}; -</script> - -<template> - <div> - <gl-modal - :modal-id="modalId" - :title="s__('DesignManagement|Delete designs confirmation')" - :ok-title="s__('DesignManagement|Delete')" - ok-variant="danger" - @ok="$emit('deleteSelectedDesigns')" - > - <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p> - </gl-modal> - <gl-deprecated-button - v-gl-modal-directive="modalId" - :variant="buttonVariant" - :disabled="isDeleting || !hasSelectedDesigns" - :class="buttonClass" - > - <slot></slot> - </gl-deprecated-button> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue b/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue deleted file mode 100644 index 62460ca551c..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue +++ /dev/null @@ -1,66 +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, - }, - projectPath: { - type: String, - required: true, - }, - iid: { - type: String, - required: true, - }, - }, - 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_legacy/components/design_note_pin.vue b/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue deleted file mode 100644 index 2b5e62c2870..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue +++ /dev/null @@ -1,61 +0,0 @@ -<script> -import { GlIcon } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; - -export default { - name: 'DesignNotePin', - components: { - GlIcon, - }, - 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 gl-p-0" - type="button" - @mousedown="$emit('mousedown', $event)" - @mouseup="$emit('mouseup', $event)" - @click="$emit('click', $event)" - > - <gl-icon v-if="isNewNote" name="image-comment-dark" :size="24" /> - <template v-else> - {{ label }} - </template> - </button> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue deleted file mode 100644 index 6a20517eed7..00000000000 --- a/app/assets/javascripts/design_management_legacy/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-500 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message"> - {{ __('Resolved by') }} - <gl-link - class="gl-text-gray-500 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_legacy/components/design_notes/design_note.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue deleted file mode 100644 index 93f8fbc5ba3..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue +++ /dev/null @@ -1,157 +0,0 @@ -<script> -/* eslint-disable vue/no-v-html */ -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.$refs.anchor.$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}`" ref="anchor" 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_legacy/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue deleted file mode 100644 index 969034909f2..00000000000 --- a/app/assets/javascripts/design_management_legacy/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_legacy/components/design_notes/toggle_replies_widget.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue deleted file mode 100644 index 2e366282de3..00000000000 --- a/app/assets/javascripts/design_management_legacy/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-500">{{ __('Last reply by') }}</span> - <gl-link - :href="lastReply.author.webUrl" - target="_blank" - class="link-inherit-color gl-text-body 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-500" - /> - </template> - </li> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_overlay.vue b/app/assets/javascripts/design_management_legacy/components/design_overlay.vue deleted file mode 100644 index 926e7c74802..00000000000 --- a/app/assets/javascripts/design_management_legacy/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_legacy/components/design_presentation.vue b/app/assets/javascripts/design_management_legacy/components/design_presentation.vue deleted file mode 100644 index 84dbb2809d9..00000000000 --- a/app/assets/javascripts/design_management_legacy/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_legacy/components/design_scaler.vue b/app/assets/javascripts/design_management_legacy/components/design_scaler.vue deleted file mode 100644 index 55dee74bef5..00000000000 --- a/app/assets/javascripts/design_management_legacy/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_legacy/components/design_sidebar.vue b/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue deleted file mode 100644 index 622120e2008..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue +++ /dev/null @@ -1,178 +0,0 @@ -<script> -import Cookies from 'js-cookie'; -import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import { parseBoolean } from '~/lib/utils/common_utils'; -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-400 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-body 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_legacy/components/image.vue b/app/assets/javascripts/design_management_legacy/components/image.vue deleted file mode 100644 index 91b7b576e0c..00000000000 --- a/app/assets/javascripts/design_management_legacy/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_legacy/components/list/item.vue b/app/assets/javascripts/design_management_legacy/components/list/item.vue deleted file mode 100644 index 40040b25e51..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/list/item.vue +++ /dev/null @@ -1,172 +0,0 @@ -<script> -import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui'; -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, - 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" - > - <div class="card-body p-0 d-flex-center overflow-hidden position-relative"> - <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute"> - <span :title="icon.tooltip" :aria-label="icon.tooltip"> - <gl-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"> - <gl-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_legacy/components/toolbar/index.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue deleted file mode 100644 index 5729072fe38..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue +++ /dev/null @@ -1,125 +0,0 @@ -<script> -import { GlDeprecatedButton, GlIcon } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -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 appDataQuery from '../../graphql/queries/app_data.query.graphql'; -import { DESIGNS_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - GlIcon, - 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, - }, - projectPath: '', - issueIid: null, - }; - }, - apollo: { - appData: { - query: appDataQuery, - manual: true, - result({ data: { projectPath, issueIid } }) { - this.projectPath = projectPath; - this.issueIid = issueIid; - }, - }, - 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')" - class="mr-3 text-plain d-flex justify-content-center align-items-center" - > - <gl-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"> - <gl-icon :size="18" name="download" /> - </gl-deprecated-button> - <delete-button - v-if="isLatestVersion && canDeleteDesign" - :is-deleting="isDeleting" - button-variant="danger" - @deleteSelectedDesigns="$emit('delete')" - > - <gl-icon :size="18" name="remove" /> - </delete-button> - </header> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue deleted file mode 100644 index bf62a8f66a6..00000000000 --- a/app/assets/javascripts/design_management_legacy/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_legacy/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue deleted file mode 100644 index 7051aaddd04..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -import { GlIcon } from '@gitlab/ui'; -import { DESIGN_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - GlIcon, - }, - 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" - > - <gl-icon :name="iconName" /> - </router-link> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/upload/button.vue b/app/assets/javascripts/design_management_legacy/components/upload/button.vue deleted file mode 100644 index 68555104a3c..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/upload/button.vue +++ /dev/null @@ -1,58 +0,0 @@ -<script> -import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants'; - -export default { - components: { - GlDeprecatedButton, - 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-deprecated-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" - @click="openFileUpload" - > - {{ s__('DesignManagement|Upload designs') }} - <gl-loading-icon v-if="isSaving" inline class="ml-1" /> - </gl-deprecated-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_legacy/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue deleted file mode 100644 index e435c84c959..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue +++ /dev/null @@ -1,134 +0,0 @@ -<script> -import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; -import { deprecatedCreateFlash as 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, - }, - 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 d-flex-center p-3" - @click="openFileUpload" - > - <div class="d-flex-center flex-column text-center"> - <gl-icon name="doc-new" :size="48" class="mb-4" /> - <p> - <gl-sprintf - :message=" - __( - '%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.', - ) - " - > - <template #lineOne="{ content }" - ><span class="d-block">{{ content }}</span> - </template> - - <template #link="{ content }"> - <gl-link class="h-100 w-100" @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>{{ __('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>{{ __('Incoming!') }}</h3> - <span>{{ __('Drop your designs to start your upload.') }}</span> - </div> - </div> - </transition> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue deleted file mode 100644 index 879d2523848..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue +++ /dev/null @@ -1,76 +0,0 @@ -<script> -import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import allVersionsMixin from '../../mixins/all_versions'; -import { findVersionId } from '../../utils/design_management_utils'; - -export default { - components: { - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, - }, - 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-deprecated-dropdown :text="dropdownText" variant="link" class="design-version-dropdown"> - <gl-deprecated-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 float-right gl-mr-2" - ></i> - </router-link> - </gl-deprecated-dropdown-item> - </gl-deprecated-dropdown> -</template> diff --git a/app/assets/javascripts/design_management_legacy/constants.js b/app/assets/javascripts/design_management_legacy/constants.js deleted file mode 100644 index 21ff361a277..00000000000 --- a/app/assets/javascripts/design_management_legacy/constants.js +++ /dev/null @@ -1,16 +0,0 @@ -// WARNING: replace this with something -// more sensical as per https://gitlab.com/gitlab-org/gitlab/issues/118611 -export const VALID_DESIGN_FILE_MIMETYPE = { - mimetype: 'image/*', - regex: /image\/.+/, -}; - -// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types -export const VALID_DATA_TRANSFER_TYPE = 'Files'; - -export const ACTIVE_DISCUSSION_SOURCE_TYPES = { - pin: 'pin', - discussion: 'discussion', -}; - -export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0']; diff --git a/app/assets/javascripts/design_management_legacy/graphql.js b/app/assets/javascripts/design_management_legacy/graphql.js deleted file mode 100644 index fae337aa75b..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql.js +++ /dev/null @@ -1,45 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { uniqueId } from 'lodash'; -import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; -import createDefaultClient from '~/lib/graphql'; -import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql'; -import typeDefs from './graphql/typedefs.graphql'; - -Vue.use(VueApollo); - -const resolvers = { - Mutation: { - updateActiveDiscussion: (_, { id = null, source }, { cache }) => { - const data = cache.readQuery({ query: activeDiscussionQuery }); - data.activeDiscussion = { - __typename: 'ActiveDiscussion', - id, - source, - }; - cache.writeQuery({ query: activeDiscussionQuery, data }); - }, - }, -}; - -const defaultClient = createDefaultClient( - resolvers, - // This config is added temporarily to resolve an issue with duplicate design IDs. - // Should be removed as soon as https://gitlab.com/gitlab-org/gitlab/issues/13495 is resolved - { - cacheConfig: { - dataIdFromObject: object => { - // eslint-disable-next-line no-underscore-dangle, @gitlab/require-i18n-strings - if (object.__typename === 'Design') { - return object.id && object.image ? `${object.id}-${object.image}` : uniqueId(); - } - return defaultDataIdFromObject(object); - }, - }, - typeDefs, - }, -); - -export default new VueApollo({ - defaultClient, -}); diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql deleted file mode 100644 index 4b1703e41c3..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql +++ /dev/null @@ -1,24 +0,0 @@ -#import "./design_note.fragment.graphql" -#import "./design_list.fragment.graphql" -#import "./diff_refs.fragment.graphql" -#import "./discussion_resolved_status.fragment.graphql" - -fragment DesignItem on Design { - ...DesignListItem - fullPath - diffRefs { - ...DesignDiffRefs - } - discussions { - nodes { - id - replyId - ...ResolvedStatus - notes { - nodes { - ...DesignNote - } - } - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql deleted file mode 100644 index bc3132f9b42..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -fragment DesignListItem on Design { - id - event - filename - notesCount - image - imageV432x230 -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql deleted file mode 100644 index 26edd2c0be1..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql +++ /dev/null @@ -1,29 +0,0 @@ -#import "./diff_refs.fragment.graphql" -#import "~/graphql_shared/fragments/author.fragment.graphql" -#import "./note_permissions.fragment.graphql" - -fragment DesignNote on Note { - id - author { - ...Author - } - body - bodyHtml - createdAt - resolved - position { - diffRefs { - ...DesignDiffRefs - } - x - y - height - width - } - userPermissions { - ...DesignNotePermissions - } - discussion { - id - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql deleted file mode 100644 index 984a55814b0..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql +++ /dev/null @@ -1,5 +0,0 @@ -fragment DesignDiffRefs on DiffRefs { - baseSha - startSha - headSha -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql deleted file mode 100644 index 7483b508721..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql +++ /dev/null @@ -1,9 +0,0 @@ -fragment ResolvedStatus on Discussion { - resolvable - resolved - resolvedAt - resolvedBy { - name - webUrl - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql deleted file mode 100644 index c243e39f3d3..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql +++ /dev/null @@ -1,3 +0,0 @@ -fragment DesignNotePermissions on NotePermissions { - adminNote -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql deleted file mode 100644 index 7eb40b12f51..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql +++ /dev/null @@ -1,4 +0,0 @@ -fragment VersionListItem on DesignVersion { - id - sha -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql deleted file mode 100644 index c8ade328120..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql +++ /dev/null @@ -1,21 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" - -mutation createImageDiffNote($input: CreateImageDiffNoteInput!) { - createImageDiffNote(input: $input) { - note { - ...DesignNote - discussion { - id - replyId - notes { - edges { - node { - ...DesignNote - } - } - } - } - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql deleted file mode 100644 index 184ee6955dc..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" - -mutation createNote($input: CreateNoteInput!) { - createNote(input: $input) { - note { - ...DesignNote - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql deleted file mode 100644 index 0b3cf636cdb..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -#import "../fragments/version.fragment.graphql" - -mutation destroyDesign($filenames: [String!]!, $projectPath: ID!, $iid: ID!) { - designManagementDelete(input: { projectPath: $projectPath, iid: $iid, filenames: $filenames }) { - version { - ...VersionListItem - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql deleted file mode 100644 index 1157fc05d5f..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql +++ /dev/null @@ -1,17 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" -#import "../fragments/discussion_resolved_status.fragment.graphql" - -mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) { - discussionToggleResolve(input: { id: $id, resolve: $resolve }) { - discussion { - id - ...ResolvedStatus - notes { - nodes { - ...DesignNote - } - } - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql deleted file mode 100644 index a24b6737159..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql +++ /dev/null @@ -1,3 +0,0 @@ -mutation updateActiveDiscussion($id: String, $source: String) { - updateActiveDiscussion(id: $id, source: $source) @client -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql deleted file mode 100644 index 5562ca9d89f..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" - -mutation updateImageDiffNote($input: UpdateImageDiffNoteInput!) { - updateImageDiffNote(input: $input) { - errors - note { - ...DesignNote - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql deleted file mode 100644 index b995e99fb6a..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" - -mutation updateNote($input: UpdateNoteInput!) { - updateNote(input: $input) { - note { - ...DesignNote - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql deleted file mode 100644 index d694e6558a0..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql +++ /dev/null @@ -1,21 +0,0 @@ -#import "../fragments/design.fragment.graphql" - -mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) { - designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) { - designs { - ...DesignItem - versions { - edges { - node { - id - sha - } - } - } - } - skippedDesigns { - filename - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql deleted file mode 100644 index 111023cea68..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql +++ /dev/null @@ -1,6 +0,0 @@ -query activeDiscussion { - activeDiscussion @client { - id - source - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql deleted file mode 100644 index e1269761206..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql +++ /dev/null @@ -1,4 +0,0 @@ -query projectFullPath { - projectPath @client - issueIid @client -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql deleted file mode 100644 index a87b256dc95..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql +++ /dev/null @@ -1,10 +0,0 @@ -query permissions($fullPath: ID!, $iid: String!) { - project(fullPath: $fullPath) { - id - issue(iid: $iid) { - userPermissions { - createDesign - } - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql deleted file mode 100644 index 07a9af55787..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql +++ /dev/null @@ -1,31 +0,0 @@ -#import "../fragments/design.fragment.graphql" -#import "~/graphql_shared/fragments/author.fragment.graphql" - -query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [String!]) { - project(fullPath: $fullPath) { - id - issue(iid: $iid) { - designCollection { - designs(atVersion: $atVersion, filenames: $filenames) { - edges { - node { - ...DesignItem - issue { - title - webPath - webUrl - participants { - edges { - node { - ...Author - } - } - } - } - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql deleted file mode 100644 index 121a50555b3..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql +++ /dev/null @@ -1,26 +0,0 @@ -#import "../fragments/design_list.fragment.graphql" -#import "../fragments/version.fragment.graphql" - -query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) { - project(fullPath: $fullPath) { - id - issue(iid: $iid) { - designCollection { - designs(atVersion: $atVersion) { - edges { - node { - ...DesignListItem - } - } - } - versions { - edges { - node { - ...VersionListItem - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql b/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql deleted file mode 100644 index fdbad4a90e0..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql +++ /dev/null @@ -1,12 +0,0 @@ -type ActiveDiscussion { - id: ID - source: String -} - -extend type Query { - activeDiscussion: ActiveDiscussion -} - -extend type Mutation { - updateActiveDiscussion(id: ID!, source: String!): Boolean -} diff --git a/app/assets/javascripts/design_management_legacy/index.js b/app/assets/javascripts/design_management_legacy/index.js deleted file mode 100644 index 1fc5779515a..00000000000 --- a/app/assets/javascripts/design_management_legacy/index.js +++ /dev/null @@ -1,61 +0,0 @@ -// This application is being moved, please do not touch this files -// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details - -import $ from 'jquery'; -import Vue from 'vue'; -import createRouter from './router'; -import App from './components/app.vue'; -import apolloProvider from './graphql'; -import getDesignListQuery from './graphql/queries/get_design_list.query.graphql'; -import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants'; - -export default () => { - const el = document.querySelector('.js-design-management'); - const badge = document.querySelector('.js-designs-count'); - const { issueIid, projectPath, issuePath } = el.dataset; - const router = createRouter(issuePath); - - $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => { - if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) { - router.push({ name: DESIGNS_ROUTE_NAME }); - } else if (id === 'discussion') { - router.push({ name: ROOT_ROUTE_NAME }); - } - }); - - apolloProvider.clients.defaultClient.cache.writeData({ - data: { - projectPath, - issueIid, - activeDiscussion: { - __typename: 'ActiveDiscussion', - id: null, - source: null, - }, - }, - }); - - apolloProvider.clients.defaultClient - .watchQuery({ - query: getDesignListQuery, - variables: { - fullPath: projectPath, - iid: issueIid, - atVersion: null, - }, - }) - .subscribe(({ data }) => { - if (badge) { - badge.textContent = data.project.issue.designCollection.designs.edges.length; - } - }); - - return new Vue({ - el, - router, - apolloProvider, - render(createElement) { - return createElement(App); - }, - }); -}; diff --git a/app/assets/javascripts/design_management_legacy/mixins/all_designs.js b/app/assets/javascripts/design_management_legacy/mixins/all_designs.js deleted file mode 100644 index 544429928d2..00000000000 --- a/app/assets/javascripts/design_management_legacy/mixins/all_designs.js +++ /dev/null @@ -1,49 +0,0 @@ -import { propertyOf } from 'lodash'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { s__ } from '~/locale'; -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import { extractNodes } from '../utils/design_management_utils'; -import allVersionsMixin from './all_versions'; -import { DESIGNS_ROUTE_NAME } from '../router/constants'; - -export default { - mixins: [allVersionsMixin], - apollo: { - designs: { - query: getDesignListQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - atVersion: this.designsVersion, - }; - }, - update: data => { - const designEdges = propertyOf(data)(['project', 'issue', 'designCollection', 'designs']); - if (designEdges) { - return extractNodes(designEdges); - } - return []; - }, - error() { - this.error = true; - }, - result() { - if (this.$route.query.version && !this.hasValidVersion) { - createFlash( - s__( - 'DesignManagement|Requested design version does not exist. Showing latest version instead', - ), - ); - this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } }); - } - }, - }, - }, - data() { - return { - designs: [], - error: false, - }; - }, -}; diff --git a/app/assets/javascripts/design_management_legacy/mixins/all_versions.js b/app/assets/javascripts/design_management_legacy/mixins/all_versions.js deleted file mode 100644 index 3966fe71732..00000000000 --- a/app/assets/javascripts/design_management_legacy/mixins/all_versions.js +++ /dev/null @@ -1,62 +0,0 @@ -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import appDataQuery from '../graphql/queries/app_data.query.graphql'; -import { findVersionId } from '../utils/design_management_utils'; - -export default { - apollo: { - appData: { - query: appDataQuery, - manual: true, - result({ data: { projectPath, issueIid } }) { - this.projectPath = projectPath; - this.issueIid = issueIid; - }, - }, - allVersions: { - query: getDesignListQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - atVersion: null, - }; - }, - update: data => data.project.issue.designCollection.versions.edges, - }, - }, - computed: { - hasValidVersion() { - return ( - this.$route.query.version && - this.allVersions && - this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version)) - ); - }, - designsVersion() { - return this.hasValidVersion - ? `gid://gitlab/DesignManagement::Version/${this.$route.query.version}` - : null; - }, - latestVersionId() { - const latestVersion = this.allVersions[0]; - return latestVersion && findVersionId(latestVersion.node.id); - }, - isLatestVersion() { - if (this.allVersions.length > 0) { - return ( - !this.$route.query.version || - !this.latestVersionId || - this.$route.query.version === this.latestVersionId - ); - } - return true; - }, - }, - data() { - return { - allVersions: [], - projectPath: '', - issueIid: null, - }; - }, -}; diff --git a/app/assets/javascripts/design_management_legacy/pages/design/index.vue b/app/assets/javascripts/design_management_legacy/pages/design/index.vue deleted file mode 100644 index 2ada9eff8c6..00000000000 --- a/app/assets/javascripts/design_management_legacy/pages/design/index.vue +++ /dev/null @@ -1,378 +0,0 @@ -<script> -import Mousetrap from 'mousetrap'; -import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; -import { ApolloMutation } from 'vue-apollo'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { fetchPolicies } from '~/lib/graphql'; -import allVersionsMixin from '../../mixins/all_versions'; -import Toolbar from '../../components/toolbar/index.vue'; -import DesignDestroyer from '../../components/design_destroyer.vue'; -import DesignScaler from '../../components/design_scaler.vue'; -import DesignPresentation from '../../components/design_presentation.vue'; -import DesignReplyForm from '../../components/design_notes/design_reply_form.vue'; -import DesignSidebar from '../../components/design_sidebar.vue'; -import getDesignQuery from '../../graphql/queries/get_design.query.graphql'; -import appDataQuery from '../../graphql/queries/app_data.query.graphql'; -import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql'; -import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql'; -import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql'; -import { - extractDiscussions, - extractDesign, - 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, - DesignReplyForm, - DesignPresentation, - DesignScaler, - DesignDestroyer, - Toolbar, - GlLoadingIcon, - GlAlert, - DesignSidebar, - }, - mixins: [allVersionsMixin], - props: { - id: { - type: String, - required: true, - }, - }, - data() { - return { - design: {}, - comment: '', - annotationCoordinates: null, - projectPath: '', - errorMessage: '', - issueIid: '', - scale: 1, - resolvedDiscussionsExpanded: false, - }; - }, - 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() { - if (!this.design.discussions) { - return []; - } - return extractDiscussions(this.design.discussions); - }, - markdownPreviewPath() { - return `/${this.projectPath}/preview_markdown?target_type=Issue`; - }, - isSubmitButtonDisabled() { - return this.comment.trim().length === 0; - }, - 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, - }, - }, - }; - }, - isAnnotating() { - return Boolean(this.annotationCoordinates); - }, - resolvedDiscussions() { - return this.discussions.filter(discussion => discussion.resolved); - }, - }, - watch: { - resolvedDiscussions(val) { - if (!val.length) { - this.resolvedDiscussionsExpanded = false; - } - }, - }, - mounted() { - Mousetrap.bind('esc', this.closeDesign); - this.trackEvent(); - // We need to reset the active discussion when opening a new design - this.updateActiveDiscussion(); - }, - 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); - }, - onResolveDiscussionError(e) { - this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); - }, - openCommentForm(annotationCoordinates) { - this.annotationCoordinates = annotationCoordinates; - if (this.$refs.newDiscussionForm) { - this.$refs.newDiscussionForm.focusInput(); - } - }, - 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, - }, - }); - }, - toggleResolvedComments() { - this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded; - }, - }, - 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" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - @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> - <design-sidebar - :design="design" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - :markdown-preview-path="markdownPreviewPath" - @onDesignDiscussionError="onDesignDiscussionError" - @onCreateImageDiffNoteError="onCreateImageDiffNoteError" - @updateNoteError="onUpdateNoteError" - @resolveDiscussionError="onResolveDiscussionError" - @toggleResolvedComments="toggleResolvedComments" - > - <template #replyForm> - <apollo-mutation - v-if="isAnnotating" - #default="{ mutate, loading }" - :mutation="$options.createImageDiffNoteMutation" - :variables="{ - input: mutationPayload, - }" - :update="addImageDiffNoteToStore" - @done="closeCommentForm" - @error="onCreateImageDiffNoteError" - > - <design-reply-form - ref="newDiscussionForm" - v-model="comment" - :is-saving="loading" - :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="closeCommentForm" - /> </apollo-mutation - ></template> - </design-sidebar> - </template> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/pages/index.vue b/app/assets/javascripts/design_management_legacy/pages/index.vue deleted file mode 100644 index 81532d75b7d..00000000000 --- a/app/assets/javascripts/design_management_legacy/pages/index.vue +++ /dev/null @@ -1,323 +0,0 @@ -<script> -import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { s__, sprintf } from '~/locale'; -import UploadButton from '../components/upload/button.vue'; -import DeleteButton from '../components/delete_button.vue'; -import Design from '../components/list/item.vue'; -import DesignDestroyer from '../components/design_destroyer.vue'; -import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue'; -import DesignDropzone from '../components/upload/design_dropzone.vue'; -import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql'; -import permissionsQuery from '../graphql/queries/design_permissions.query.graphql'; -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import allDesignsMixin from '../mixins/all_designs'; -import { - UPLOAD_DESIGN_ERROR, - EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE, - EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE, - designUploadSkippedWarning, - designDeletionError, -} from '../utils/error_messages'; -import { updateStoreAfterUploadDesign } from '../utils/cache_update'; -import { - designUploadOptimisticResponse, - isValidDesignFile, -} from '../utils/design_management_utils'; -import { getFilename } from '~/lib/utils/file_upload'; -import { DESIGNS_ROUTE_NAME } from '../router/constants'; - -const MAXIMUM_FILE_UPLOAD_LIMIT = 10; - -export default { - components: { - GlLoadingIcon, - GlAlert, - GlDeprecatedButton, - UploadButton, - Design, - DesignDestroyer, - DesignVersionDropdown, - DeleteButton, - DesignDropzone, - }, - mixins: [allDesignsMixin], - apollo: { - permissions: { - query: permissionsQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - }; - }, - update: data => data.project.issue.userPermissions, - }, - }, - data() { - return { - permissions: { - createDesign: false, - }, - filesToBeSaved: [], - selectedDesigns: [], - }; - }, - computed: { - isLoading() { - return this.$apollo.queries.designs.loading || this.$apollo.queries.permissions.loading; - }, - isSaving() { - return this.filesToBeSaved.length > 0; - }, - canCreateDesign() { - return this.permissions.createDesign; - }, - showToolbar() { - return this.canCreateDesign && this.allVersions.length > 0; - }, - hasDesigns() { - return this.designs.length > 0; - }, - hasSelectedDesigns() { - return this.selectedDesigns.length > 0; - }, - canDeleteDesigns() { - return this.isLatestVersion && this.hasSelectedDesigns; - }, - projectQueryBody() { - return { - query: getDesignListQuery, - variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null }, - }; - }, - selectAllButtonText() { - return this.hasSelectedDesigns - ? s__('DesignManagement|Deselect all') - : s__('DesignManagement|Select all'); - }, - }, - mounted() { - this.toggleOnPasteListener(this.$route.name); - }, - methods: { - resetFilesToBeSaved() { - this.filesToBeSaved = []; - }, - /** - * Determine if a design upload is valid, given [files] - * @param {Array<File>} files - */ - isValidDesignUpload(files) { - if (!this.canCreateDesign) return false; - - if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) { - createFlash( - sprintf( - s__( - 'DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again.', - ), - { - upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT, - }, - ), - ); - - return false; - } - return true; - }, - onUploadDesign(files) { - // convert to Array so that we have Array methods (.map, .some, etc.) - this.filesToBeSaved = Array.from(files); - if (!this.isValidDesignUpload(this.filesToBeSaved)) return null; - - const mutationPayload = { - optimisticResponse: designUploadOptimisticResponse(this.filesToBeSaved), - variables: { - files: this.filesToBeSaved, - projectPath: this.projectPath, - iid: this.issueIid, - }, - context: { - hasUpload: true, - }, - mutation: uploadDesignMutation, - update: this.afterUploadDesign, - }; - - return this.$apollo - .mutate(mutationPayload) - .then(res => this.onUploadDesignDone(res)) - .catch(() => this.onUploadDesignError()); - }, - afterUploadDesign( - store, - { - data: { designManagementUpload }, - }, - ) { - updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody); - }, - onUploadDesignDone(res) { - const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || []; - const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles); - if (skippedWarningMessage) { - createFlash(skippedWarningMessage, 'warning'); - } - - // if this upload resulted in a new version being created, redirect user to the latest version - if (!this.isLatestVersion) { - this.$router.push({ name: DESIGNS_ROUTE_NAME }, () => {}); - } - this.resetFilesToBeSaved(); - }, - onUploadDesignError() { - this.resetFilesToBeSaved(); - createFlash(UPLOAD_DESIGN_ERROR); - }, - changeSelectedDesigns(filename) { - if (this.isDesignSelected(filename)) { - this.selectedDesigns = this.selectedDesigns.filter(design => design !== filename); - } else { - this.selectedDesigns.push(filename); - } - }, - toggleDesignsSelection() { - if (this.hasSelectedDesigns) { - this.selectedDesigns = []; - } else { - this.selectedDesigns = this.designs.map(design => design.filename); - } - }, - isDesignSelected(filename) { - return this.selectedDesigns.includes(filename); - }, - isDesignToBeSaved(filename) { - return this.filesToBeSaved.some(file => file.name === filename); - }, - canSelectDesign(filename) { - return this.isLatestVersion && this.canCreateDesign && !this.isDesignToBeSaved(filename); - }, - onDesignDelete() { - this.selectedDesigns = []; - if (this.$route.query.version) this.$router.push({ name: DESIGNS_ROUTE_NAME }); - }, - onDesignDeleteError() { - const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 }); - createFlash(errorMessage); - }, - onExistingDesignDropzoneChange(files, existingDesignFilename) { - const filesArr = Array.from(files); - - if (filesArr.length > 1) { - createFlash(EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE); - return; - } - - if (!filesArr.some(({ name }) => existingDesignFilename === name)) { - createFlash(EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE); - return; - } - - this.onUploadDesign(files); - }, - onDesignPaste(event) { - const { clipboardData } = event; - const files = Array.from(clipboardData.files); - if (clipboardData && files.length > 0) { - if (!files.some(isValidDesignFile)) { - return; - } - event.preventDefault(); - let filename = getFilename(event); - if (!filename || filename === 'image.png') { - filename = `design_${Date.now()}.png`; - } - const newFile = new File([files[0]], filename); - this.onUploadDesign([newFile]); - } - }, - toggleOnPasteListener(route) { - if (route === DESIGNS_ROUTE_NAME) { - document.addEventListener('paste', this.onDesignPaste); - } else { - document.removeEventListener('paste', this.onDesignPaste); - } - }, - }, - beforeRouteUpdate(to, from, next) { - this.toggleOnPasteListener(to.name); - this.selectedDesigns = []; - next(); - }, - beforeRouteLeave(to, from, next) { - this.toggleOnPasteListener(to.name); - next(); - }, -}; -</script> - -<template> - <div> - <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex"> - <div class="d-flex justify-content-between align-items-center w-100"> - <design-version-dropdown /> - <div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]"> - <gl-deprecated-button - v-if="isLatestVersion" - variant="link" - class="mr-2 js-select-all" - @click="toggleDesignsSelection" - >{{ selectAllButtonText }}</gl-deprecated-button - > - <design-destroyer - #default="{ mutate, loading }" - :filenames="selectedDesigns" - :project-path="projectPath" - :iid="issueIid" - @done="onDesignDelete" - @error="onDesignDeleteError" - > - <delete-button - v-if="isLatestVersion" - :is-deleting="loading" - button-class="btn-danger btn-inverted mr-2" - :has-selected-designs="hasSelectedDesigns" - @deleteSelectedDesigns="mutate()" - > - {{ s__('DesignManagement|Delete selected') }} - <gl-loading-icon v-if="loading" inline class="ml-1" /> - </delete-button> - </design-destroyer> - <upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" /> - </div> - </div> - </header> - <div class="mt-4"> - <gl-loading-icon v-if="isLoading" size="md" /> - <gl-alert v-else-if="error" variant="danger" :dismissible="false"> - {{ __('An error occurred while loading designs. Please try again.') }} - </gl-alert> - <ol v-else class="list-unstyled row"> - <li class="col-md-6 col-lg-4 mb-3"> - <design-dropzone class="design-list-item" @change="onUploadDesign" /> - </li> - <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3"> - <design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)" - ><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)" - /></design-dropzone> - - <input - v-if="canSelectDesign(design.filename)" - :checked="isDesignSelected(design.filename)" - type="checkbox" - class="design-checkbox" - @change="changeSelectedDesigns(design.filename)" - /> - </li> - </ol> - </div> - <router-view :key="$route.fullPath" /> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/router/constants.js b/app/assets/javascripts/design_management_legacy/router/constants.js deleted file mode 100644 index abeef520e33..00000000000 --- a/app/assets/javascripts/design_management_legacy/router/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const ROOT_ROUTE_NAME = 'root'; -export const DESIGNS_ROUTE_NAME = 'designs'; -export const DESIGN_ROUTE_NAME = 'design'; diff --git a/app/assets/javascripts/design_management_legacy/router/index.js b/app/assets/javascripts/design_management_legacy/router/index.js deleted file mode 100644 index 28a81ed0278..00000000000 --- a/app/assets/javascripts/design_management_legacy/router/index.js +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'jquery'; -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import routes from './routes'; -import { DESIGN_ROUTE_NAME } from './constants'; -import { getPageLayoutElement } from '~/design_management_legacy/utils/design_management_utils'; -import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants'; - -Vue.use(VueRouter); - -export default function createRouter(base) { - const router = new VueRouter({ - base, - mode: 'history', - routes, - }); - const pageEl = getPageLayoutElement(); - - router.beforeEach(({ meta: { el }, name }, _, next) => { - $(`#${el}`).tab('show'); - - // apply a fullscreen layout style in Design View (a.k.a design detail) - if (pageEl) { - if (name === DESIGN_ROUTE_NAME) { - pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST); - } else { - pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST); - } - } - - next(); - }); - - return router; -} diff --git a/app/assets/javascripts/design_management_legacy/router/routes.js b/app/assets/javascripts/design_management_legacy/router/routes.js deleted file mode 100644 index 788910e5514..00000000000 --- a/app/assets/javascripts/design_management_legacy/router/routes.js +++ /dev/null @@ -1,44 +0,0 @@ -import Home from '../pages/index.vue'; -import DesignDetail from '../pages/design/index.vue'; -import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants'; - -export default [ - { - name: ROOT_ROUTE_NAME, - path: '/', - component: Home, - meta: { - el: 'discussion', - }, - }, - { - name: DESIGNS_ROUTE_NAME, - path: '/designs', - component: Home, - meta: { - el: 'designs', - }, - children: [ - { - name: DESIGN_ROUTE_NAME, - path: ':id', - component: DesignDetail, - meta: { - el: 'designs', - }, - beforeEnter( - { - params: { id }, - }, - from, - next, - ) { - if (typeof id === 'string') { - next(); - } - }, - props: ({ params: { id } }) => ({ id }), - }, - ], - }, -]; diff --git a/app/assets/javascripts/design_management_legacy/utils/cache_update.js b/app/assets/javascripts/design_management_legacy/utils/cache_update.js deleted file mode 100644 index 5ba6f84c413..00000000000 --- a/app/assets/javascripts/design_management_legacy/utils/cache_update.js +++ /dev/null @@ -1,276 +0,0 @@ -/* eslint-disable @gitlab/require-i18n-strings */ - -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { extractCurrentDiscussion, extractDesign } from './design_management_utils'; -import { - ADD_IMAGE_DIFF_NOTE_ERROR, - UPDATE_IMAGE_DIFF_NOTE_ERROR, - ADD_DISCUSSION_COMMENT_ERROR, - designDeletionError, -} from './error_messages'; - -const deleteDesignsFromStore = (store, query, selectedDesigns) => { - const data = store.readQuery(query); - - const changedDesigns = data.project.issue.designCollection.designs.edges.filter( - ({ node }) => !selectedDesigns.includes(node.filename), - ); - data.project.issue.designCollection.designs.edges = [...changedDesigns]; - - store.writeQuery({ - ...query, - data, - }); -}; - -/** - * Adds a new version of designs to store - * - * @param {Object} store - * @param {Object} query - * @param {Object} version - */ -const addNewVersionToStore = (store, query, version) => { - if (!version) return; - - const data = store.readQuery(query); - const newEdge = { node: version, __typename: 'DesignVersionEdge' }; - - data.project.issue.designCollection.versions.edges = [ - newEdge, - ...data.project.issue.designCollection.versions.edges, - ]; - - store.writeQuery({ - ...query, - data, - }); -}; - -const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => { - const data = store.readQuery({ - query, - variables: queryVariables, - }); - - const design = extractDesign(data); - const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId); - currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note]; - - design.notesCount += 1; - if ( - !design.issue.participants.edges.some( - participant => participant.node.username === createNote.note.author.username, - ) - ) { - design.issue.participants.edges = [ - ...design.issue.participants.edges, - { - __typename: 'UserEdge', - node: { - __typename: 'User', - ...createNote.note.author, - }, - }, - ]; - } - store.writeQuery({ - query, - variables: queryVariables, - data: { - ...data, - design: { - ...design, - }, - }, - }); -}; - -const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => { - const data = store.readQuery({ - query, - variables, - }); - const newDiscussion = { - __typename: 'Discussion', - id: createImageDiffNote.note.discussion.id, - replyId: createImageDiffNote.note.discussion.replyId, - resolvable: true, - resolved: false, - resolvedAt: null, - resolvedBy: null, - notes: { - __typename: 'NoteConnection', - nodes: [createImageDiffNote.note], - }, - }; - const design = extractDesign(data); - const notesCount = design.notesCount + 1; - design.discussions.nodes = [...design.discussions.nodes, newDiscussion]; - if ( - !design.issue.participants.edges.some( - participant => participant.node.username === createImageDiffNote.note.author.username, - ) - ) { - design.issue.participants.edges = [ - ...design.issue.participants.edges, - { - __typename: 'UserEdge', - node: { - __typename: 'User', - ...createImageDiffNote.note.author, - }, - }, - ]; - } - store.writeQuery({ - query, - variables, - data: { - ...data, - design: { - ...design, - notesCount, - }, - }, - }); -}; - -const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => { - const data = store.readQuery({ - query, - variables, - }); - - const design = extractDesign(data); - const discussion = extractCurrentDiscussion( - design.discussions, - updateImageDiffNote.note.discussion.id, - ); - - discussion.notes = { - ...discussion.notes, - nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)], - }; - - store.writeQuery({ - query, - variables, - data: { - ...data, - design, - }, - }); -}; - -const addNewDesignToStore = (store, designManagementUpload, query) => { - const data = store.readQuery(query); - - const newDesigns = data.project.issue.designCollection.designs.edges.reduce((acc, design) => { - if (!acc.find(d => d.filename === design.node.filename)) { - acc.push(design.node); - } - - return acc; - }, designManagementUpload.designs); - - let newVersionNode; - const findNewVersions = designManagementUpload.designs.find(design => design.versions); - - if (findNewVersions) { - const findNewVersionsEdges = findNewVersions.versions.edges; - - if (findNewVersionsEdges && findNewVersionsEdges.length) { - newVersionNode = [findNewVersionsEdges[0]]; - } - } - - const newVersions = [ - ...(newVersionNode || []), - ...data.project.issue.designCollection.versions.edges, - ]; - - const updatedDesigns = { - __typename: 'DesignCollection', - designs: { - __typename: 'DesignConnection', - edges: newDesigns.map(design => ({ - __typename: 'DesignEdge', - node: design, - })), - }, - versions: { - __typename: 'DesignVersionConnection', - edges: newVersions, - }, - }; - - data.project.issue.designCollection = updatedDesigns; - - store.writeQuery({ - ...query, - data, - }); -}; - -const onError = (data, message) => { - createFlash(message); - throw new Error(data.errors); -}; - -export const hasErrors = ({ errors = [] }) => errors?.length; - -/** - * Updates a store after design deletion - * - * @param {Object} store - * @param {Object} data - * @param {Object} query - * @param {Array} designs - */ -export const updateStoreAfterDesignsDelete = (store, data, query, designs) => { - if (hasErrors(data)) { - onError(data, designDeletionError({ singular: designs.length === 1 })); - } else { - deleteDesignsFromStore(store, query, designs); - addNewVersionToStore(store, query, data.version); - } -}; - -export const updateStoreAfterAddDiscussionComment = ( - store, - data, - query, - queryVariables, - discussionId, -) => { - if (hasErrors(data)) { - onError(data, ADD_DISCUSSION_COMMENT_ERROR); - } else { - addDiscussionCommentToStore(store, data, query, queryVariables, discussionId); - } -}; - -export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => { - if (hasErrors(data)) { - onError(data, ADD_IMAGE_DIFF_NOTE_ERROR); - } else { - addImageDiffNoteToStore(store, data, query, queryVariables); - } -}; - -export const updateStoreAfterUpdateImageDiffNote = (store, data, query, queryVariables) => { - if (hasErrors(data)) { - onError(data, UPDATE_IMAGE_DIFF_NOTE_ERROR); - } else { - updateImageDiffNoteInStore(store, data, query, queryVariables); - } -}; - -export const updateStoreAfterUploadDesign = (store, data, query) => { - if (hasErrors(data)) { - onError(data, data.errors[0]); - } else { - addNewDesignToStore(store, data, query); - } -}; diff --git a/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js b/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js deleted file mode 100644 index 22705cf67a1..00000000000 --- a/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js +++ /dev/null @@ -1,128 +0,0 @@ -import { uniqueId } from 'lodash'; -import { VALID_DESIGN_FILE_MIMETYPE } from '../constants'; - -export const isValidDesignFile = ({ type }) => - (type.match(VALID_DESIGN_FILE_MIMETYPE.regex) || []).length > 0; - -/** - * Returns formatted array that doesn't contain - * `edges`->`node` nesting - * - * @param {Array} elements - */ - -export const extractNodes = elements => elements.edges.map(({ node }) => node); - -/** - * Returns formatted array of discussions that doesn't contain - * `edges`->`node` nesting for child notes - * - * @param {Array} discussions - */ - -export const extractDiscussions = discussions => - discussions.nodes.map((discussion, index) => ({ - ...discussion, - index: index + 1, - notes: discussion.notes.nodes, - })); - -/** - * Returns a discussion with the given id from discussions array - * - * @param {Array} discussions - */ - -export const extractCurrentDiscussion = (discussions, id) => - discussions.nodes.find(discussion => discussion.id === id); - -export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1]; - -export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1]; - -export const extractDesigns = data => data.project.issue.designCollection.designs.edges; - -export const extractDesign = data => (extractDesigns(data) || [])[0]?.node; - -/** - * Generates optimistic response for a design upload mutation - * @param {Array<File>} files - */ -export const designUploadOptimisticResponse = files => { - const designs = files.map(file => ({ - // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 - // eslint-disable-next-line @gitlab/require-i18n-strings - __typename: 'Design', - id: -uniqueId(), - image: '', - imageV432x230: '', - filename: file.name, - fullPath: '', - notesCount: 0, - event: 'NONE', - diffRefs: { - __typename: 'DiffRefs', - baseSha: '', - startSha: '', - headSha: '', - }, - discussions: { - __typename: 'DesignDiscussion', - nodes: [], - }, - versions: { - __typename: 'DesignVersionConnection', - edges: { - __typename: 'DesignVersionEdge', - node: { - __typename: 'DesignVersion', - id: -uniqueId(), - sha: -uniqueId(), - }, - }, - }, - })); - - return { - // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 - // eslint-disable-next-line @gitlab/require-i18n-strings - __typename: 'Mutation', - designManagementUpload: { - __typename: 'DesignManagementUploadPayload', - designs, - skippedDesigns: [], - errors: [], - }, - }; -}; - -/** - * Generates optimistic response for a design upload mutation - * @param {Array<File>} files - */ -export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({ - // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 - // eslint-disable-next-line @gitlab/require-i18n-strings - __typename: 'Mutation', - updateImageDiffNote: { - __typename: 'UpdateImageDiffNotePayload', - note: { - ...note, - position: { - ...note.position, - ...position, - }, - }, - errors: [], - }, -}); - -const normalizeAuthor = author => ({ - ...author, - web_url: author.webUrl, - avatar_url: author.avatarUrl, -}); - -export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node)); - -export const getPageLayoutElement = () => document.querySelector('.layout-page'); diff --git a/app/assets/javascripts/design_management_legacy/utils/error_messages.js b/app/assets/javascripts/design_management_legacy/utils/error_messages.js deleted file mode 100644 index 7666c726c2f..00000000000 --- a/app/assets/javascripts/design_management_legacy/utils/error_messages.js +++ /dev/null @@ -1,95 +0,0 @@ -import { __, s__, n__, sprintf } from '~/locale'; - -export const ADD_DISCUSSION_COMMENT_ERROR = s__( - 'DesignManagement|Could not add a new comment. Please try again.', -); - -export const ADD_IMAGE_DIFF_NOTE_ERROR = s__( - 'DesignManagement|Could not create new discussion. Please try again.', -); - -export const UPDATE_IMAGE_DIFF_NOTE_ERROR = s__( - 'DesignManagement|Could not update discussion. Please try again.', -); - -export const UPDATE_NOTE_ERROR = s__('DesignManagement|Could not update note. Please try again.'); - -export const UPLOAD_DESIGN_ERROR = s__( - 'DesignManagement|Error uploading a new design. Please try again.', -); - -export const UPLOAD_DESIGN_INVALID_FILETYPE_ERROR = __( - 'Could not upload your designs as one or more files uploaded are not supported.', -); - -export const DESIGN_NOT_FOUND_ERROR = __('Could not find design.'); - -export const DESIGN_VERSION_NOT_EXIST_ERROR = __('Requested design version does not exist.'); - -const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped.'); - -const ALL_DESIGNS_SKIPPED_MESSAGE = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__( - 'The designs you tried uploading did not change.', -)}`; - -export const EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE = __( - 'You can only upload one design when dropping onto an existing design.', -); - -export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __( - 'You must upload a file with the same file name when dropping onto an existing design.', -); - -const MAX_SKIPPED_FILES_LISTINGS = 5; - -const oneDesignSkippedMessage = filename => - `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${sprintf(s__('DesignManagement|%{filename} did not change.'), { - filename, - })}`; - -/** - * Return warning message indicating that some (but not all) uploaded - * files were skipped. - * @param {Array<{ filename }>} skippedFiles - */ -const someDesignsSkippedMessage = skippedFiles => { - const designsSkippedMessage = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__( - 'Some of the designs you tried uploading did not change:', - )}`; - - const moreText = sprintf(s__(`DesignManagement|and %{moreCount} more.`), { - moreCount: skippedFiles.length - MAX_SKIPPED_FILES_LISTINGS, - }); - - return `${designsSkippedMessage} ${skippedFiles - .slice(0, MAX_SKIPPED_FILES_LISTINGS) - .map(({ filename }) => filename) - .join(', ')}${skippedFiles.length > MAX_SKIPPED_FILES_LISTINGS ? `, ${moreText}` : '.'}`; -}; - -export const designDeletionError = ({ singular = true } = {}) => { - const design = singular ? __('a design') : __('designs'); - return sprintf(s__('Could not delete %{design}. Please try again.'), { - design, - }); -}; - -/** - * Return warning message, if applicable, that one, some or all uploaded - * files were skipped. - * @param {Array<{ filename }>} uploadedDesigns - * @param {Array<{ filename }>} skippedFiles - */ -export const designUploadSkippedWarning = (uploadedDesigns, skippedFiles) => { - if (skippedFiles.length === 0) { - return null; - } - - if (skippedFiles.length === uploadedDesigns.length) { - const { filename } = skippedFiles[0]; - - return n__(oneDesignSkippedMessage(filename), ALL_DESIGNS_SKIPPED_MESSAGE, skippedFiles.length); - } - - return someDesignsSkippedMessage(skippedFiles); -}; diff --git a/app/assets/javascripts/design_management_legacy/utils/tracking.js b/app/assets/javascripts/design_management_legacy/utils/tracking.js deleted file mode 100644 index 49fa306914c..00000000000 --- a/app/assets/javascripts/design_management_legacy/utils/tracking.js +++ /dev/null @@ -1,26 +0,0 @@ -import Tracking from '~/tracking'; - -// Tracking Constants -const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0'; -const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design'; -const DESIGN_TRACKING_EVENT_NAME = 'view_design'; - -export function trackDesignDetailView( - referer = '', - owner = '', - designVersion = 1, - latestVersion = false, -) { - Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENT_NAME, { - label: DESIGN_TRACKING_EVENT_NAME, - context: { - schema: DESIGN_TRACKING_CONTEXT_SCHEMA, - data: { - 'design-version-number': designVersion, - 'design-is-current-version': latestVersion, - 'internal-object-referrer': referer, - 'design-collection-owner': owner, - }, - }, - }); -} diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 1eb89b41495..ed68ca5cae9 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapGetters } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import IdeTree from './ide_tree.vue'; import ResizablePanel from './resizable_panel.vue'; import ActivityBar from './activity_bar.vue'; diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 36e8951bea3..776d8459515 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import FileTree from '~/vue_shared/components/file_tree.vue'; import IdeFileRow from './ide_file_row.vue'; import NavDropdown from './nav_dropdown.vue'; diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue index 46852e4ddd9..3be592baf29 100644 --- a/app/assets/javascripts/incidents/components/incidents_list.vue +++ b/app/assets/javascripts/incidents/components/incidents_list.vue @@ -208,6 +208,31 @@ export default { isEmpty() { return !this.incidents.list?.length; }, + showList() { + return !this.isEmpty || this.errored || this.loading; + }, + activeClosedTabHasNoIncidents() { + const { all, closed } = this.incidentsCount || {}; + const isClosedTabActive = this.statusFilter === this.$options.statusTabs[1].filters; + + return isClosedTabActive && all && !closed; + }, + emptyStateData() { + const { + emptyState: { title, emptyClosedTabTitle, description }, + createIncidentBtnLabel, + } = this.$options.i18n; + + if (this.activeClosedTabHasNoIncidents) { + return { title: emptyClosedTabTitle }; + } + return { + title, + description, + btnLink: this.newIncidentPath, + btnText: createIncidentBtnLabel, + }; + }, }, methods: { onInputChange: debounce(function debounceSearch(input) { @@ -279,7 +304,7 @@ export default { </gl-tabs> <gl-button - v-if="!isEmpty" + v-if="!isEmpty || activeClosedTabHasNoIncidents" class="gl-my-3 gl-mr-5 create-incident-button" data-testid="createIncidentBtn" data-qa-selector="create_incident_button" @@ -307,6 +332,7 @@ export default { {{ s__('IncidentManagement|Incidents') }} </h4> <gl-table + v-if="showList" :items="incidents.list || []" :fields="availableFields" :show-empty="true" @@ -379,21 +405,20 @@ export default { <gl-loading-icon size="lg" color="dark" class="mt-3" /> </template> - <template #empty> - <gl-empty-state - v-if="!errored" - :title="$options.i18n.emptyState.title" - :svg-path="emptyListSvgPath" - :description="$options.i18n.emptyState.description" - :primary-button-link="newIncidentPath" - :primary-button-text="$options.i18n.createIncidentBtnLabel" - /> - <span v-else> - {{ $options.i18n.noIncidents }} - </span> + <template v-if="errored" #empty> + {{ $options.i18n.noIncidents }} </template> </gl-table> + <gl-empty-state + v-else + :title="emptyStateData.title" + :svg-path="emptyListSvgPath" + :description="emptyStateData.description" + :primary-button-link="emptyStateData.btnLink" + :primary-button-text="emptyStateData.btnText" + /> + <gl-pagination v-if="showPaginationControls" :value="pagination.currentPage" diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js index 42016ed49dd..289b36d9848 100644 --- a/app/assets/javascripts/incidents/constants.js +++ b/app/assets/javascripts/incidents/constants.js @@ -9,6 +9,7 @@ export const I18N = { searchPlaceholder: __('Search results…'), emptyState: { title: s__('IncidentManagement|Display your incidents in a dedicated view'), + emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'), description: s__( 'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.', ), diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue index fecb7353efb..0d4f5bce965 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -3,7 +3,7 @@ import { toNumber, omit } from 'lodash'; import { GlEmptyState, GlPagination, - GlSkeletonLoading, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSafeHtmlDirective as SafeHtml, } from '@gitlab/ui'; import { deprecatedCreateFlash as flash } from '~/flash'; diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue index e6bf7a6ec02..bf810978648 100644 --- a/app/assets/javascripts/mr_popover/components/mr_popover.vue +++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue @@ -1,6 +1,6 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ -import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; +import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import CiIcon from '../../vue_shared/components/ci_icon.vue'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import query from '../queries/merge_request.query.graphql'; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 3940b4b4724..4372970927f 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -17,7 +17,7 @@ import Autosize from 'autosize'; import 'jquery.caret'; // required by at.js import '@gitlab/at.js'; import Vue from 'vue'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import AjaxCache from '~/lib/utils/ajax_cache'; import syntaxHighlight from '~/syntax_highlight'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 2a3b3156cc0..c01cd8f8037 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -1,7 +1,7 @@ <script> /* eslint-disable vue/no-v-html */ import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index a577d2e1ecd..f38c860c5d0 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -25,12 +25,6 @@ export default function() { initSentryErrorStackTraceApp(); initRelatedMergeRequestsApp(); - // This will be removed when we remove the `design_management_moved` feature flag - // See https://gitlab.com/gitlab-org/gitlab/-/issues/223197 - import(/* webpackChunkName: 'design_management' */ '~/design_management_legacy') - .then(module => module.default()) - .catch(() => {}); - import(/* webpackChunkName: 'design_management' */ '~/design_management') .then(module => module.default()) .catch(() => {}); diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index 67085ecca2b..22485a3344b 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -1,6 +1,11 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading, GlEmptyState, GlLink, GlButton } from '@gitlab/ui'; +import { + GlDeprecatedSkeletonLoading as GlSkeletonLoading, + GlEmptyState, + GlLink, + GlButton, +} from '@gitlab/ui'; import { getParameterByName, historyPushState, diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue index 0e65d722952..8b89f0cf3fc 100644 --- a/app/assets/javascripts/releases/components/app_show.vue +++ b/app/assets/javascripts/releases/components/app_show.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import ReleaseBlock from './release_block.vue'; export default { diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index fd70a6419fc..c6652c57c1f 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoading, GlButton } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui'; import { sprintf, __ } from '../../../locale'; import getRefMixin from '../../mixins/get_ref'; import projectPathQuery from '../../queries/project_path.query.graphql'; diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index c6c41e4146a..d749a8c0dee 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -4,7 +4,7 @@ import { escapeRegExp } from 'lodash'; import { GlBadge, GlLink, - GlSkeletonLoading, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon, GlIcon, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue index c7d9453a5c9..4de41dd5887 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; import { n__ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue'; diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index 0248dc79441..6bb05e59f6b 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -3,7 +3,7 @@ import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import { forEach, escape } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue index e75ac8c54bc..53dbae39608 100644 --- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; export default { diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 4fa9724448d..b0f2f846ab2 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -19,7 +19,12 @@ */ import $ from 'jquery'; import { mapGetters, mapActions, mapState } from 'vuex'; -import { GlDeprecatedButton, GlSkeletonLoading, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { + GlDeprecatedButton, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, + GlTooltipDirective, + GlIcon, +} from '@gitlab/ui'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; import noteHeader from '~/notes/components/note_header.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index fd4fcca312a..6aaff000845 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -1,6 +1,6 @@ <script> /* eslint-disable vue/no-v-html */ -import { GlPopover, GlSkeletonLoading, GlIcon } from '@gitlab/ui'; +import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlIcon } from '@gitlab/ui'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; import { glEmojiTag } from '../../../emoji'; diff --git a/app/views/projects/issues/_design_management.html.haml b/app/views/projects/issues/_design_management.html.haml index 9d88d77eac9..0c962cdd2b6 100644 --- a/app/views/projects/issues/_design_management.html.haml +++ b/app/views/projects/issues/_design_management.html.haml @@ -4,20 +4,7 @@ - enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end } - if @project.design_management_enabled? - - if Feature.enabled?(:design_management_moved, @project, default_enabled: true) - .js-design-management-new{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } } - - else - .js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } } + .js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } } - else - - if Feature.enabled?(:design_management_moved, @project, default_enabled: true) - .gl-border-solid.gl-border-1.gl-border-gray-100.gl-rounded-base.gl-mt-5.gl-p-3.gl-text-center - = enable_lfs_message - - else - .mt-4 - .row.empty-state - .col-12 - .text-content - %h4.center - = _('The one place for your designs') - %p.center - = enable_lfs_message + .gl-border-solid.gl-border-1.gl-border-gray-100.gl-rounded-base.gl-mt-5.gl-p-3.gl-text-center + = enable_lfs_message diff --git a/app/views/projects/issues/_tabs.html.haml b/app/views/projects/issues/_tabs.html.haml deleted file mode 100644 index d998a01623f..00000000000 --- a/app/views/projects/issues/_tabs.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%ul.nav-tabs.nav.nav-links{ role: 'tablist' } - %li - = link_to '#discussion-tab', class: 'active js-issue-tabs', id: 'discussion', role: 'tab', 'aria-controls': 'js-discussion', 'aria-selected': 'true', data: { toggle: 'tab', target: '#discussion-tab', qa_selector: 'discussion_tab_link' } do - = _('Discussion') - %span.badge.badge-pill.js-discussions-count - %li - = link_to '#designs-tab', class: 'js-issue-tabs', id: 'designs', role: 'tab', 'aria-controls': 'js-designs', 'aria-selected': 'false', data: { toggle: 'tab', target: '#designs-tab', qa_selector: 'designs_tab_link' } do - = _('Designs') - %span.badge.badge-pill.js-designs-count -.tab-content - #discussion-tab.tab-pane.show.active{ role: 'tabpanel', 'aria-labelledby': 'discussion', data: { qa_selector: 'discussion_tab_content' } } - = render 'projects/issues/discussion' - #designs-tab.tab-pane{ role: 'tabpanel', 'aria-labelledby': 'designs', data: { qa_selector: 'designs_tab_content' } } - = render 'projects/issues/design_management' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 8d8fdcf70ea..c762b044c3e 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -76,8 +76,7 @@ - if @issue.sentry_issue.present? #js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) } - - if Feature.enabled?(:design_management_moved, @project, default_enabled: true) - = render 'projects/issues/design_management' + = render 'projects/issues/design_management' = render_if_exists 'projects/issues/related_issues' @@ -97,9 +96,6 @@ #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } } = render 'new_branch' if show_new_branch_button? - - if Feature.enabled?(:design_management_moved, @project, default_enabled: true) - = render 'projects/issues/discussion' - - else - = render 'projects/issues/tabs' + = render 'projects/issues/discussion' = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees diff --git a/changelogs/unreleased/229636-incident-empty-state-polish.yml b/changelogs/unreleased/229636-incident-empty-state-polish.yml new file mode 100644 index 00000000000..425c3d22ad9 --- /dev/null +++ b/changelogs/unreleased/229636-incident-empty-state-polish.yml @@ -0,0 +1,5 @@ +--- +title: Update empty state behavior for incidents list +merge_request: 40872 +author: +type: other diff --git a/changelogs/unreleased/agent_gitops_sync_count.yml b/changelogs/unreleased/agent_gitops_sync_count.yml new file mode 100644 index 00000000000..e1fb7bfe0f9 --- /dev/null +++ b/changelogs/unreleased/agent_gitops_sync_count.yml @@ -0,0 +1,5 @@ +--- +title: Add kubernetes_agent_gitops_sync usage ping metric +merge_request: 40568 +author: +type: other diff --git a/changelogs/unreleased/dblessing-doorkeeper-hex-generator.yml b/changelogs/unreleased/dblessing-doorkeeper-hex-generator.yml new file mode 100644 index 00000000000..7b21903d555 --- /dev/null +++ b/changelogs/unreleased/dblessing-doorkeeper-hex-generator.yml @@ -0,0 +1,5 @@ +--- +title: Restore doorkeeper generator to hex due to breaking change +merge_request: 41169 +author: +type: fixed diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 2f98471772f..6b54b5074d5 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -3,6 +3,10 @@ Doorkeeper.configure do # Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper orm :active_record + # Restore to pre-5.1 generator due to breaking change. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/244371 + default_generator_method :hex + # This block will be called to check whether the resource owner is authenticated or not. resource_owner_authenticator do # Put your resource owner authentication logic here. diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md index 75183f54990..e823dc61023 100644 --- a/doc/administration/auth/ldap/ldap-troubleshooting.md +++ b/doc/administration/auth/ldap/ldap-troubleshooting.md @@ -136,6 +136,27 @@ are true for the user in question: - Run [an LDAP check command](#ldap-check) to make sure that the LDAP settings are correct and [GitLab can see your users](#no-users-are-found). +#### Access denied for your LDAP account + +There is [a bug](https://gitlab.com/gitlab-org/gitlab/-/issues/235930) that +may affect users with [Auditor level access](../../auditor_users.md). When +downgrading from Premium/Ultimate, Auditor users who try to sign in +may see the following message: `Access denied for your LDAP account`. + +We have a workaround, based on toggling the access level of affected users: + +1. As an administrator, go to **Admin Area > Overview > Users**. +1. Select the name of the affected user. +1. In the user's administrative page, press **Edit** on the top right of the page. +1. Change the user's access level from **Regular** to **Admin** (or vice versa), + and press **Save changes** at the bottom of the page. +1. Press **Edit** on the top right of the user's profile page + again. +1. Restore the user's original access level (**Regular** or **Admin**) + and press **Save changes** again. + +The user should now be able to sign in. + #### Email has already been taken A user tries to sign-in with the correct LDAP credentials, is denied access, diff --git a/doc/api/snippets.md b/doc/api/snippets.md index 0cdc07b1f46..6863763ff24 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -169,9 +169,9 @@ Parameters: | Attribute | Type | Required | Description | |:------------|:--------|:---------|:-------------------------------------------------------------------| -| `id` | integer | yes | ID of snippet to retrieve | -| `ref` | string | yes | Reference to a tag, branch or commit | -| `file_path` | string | yes | URL-encoded path to the file | +| `id` | integer | yes | ID of snippet to retrieve. | +| `ref` | string | yes | Reference to a tag, branch or commit. | +| `file_path` | string | yes | URL-encoded path to the file. | Example request: diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index f516f4080fa..bb7420e24e9 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -377,7 +377,7 @@ Alternatively, when users need to [link SAML to their existing GitLab.com accoun | Cause | Solution | |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| As mentioned in the [NameID](#nameid) section, if the NameID changes for any user, the user can be locked out. This is a common problem when an email address is used as the identifier. | Follow the steps outlined in the ["SAML authentication failed: User has already been taken"](#message-saml-authentication-failed-user-has-already-been-taken) section. If many users are affected, we recommend that you use the appropriate API. | +| As mentioned in the [NameID](#nameid) section, if the NameID changes for any user, the user can be locked out. This is a common problem when an email address is used as the identifier. | Follow the steps outlined in the ["SAML authentication failed: User has already been taken"](#message-saml-authentication-failed-user-has-already-been-taken) section. | ### I need to change my SAML app diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index 5e456c7986c..94136ae171d 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -72,39 +72,12 @@ and connect to GitLab through a personal access token. The details are explained ## The Design Management section > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223193) in GitLab 13.2, Designs are displayed directly on the issue description rather than on a separate tab. -> - The new display is deployed behind a feature flag, enabled by default. -> - It's enabled on GitLab.com. -> - It cannot be enabled or disabled per-project. -> - It's recommended for production use. -> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-displaying-designs-on-the-issue-description-core-only). If disabled, it will move Designs back to the **Designs** tab. +> - New display's feature flag [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/223197) in GitLab 13.4. You can find to the **Design Management** section in the issue description: ![Designs section](img/design_management_v13_2.png) -### Enable or disable displaying Designs on the issue description **(CORE ONLY)** - -Displaying Designs on the issue description is under development but ready for -production use. It is deployed behind a feature flag that is **enabled by -default**. -[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) -can opt to disable it for your instance. - -To disable it: - -```ruby -Feature.disable(:design_management_moved) -``` - -To enable it: - -```ruby -Feature.enable(:design_management_moved) -``` - -By disabling this feature, designs will be displayed on the **Designs** tab -instead of directly on the issue description. - ## Adding designs To upload Design images, drag files from your computer and drop them in the Design Management section, diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index 49ace13f322..bab829a609e 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -96,6 +96,25 @@ module API gitaly_repository: gitaly_repository(project) } end + + desc 'POST usage metrics' do + detail 'Updates usage metrics for agent' + end + route_setting :authentication, cluster_agent_token_allowed: true + params do + requires :gitops_sync_count, type: Integer, desc: 'The count to increment the gitops_sync metric by' + end + post '/usage_metrics' do + gitops_sync_count = params[:gitops_sync_count] + + if gitops_sync_count < 0 + bad_request!('gitops_sync_count must be greater than or equal to zero') + else + Gitlab::UsageDataCounters::KubernetesAgentCounter.increment_gitops_sync(gitops_sync_count) + + no_content! + end + end end end end diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb index 803acef9a40..8d5611411c9 100644 --- a/lib/gitlab/error_tracking.rb +++ b/lib/gitlab/error_tracking.rb @@ -26,8 +26,6 @@ module Gitlab # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) config.processors << ::Gitlab::ErrorTracking::Processor::SidekiqProcessor - config.processors << ::Gitlab::ErrorTracking::Processor::GrpcErrorProcessor - # Sanitize authentication headers config.sanitize_http_headers = %w[Authorization Private-Token] config.tags = extra_tags_from_env.merge(program: Gitlab.process_name) diff --git a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb deleted file mode 100644 index a19e066a660..00000000000 --- a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module ErrorTracking - module Processor - class GrpcErrorProcessor < ::Raven::Processor - DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:(.*)') - - def process(value) - return value unless grpc_exception?(value) - - process_message(value) - process_exception_values(value) - process_custom_fingerprint(value) - - value - end - - def grpc_exception?(value) - value[:exception] && value[:message].start_with?('GRPC::') - end - - def process_message(value) - message, debug_str = split_debug_error_string(value[:message]) - - return unless message - - value[:message] = message - extra = value[:extra] || {} - extra[:grpc_debug_error_string] = debug_str if debug_str - end - - def process_exception_values(value) - exceptions = value.dig(:exception, :values) - - return unless exceptions.is_a?(Array) - - exceptions.each do |entry| - message, _ = split_debug_error_string(entry[:value]) - entry[:value] = message if message - end - end - - def process_custom_fingerprint(value) - fingerprint = value[:fingerprint] - - return value unless custom_grpc_fingerprint?(fingerprint) - - message, _ = split_debug_error_string(fingerprint[1]) - fingerprint[1] = message if message - end - - private - - def custom_grpc_fingerprint?(fingerprint) - fingerprint.is_a?(Array) && fingerprint.length == 2 && fingerprint[0].start_with?('GRPC::') - end - - def split_debug_error_string(message) - return unless message - - match = DEBUG_ERROR_STRING_REGEX.match(message) - - return unless match - - [match[1], match[2]] - end - end - end - end -end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index b6bffb11344..96f3487fd6f 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -13,7 +13,6 @@ module Gitlab TAG_REF_PREFIX = "refs/tags/" BRANCH_REF_PREFIX = "refs/heads/" - BaseError = Class.new(StandardError) CommandError = Class.new(BaseError) CommitError = Class.new(BaseError) OSError = Class.new(BaseError) diff --git a/lib/gitlab/git/base_error.rb b/lib/gitlab/git/base_error.rb new file mode 100644 index 00000000000..a7eaa82b347 --- /dev/null +++ b/lib/gitlab/git/base_error.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class BaseError < StandardError + DEBUG_ERROR_STRING_REGEX = /(.*?) debug_error_string:.*$/m.freeze + + def initialize(msg = nil) + if msg + raw_message = msg.to_s + match = DEBUG_ERROR_STRING_REGEX.match(raw_message) + raw_message = match[1] if match + + super(raw_message) + else + super + end + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index a9e2ba4d85c..b2c296cc3d5 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -245,7 +245,8 @@ module Gitlab Gitlab::UsageDataCounters::ProductivityAnalyticsCounter, Gitlab::UsageDataCounters::SourceCodeCounter, Gitlab::UsageDataCounters::MergeRequestCounter, - Gitlab::UsageDataCounters::DesignsCounter + Gitlab::UsageDataCounters::DesignsCounter, + Gitlab::UsageDataCounters::KubernetesAgentCounter ] end diff --git a/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb b/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb new file mode 100644 index 00000000000..eae42bdc4a1 --- /dev/null +++ b/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + class KubernetesAgentCounter < BaseCounter + PREFIX = 'kubernetes_agent' + KNOWN_EVENTS = %w[gitops_sync].freeze + + class << self + def increment_gitops_sync(incr) + raise ArgumentError, 'must be greater than or equal to zero' if incr < 0 + + # rather then hitting redis for this no-op, we return early + # note: redis returns the increment, so we mimic this here + return 0 if incr == 0 + + increment_by(redis_key(:gitops_sync), incr) + end + end + end + end +end diff --git a/lib/gitlab/usage_data_counters/redis_counter.rb b/lib/gitlab/usage_data_counters/redis_counter.rb index 75d5a75e3a4..2406f771fd8 100644 --- a/lib/gitlab/usage_data_counters/redis_counter.rb +++ b/lib/gitlab/usage_data_counters/redis_counter.rb @@ -9,6 +9,12 @@ module Gitlab Gitlab::Redis::SharedState.with { |redis| redis.incr(redis_counter_key) } end + def increment_by(redis_counter_key, incr) + return unless Gitlab::CurrentSettings.usage_ping_enabled + + Gitlab::Redis::SharedState.with { |redis| redis.incrby(redis_counter_key, incr) } + end + def total_count(redis_counter_key) Gitlab::Redis::SharedState.with { |redis| redis.get(redis_counter_key).to_i } end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 17aed3bf769..ed03c8b01a0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -515,9 +515,6 @@ msgstr "" msgid "%{level_name} is not allowed since the fork source project has lower visibility." msgstr "" -msgid "%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}." -msgstr "" - msgid "%{link_start}Learn more%{link_end} about what information is shared with GitLab Inc." msgstr "" @@ -7039,9 +7036,6 @@ msgstr "" msgid "Could not create wiki page" msgstr "" -msgid "Could not delete %{design}. Please try again." -msgstr "" - msgid "Could not delete chat nickname %{chat_name}." msgstr "" @@ -8046,9 +8040,6 @@ msgstr "" msgid "Deleted chat nickname: %{chat_name}!" msgstr "" -msgid "Deleted in this version" -msgstr "" - msgid "Deleted projects" msgstr "" @@ -8487,9 +8478,6 @@ msgstr "" msgid "DesignManagement|Are you sure you want to cancel creating this comment?" msgstr "" -msgid "DesignManagement|Are you sure you want to delete the selected designs?" -msgstr "" - msgid "DesignManagement|Cancel changes" msgstr "" @@ -8520,15 +8508,6 @@ msgstr "" msgid "DesignManagement|Could not update note. Please try again." msgstr "" -msgid "DesignManagement|Delete" -msgstr "" - -msgid "DesignManagement|Delete designs confirmation" -msgstr "" - -msgid "DesignManagement|Delete selected" -msgstr "" - msgid "DesignManagement|Deselect all" msgstr "" @@ -8745,9 +8724,6 @@ msgstr "" msgid "Discuss a specific suggestion or question." msgstr "" -msgid "Discussion" -msgstr "" - msgid "Discussion to reply to cannot be found" msgstr "" @@ -13115,6 +13091,9 @@ msgstr "" msgid "IncidentManagement|Published to status page" msgstr "" +msgid "IncidentManagement|There are no closed incidents" +msgstr "" + msgid "IncidentManagement|There was an error displaying the incidents." msgstr "" @@ -22750,12 +22729,6 @@ msgstr "" msgid "Showing %{pageSize} of %{total} issues" msgstr "" -msgid "Showing Latest Version" -msgstr "" - -msgid "Showing Version #%{versionNumber}" -msgstr "" - msgid "Showing all issues" msgstr "" @@ -24421,9 +24394,6 @@ msgstr "" msgid "The %{link_start}true-up model%{link_end} allows having more users, and additional users will incur a retroactive charge on renewal." msgstr "" -msgid "The %{true_up_link_start}true-up model%{link_end} has a retroactive charge for these users at the next renewal. If you want to update your license sooner to prevent this, %{support_link_start}please contact our Support team%{link_end}." -msgstr "" - msgid "The %{type} contains the following error:" msgid_plural "The %{type} contains the following errors:" msgstr[0] "" @@ -24659,9 +24629,6 @@ msgstr "" msgid "The number of times an upload record could not find its file" msgstr "" -msgid "The one place for your designs" -msgstr "" - msgid "The parent epic is confidential and can only contain confidential epics and issues" msgstr "" @@ -28554,6 +28521,9 @@ msgstr "" msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile" msgstr "" +msgid "You'll be charged for %{true_up_link_start}users over license%{link_end} on a quartely or annual basis, depending on the terms of your agreement." +msgstr "" + msgid "You'll be signed out from your current account automatically." msgstr "" @@ -28746,6 +28716,9 @@ msgstr "" msgid "Your instance has %{remaining_user_count} users remaining of the %{total_user_count} included in your subscription. You can add more users than the number included in your license, and we will include the overage in your next bill." msgstr "" +msgid "Your instance has exceeded your subscription's licensed user count." +msgstr "" + msgid "Your instance is approaching its licensed user count" msgstr "" diff --git a/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb index aff8951d9de..908e30478b2 100644 --- a/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb +++ b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb @@ -8,57 +8,26 @@ RSpec.describe 'User paginates issue designs', :js do let(:project) { create(:project_empty_repo, :public) } let(:issue) { create(:issue, project: project) } - context 'design_management_moved flag disabled' do - before do - stub_feature_flags(design_management_moved: false) - enable_design_management - - create_list(:design, 2, :with_file, issue: issue) - visit project_issue_path(project, issue) - click_link 'Designs' - wait_for_requests - find('.js-design-list-item', match: :first).click - end - - it 'paginates to next design' do - expect(find('.js-previous-design')[:disabled]).to eq('true') - - page.within(find('.js-design-header')) do - expect(page).to have_content('1 of 2') - end - - find('.js-next-design').click - - expect(find('.js-previous-design')[:disabled]).not_to eq('true') - - page.within(find('.js-design-header')) do - expect(page).to have_content('2 of 2') - end - end + before do + enable_design_management + create_list(:design, 2, :with_file, issue: issue) + visit project_issue_path(project, issue) + find('.js-design-list-item', match: :first).click end - context 'design_management_moved flag enabled' do - before do - enable_design_management - create_list(:design, 2, :with_file, issue: issue) - visit project_issue_path(project, issue) - find('.js-design-list-item', match: :first).click - end + it 'paginates to next design' do + expect(find('.js-previous-design')[:disabled]).to eq('true') - it 'paginates to next design' do - expect(find('.js-previous-design')[:disabled]).to eq('true') - - page.within(find('.js-design-header')) do - expect(page).to have_content('1 of 2') - end + page.within(find('.js-design-header')) do + expect(page).to have_content('1 of 2') + end - find('.js-next-design').click + find('.js-next-design').click - expect(find('.js-previous-design')[:disabled]).not_to eq('true') + expect(find('.js-previous-design')[:disabled]).not_to eq('true') - page.within(find('.js-design-header')) do - expect(page).to have_content('2 of 2') - end + page.within(find('.js-design-header')) do + expect(page).to have_content('2 of 2') end end end diff --git a/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb index 4e45312eac3..cfd8a4540ee 100644 --- a/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb +++ b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb @@ -8,32 +8,13 @@ RSpec.describe 'User design permissions', :js do let(:project) { create(:project_empty_repo, :public) } let(:issue) { create(:issue, project: project) } - context 'design_management_moved flag disabled' do - before do - enable_design_management - stub_feature_flags(design_management_moved: false) + before do + enable_design_management - visit project_issue_path(project, issue) - - click_link 'Designs' - - wait_for_requests - end - - it 'user does not have permissions to upload design' do - expect(page).not_to have_field('design_file') - end + visit project_issue_path(project, issue) end - context 'design_management_moved flag enabled' do - before do - enable_design_management - - visit project_issue_path(project, issue) - end - - it 'user does not have permissions to upload design' do - expect(page).not_to have_field('design_file') - end + it 'user does not have permissions to upload design' do + expect(page).not_to have_field('design_file') end end diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb index 29a27992a0d..8998cae621d 100644 --- a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb +++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb @@ -11,81 +11,34 @@ RSpec.describe 'User uploads new design', :js do before do sign_in(user) + enable_design_management(feature_enabled) + visit project_issue_path(project, issue) end - context 'design_management_moved flag disabled' do - before do - enable_design_management(feature_enabled) - stub_feature_flags(design_management_moved: false) - visit project_issue_path(project, issue) + context "when the feature is available" do + let(:feature_enabled) { true } - click_link 'Designs' + it 'uploads designs' do + upload_design(logo_fixture, count: 1) - wait_for_requests - end - - context "when the feature is available" do - let(:feature_enabled) { true } - - it 'uploads designs' do - upload_design(logo_fixture, count: 1) - - expect(page).to have_selector('.js-design-list-item', count: 1) - - within first('#designs-tab .js-design-list-item') do - expect(page).to have_content('dk.png') - end + expect(page).to have_selector('.js-design-list-item', count: 1) - upload_design(gif_fixture, count: 2) - - # Known bug in the legacy implementation: new designs are inserted - # in the beginning on the frontend. - expect(page).to have_selector('.js-design-list-item', count: 2) - expect(page.all('.js-design-list-item').map(&:text)).to eq(['banana_sample.gif', 'dk.png']) + within first('[data-testid="designs-root"] .js-design-list-item') do + expect(page).to have_content('dk.png') end - end - context 'when the feature is not available' do - let(:feature_enabled) { false } + upload_design(gif_fixture, count: 2) - it 'shows the message about requirements' do - expect(page).to have_content("To upload designs, you'll need to enable LFS.") - end + expect(page).to have_selector('.js-design-list-item', count: 2) + expect(page.all('.js-design-list-item').map(&:text)).to eq(['dk.png', 'banana_sample.gif']) end end - context 'design_management_moved flag enabled' do - before do - enable_design_management(feature_enabled) - stub_feature_flags(design_management_moved: true) - visit project_issue_path(project, issue) - end - - context "when the feature is available" do - let(:feature_enabled) { true } + context 'when the feature is not available' do + let(:feature_enabled) { false } - it 'uploads designs' do - upload_design(logo_fixture, count: 1) - - expect(page).to have_selector('.js-design-list-item', count: 1) - - within first('[data-testid="designs-root"] .js-design-list-item') do - expect(page).to have_content('dk.png') - end - - upload_design(gif_fixture, count: 2) - - expect(page).to have_selector('.js-design-list-item', count: 2) - expect(page.all('.js-design-list-item').map(&:text)).to eq(['dk.png', 'banana_sample.gif']) - end - end - - context 'when the feature is not available' do - let(:feature_enabled) { false } - - it 'shows the message about requirements' do - expect(page).to have_content("To upload designs, you'll need to enable LFS.") - end + it 'shows the message about requirements' do + expect(page).to have_content("To upload designs, you'll need to enable LFS.") end end diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb index 49245218e81..b513a4fe3fa 100644 --- a/spec/features/projects/issues/design_management/user_views_design_spec.rb +++ b/spec/features/projects/issues/design_management/user_views_design_spec.rb @@ -9,42 +9,19 @@ RSpec.describe 'User views issue designs', :js do let_it_be(:issue) { create(:issue, project: project) } let_it_be(:design) { create(:design, :with_file, issue: issue) } - context 'design_management_moved flag disabled' do - before do - enable_design_management - stub_feature_flags(design_management_moved: false) + before do + enable_design_management - visit project_issue_path(project, issue) - - click_link 'Designs' - end - - it 'opens design detail' do - click_link design.filename - - page.within(find('.js-design-header')) do - expect(page).to have_content(design.filename) - end - - expect(page).to have_selector('.js-design-image') - end + visit project_issue_path(project, issue) end - context 'design_management_moved flag enabled' do - before do - enable_design_management + it 'opens design detail' do + click_link design.filename - visit project_issue_path(project, issue) + page.within(find('.js-design-header')) do + expect(page).to have_content(design.filename) end - it 'opens design detail' do - click_link design.filename - - page.within(find('.js-design-header')) do - expect(page).to have_content(design.filename) - end - - expect(page).to have_selector('.js-design-image') - end + expect(page).to have_selector('.js-design-image') end end diff --git a/spec/features/projects/issues/design_management/user_views_designs_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_spec.rb index 772a9ffbe6f..46c772027ad 100644 --- a/spec/features/projects/issues/design_management/user_views_designs_spec.rb +++ b/spec/features/projects/issues/design_management/user_views_designs_spec.rb @@ -9,78 +9,37 @@ RSpec.describe 'User views issue designs', :js do let_it_be(:issue) { create(:issue, project: project) } let_it_be(:design) { create(:design, :with_file, issue: issue) } - context 'design_management_moved flag disabled' do - before do - enable_design_management - stub_feature_flags(design_management_moved: false) - end - - context 'navigates from the issue view' do - before do - visit project_issue_path(project, issue) - click_link 'Designs' - wait_for_requests - end - - it 'fetches list of designs' do - expect(page).to have_selector('.js-design-list-item', count: 1) - end - end - - context 'navigates directly to the design collection view' do - before do - visit designs_project_issue_path(project, issue) - end + before do + enable_design_management + end - it 'expands the sidebar' do - expect(page).to have_selector('.layout-page.right-sidebar-expanded') - end + context 'navigates from the issue view' do + before do + visit project_issue_path(project, issue) end - context 'navigates directly to the individual design view' do - before do - visit designs_project_issue_path(project, issue, vueroute: design.filename) - end - - it 'sees the design' do - expect(page).to have_selector('.js-design-detail') - end + it 'fetches list of designs' do + expect(page).to have_selector('.js-design-list-item', count: 1) end end - context 'design_management_moved flag enabled' do + context 'navigates directly to the design collection view' do before do - enable_design_management + visit designs_project_issue_path(project, issue) end - context 'navigates from the issue view' do - before do - visit project_issue_path(project, issue) - end - - it 'fetches list of designs' do - expect(page).to have_selector('.js-design-list-item', count: 1) - end + it 'expands the sidebar' do + expect(page).to have_selector('.layout-page.right-sidebar-expanded') end + end - context 'navigates directly to the design collection view' do - before do - visit designs_project_issue_path(project, issue) - end - - it 'expands the sidebar' do - expect(page).to have_selector('.layout-page.right-sidebar-expanded') - end + context 'navigates directly to the individual design view' do + before do + visit designs_project_issue_path(project, issue, vueroute: design.filename) end - context 'navigates directly to the individual design view' do - before do - visit designs_project_issue_path(project, issue, vueroute: design.filename) - end - - it 'sees the design' do - expect(page).to have_selector('.js-design-detail') - end + it 'sees the design' do + expect(page).to have_selector('.js-design-detail') end end end diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js index c6a5f66a627..628c35ae839 100644 --- a/spec/frontend/clusters_list/components/clusters_spec.js +++ b/spec/frontend/clusters_list/components/clusters_spec.js @@ -1,6 +1,11 @@ import MockAdapter from 'axios-mock-adapter'; import { mount } from '@vue/test-utils'; -import { GlLoadingIcon, GlPagination, GlSkeletonLoading, GlTable } from '@gitlab/ui'; +import { + GlLoadingIcon, + GlPagination, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, + GlTable, +} from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import axios from '~/lib/utils/axios_utils'; import Clusters from '~/clusters_list/components/clusters.vue'; diff --git a/spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap deleted file mode 100644 index 62a0f675cff..00000000000 --- a/spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design note pin component should match the snapshot of note when repositioning 1`] = ` -<button - aria-label="Comment form position" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator" - style="left: 10px; top: 10px; cursor: move;" - type="button" -> - <gl-icon-stub - name="image-comment-dark" - size="24" - /> -</button> -`; - -exports[`Design note pin component should match the snapshot of note with index 1`] = ` -<button - aria-label="Comment '1' position" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 js-image-badge badge badge-pill" - style="left: 10px; top: 10px;" - type="button" -> - - 1 - -</button> -`; - -exports[`Design note pin component should match the snapshot of note without index 1`] = ` -<button - aria-label="Comment form position" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator" - style="left: 10px; top: 10px;" - type="button" -> - <gl-icon-stub - name="image-comment-dark" - size="24" - /> -</button> -`; diff --git a/spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap deleted file mode 100644 index 189962c5b2e..00000000000 --- a/spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap +++ /dev/null @@ -1,104 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management design presentation component currentCommentForm is equal to current annotation position when isAnnotating is true 1`] = ` -<div - class="h-100 w-100 p-3 overflow-auto position-relative" -> - <div - class="h-100 w-100 d-flex align-items-center position-relative" - > - <design-image-stub - image="test.jpg" - name="test" - scale="1" - /> - - <design-overlay-stub - currentcommentform="[object Object]" - dimensions="[object Object]" - notes="" - position="[object Object]" - /> - </div> -</div> -`; - -exports[`Design management design presentation component currentCommentForm is null when isAnnotating is false 1`] = ` -<div - class="h-100 w-100 p-3 overflow-auto position-relative" -> - <div - class="h-100 w-100 d-flex align-items-center position-relative" - > - <design-image-stub - image="test.jpg" - name="test" - scale="1" - /> - - <design-overlay-stub - dimensions="[object Object]" - notes="" - position="[object Object]" - /> - </div> -</div> -`; - -exports[`Design management design presentation component currentCommentForm is null when isAnnotating is true but annotation position is falsey 1`] = ` -<div - class="h-100 w-100 p-3 overflow-auto position-relative" -> - <div - class="h-100 w-100 d-flex align-items-center position-relative" - > - <design-image-stub - image="test.jpg" - name="test" - scale="1" - /> - - <design-overlay-stub - dimensions="[object Object]" - notes="" - position="[object Object]" - /> - </div> -</div> -`; - -exports[`Design management design presentation component renders empty state when no image provided 1`] = ` -<div - class="h-100 w-100 p-3 overflow-auto position-relative" -> - <div - class="h-100 w-100 d-flex align-items-center position-relative" - > - <!----> - - <!----> - </div> -</div> -`; - -exports[`Design management design presentation component renders image and overlay when image provided 1`] = ` -<div - class="h-100 w-100 p-3 overflow-auto position-relative" -> - <div - class="h-100 w-100 d-flex align-items-center position-relative" - > - <design-image-stub - image="test.jpg" - name="test" - scale="1" - /> - - <design-overlay-stub - dimensions="[object Object]" - notes="" - position="[object Object]" - /> - </div> -</div> -`; diff --git a/spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap deleted file mode 100644 index cb4575cbd11..00000000000 --- a/spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap +++ /dev/null @@ -1,115 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management design scaler component minus and reset buttons are disabled when scale === 1 1`] = ` -<div - class="design-scaler btn-group" - role="group" -> - <button - class="btn" - disabled="disabled" - > - <span - class="d-flex-center gl-icon s16" - > - - – - - </span> - </button> - - <button - class="btn" - disabled="disabled" - > - <gl-icon-stub - name="redo" - size="16" - /> - </button> - - <button - class="btn" - > - <gl-icon-stub - name="plus" - size="16" - /> - </button> -</div> -`; - -exports[`Design management design scaler component minus and reset buttons are enabled when scale > 1 1`] = ` -<div - class="design-scaler btn-group" - role="group" -> - <button - class="btn" - > - <span - class="d-flex-center gl-icon s16" - > - - – - - </span> - </button> - - <button - class="btn" - > - <gl-icon-stub - name="redo" - size="16" - /> - </button> - - <button - class="btn" - > - <gl-icon-stub - name="plus" - size="16" - /> - </button> -</div> -`; - -exports[`Design management design scaler component plus button is disabled when scale === 2 1`] = ` -<div - class="design-scaler btn-group" - role="group" -> - <button - class="btn" - > - <span - class="d-flex-center gl-icon s16" - > - - – - - </span> - </button> - - <button - class="btn" - > - <gl-icon-stub - name="redo" - size="16" - /> - </button> - - <button - class="btn" - disabled="disabled" - > - <gl-icon-stub - name="plus" - size="16" - /> - </button> -</div> -`; diff --git a/spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap deleted file mode 100644 index acaa62b11eb..00000000000 --- a/spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap +++ /dev/null @@ -1,68 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management large image component renders image 1`] = ` -<div - class="m-auto js-design-image" -> - <!----> - - <img - alt="test" - class="mh-100 img-fluid" - src="test.jpg" - /> -</div> -`; - -exports[`Design management large image component renders loading state 1`] = ` -<div - class="m-auto js-design-image" - isloading="true" -> - <!----> - - <img - alt="" - class="mh-100 img-fluid" - src="" - /> -</div> -`; - -exports[`Design management large image component renders media broken icon on error 1`] = ` -<gl-icon-stub - class="text-secondary-100" - name="media-broken" - size="48" -/> -`; - -exports[`Design management large image component sets correct classes and styles if imageStyle is set 1`] = ` -<div - class="m-auto js-design-image" -> - <!----> - - <img - alt="test" - class="mh-100" - src="test.jpg" - style="width: 100px; height: 100px;" - /> -</div> -`; - -exports[`Design management large image component zoom sets image style when zoomed 1`] = ` -<div - class="m-auto js-design-image" -> - <!----> - - <img - alt="test" - class="mh-100" - src="test.jpg" - style="width: 200px; height: 200px;" - /> -</div> -`; diff --git a/spec/frontend/design_management_legacy/components/delete_button_spec.js b/spec/frontend/design_management_legacy/components/delete_button_spec.js deleted file mode 100644 index 73b4908d06a..00000000000 --- a/spec/frontend/design_management_legacy/components/delete_button_spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui'; -import BatchDeleteButton from '~/design_management_legacy/components/delete_button.vue'; - -describe('Batch delete button component', () => { - let wrapper; - - const findButton = () => wrapper.find(GlDeprecatedButton); - const findModal = () => wrapper.find(GlModal); - - function createComponent(isDeleting = false) { - wrapper = shallowMount(BatchDeleteButton, { - propsData: { - isDeleting, - }, - directives: { - GlModalDirective, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders non-disabled button by default', () => { - createComponent(); - - expect(findButton().exists()).toBe(true); - expect(findButton().attributes('disabled')).toBeFalsy(); - }); - - it('renders disabled button when design is deleting', () => { - createComponent(true); - expect(findButton().attributes('disabled')).toBeTruthy(); - }); - - it('emits `deleteSelectedDesigns` event on modal ok click', () => { - createComponent(); - findButton().vm.$emit('click'); - return wrapper.vm - .$nextTick() - .then(() => { - findModal().vm.$emit('ok'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted().deleteSelectedDesigns).toBeTruthy(); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_note_pin_spec.js b/spec/frontend/design_management_legacy/components/design_note_pin_spec.js deleted file mode 100644 index 3077928cf86..00000000000 --- a/spec/frontend/design_management_legacy/components/design_note_pin_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import DesignNotePin from '~/design_management_legacy/components/design_note_pin.vue'; - -describe('Design note pin component', () => { - let wrapper; - - function createComponent(propsData = {}) { - wrapper = shallowMount(DesignNotePin, { - propsData: { - position: { - left: '10px', - top: '10px', - }, - ...propsData, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('should match the snapshot of note without index', () => { - createComponent(); - expect(wrapper.element).toMatchSnapshot(); - }); - - it('should match the snapshot of note with index', () => { - createComponent({ label: 1 }); - expect(wrapper.element).toMatchSnapshot(); - }); - - it('should match the snapshot of note when repositioning', () => { - createComponent({ repositioning: true }); - expect(wrapper.element).toMatchSnapshot(); - }); - - describe('pinStyle', () => { - it('sets cursor to `move` when repositioning = true', () => { - createComponent({ repositioning: true }); - expect(wrapper.vm.pinStyle.cursor).toBe('move'); - }); - - it('does not set cursor when repositioning = false', () => { - createComponent(); - expect(wrapper.vm.pinStyle.cursor).toBe(undefined); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap deleted file mode 100644 index b55bacb6fc5..00000000000 --- a/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design note component should match the snapshot 1`] = ` -<timeline-entry-item-stub - class="design-note note-form" - id="note_123" -> - <user-avatar-link-stub - imgalt="" - imgcssclasses="" - imgsize="40" - imgsrc="" - linkhref="" - tooltipplacement="top" - tooltiptext="" - username="" - /> - - <div - class="d-flex justify-content-between" - > - <div> - <a - class="js-user-link" - data-user-id="author-id" - > - <span - class="note-header-author-name bold" - > - - </span> - - <!----> - - <span - class="note-headline-light" - > - @ - </span> - </a> - - <span - class="note-headline-light note-headline-meta" - > - <span - class="system-note-message" - /> - - <!----> - </span> - </div> - - <div - class="gl-display-flex" - > - - <!----> - </div> - </div> - - <div - class="note-text js-note-text md" - data-qa-selector="note_content" - /> - -</timeline-entry-item-stub> -`; diff --git a/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap deleted file mode 100644 index e01c79e3520..00000000000 --- a/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = ` -"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\"> - <!----> - Comment -</button>" -`; - -exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = ` -"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\"> - <!----> - Save comment -</button>" -`; diff --git a/spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js deleted file mode 100644 index 2330cb31f4d..00000000000 --- a/spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js +++ /dev/null @@ -1,318 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlLoadingIcon } from '@gitlab/ui'; -import notes from '../../mock_data/notes'; -import DesignDiscussion from '~/design_management_legacy/components/design_notes/design_discussion.vue'; -import DesignNote from '~/design_management_legacy/components/design_notes/design_note.vue'; -import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue'; -import createNoteMutation from '~/design_management_legacy/graphql/mutations/create_note.mutation.graphql'; -import toggleResolveDiscussionMutation from '~/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql'; -import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; -import ToggleRepliesWidget from '~/design_management_legacy/components/design_notes/toggle_replies_widget.vue'; - -const discussion = { - id: '0', - resolved: false, - resolvable: true, - notes, -}; - -describe('Design discussions component', () => { - let wrapper; - - const findDesignNotes = () => wrapper.findAll(DesignNote); - const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder); - const findReplyForm = () => wrapper.find(DesignReplyForm); - const findRepliesWidget = () => wrapper.find(ToggleRepliesWidget); - const findResolveButton = () => wrapper.find('[data-testid="resolve-button"]'); - const findResolveIcon = () => wrapper.find('[data-testid="resolve-icon"]'); - const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]'); - const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon); - const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]'); - - const mutationVariables = { - mutation: createNoteMutation, - update: expect.anything(), - variables: { - input: { - noteableId: 'noteable-id', - body: 'test', - discussionId: '0', - }, - }, - }; - const mutate = jest.fn(() => Promise.resolve()); - const $apollo = { - mutate, - }; - - function createComponent(props = {}, data = {}) { - wrapper = mount(DesignDiscussion, { - propsData: { - resolvedDiscussionsExpanded: true, - discussion, - noteableId: 'noteable-id', - designId: 'design-id', - discussionIndex: 1, - discussionWithOpenForm: '', - ...props, - }, - data() { - return { - ...data, - }; - }, - mocks: { - $apollo, - $route: { - hash: '#note_1', - }, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - describe('when discussion is not resolvable', () => { - beforeEach(() => { - createComponent({ - discussion: { - ...discussion, - resolvable: false, - }, - }); - }); - - it('does not render an icon to resolve a thread', () => { - expect(findResolveIcon().exists()).toBe(false); - }); - - it('does not render a checkbox in reply form', () => { - findReplyPlaceholder().vm.$emit('onClick'); - - return wrapper.vm.$nextTick().then(() => { - expect(findResolveCheckbox().exists()).toBe(false); - }); - }); - }); - - describe('when discussion is unresolved', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders correct amount of discussion notes', () => { - expect(findDesignNotes()).toHaveLength(2); - expect(findDesignNotes().wrappers.every(w => w.isVisible())).toBe(true); - }); - - it('renders reply placeholder', () => { - expect(findReplyPlaceholder().isVisible()).toBe(true); - }); - - it('does not render toggle replies widget', () => { - expect(findRepliesWidget().exists()).toBe(false); - }); - - it('renders a correct icon to resolve a thread', () => { - expect(findResolveIcon().props('name')).toBe('check-circle'); - }); - - it('renders a checkbox with Resolve thread text in reply form', () => { - findReplyPlaceholder().vm.$emit('onClick'); - wrapper.setProps({ discussionWithOpenForm: discussion.id }); - - return wrapper.vm.$nextTick().then(() => { - expect(findResolveCheckbox().text()).toBe('Resolve thread'); - }); - }); - - it('does not render resolved message', () => { - expect(findResolvedMessage().exists()).toBe(false); - }); - }); - - describe('when discussion is resolved', () => { - beforeEach(() => { - createComponent({ - discussion: { - ...discussion, - resolved: true, - resolvedBy: notes[0].author, - resolvedAt: '2020-05-08T07:10:45Z', - }, - }); - }); - - it('shows only the first note', () => { - expect( - findDesignNotes() - .at(0) - .isVisible(), - ).toBe(true); - expect( - findDesignNotes() - .at(1) - .isVisible(), - ).toBe(false); - }); - - it('renders resolved message', () => { - expect(findResolvedMessage().exists()).toBe(true); - }); - - it('does not show renders reply placeholder', () => { - expect(findReplyPlaceholder().isVisible()).toBe(false); - }); - - it('renders toggle replies widget with correct props', () => { - expect(findRepliesWidget().exists()).toBe(true); - expect(findRepliesWidget().props()).toEqual({ - collapsed: true, - replies: notes.slice(1), - }); - }); - - it('renders a correct icon to resolve a thread', () => { - expect(findResolveIcon().props('name')).toBe('check-circle-filled'); - }); - - describe('when replies are expanded', () => { - beforeEach(() => { - findRepliesWidget().vm.$emit('toggle'); - return wrapper.vm.$nextTick(); - }); - - it('renders replies widget with collapsed prop equal to false', () => { - expect(findRepliesWidget().props('collapsed')).toBe(false); - }); - - it('renders the second note', () => { - expect( - findDesignNotes() - .at(1) - .isVisible(), - ).toBe(true); - }); - - it('renders a reply placeholder', () => { - expect(findReplyPlaceholder().isVisible()).toBe(true); - }); - - it('renders a checkbox with Unresolve thread text in reply form', () => { - findReplyPlaceholder().vm.$emit('onClick'); - wrapper.setProps({ discussionWithOpenForm: discussion.id }); - - return wrapper.vm.$nextTick().then(() => { - expect(findResolveCheckbox().text()).toBe('Unresolve thread'); - }); - }); - }); - }); - - it('hides reply placeholder and opens form on placeholder click', () => { - createComponent(); - findReplyPlaceholder().vm.$emit('onClick'); - wrapper.setProps({ discussionWithOpenForm: discussion.id }); - - return wrapper.vm.$nextTick().then(() => { - expect(findReplyPlaceholder().exists()).toBe(false); - expect(findReplyForm().exists()).toBe(true); - }); - }); - - it('calls mutation on submitting form and closes the form', () => { - createComponent( - { discussionWithOpenForm: discussion.id }, - { discussionComment: 'test', isFormRendered: true }, - ); - - findReplyForm().vm.$emit('submitForm'); - expect(mutate).toHaveBeenCalledWith(mutationVariables); - - return mutate() - .then(() => { - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findReplyForm().exists()).toBe(false); - }); - }); - - it('clears the discussion comment on closing comment form', () => { - createComponent( - { discussionWithOpenForm: discussion.id }, - { discussionComment: 'test', isFormRendered: true }, - ); - - return wrapper.vm - .$nextTick() - .then(() => { - findReplyForm().vm.$emit('cancelForm'); - - expect(wrapper.vm.discussionComment).toBe(''); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findReplyForm().exists()).toBe(false); - }); - }); - - it('applies correct class to design notes when discussion is highlighted', () => { - createComponent( - {}, - { - activeDiscussion: { - id: notes[0].id, - source: 'pin', - }, - }, - ); - - expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe( - true, - ); - }); - - it('calls toggleResolveDiscussion mutation on resolve thread button click', () => { - createComponent(); - findResolveButton().trigger('click'); - expect(mutate).toHaveBeenCalledWith({ - mutation: toggleResolveDiscussionMutation, - variables: { - id: discussion.id, - resolve: true, - }, - }); - return wrapper.vm.$nextTick(() => { - expect(findResolveLoadingIcon().exists()).toBe(true); - }); - }); - - it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => { - createComponent( - { discussionWithOpenForm: discussion.id }, - { discussionComment: 'test', isFormRendered: true }, - ); - findResolveButton().trigger('click'); - findReplyForm().vm.$emit('submitForm'); - - return mutate().then(() => { - expect(mutate).toHaveBeenCalledWith({ - mutation: toggleResolveDiscussionMutation, - variables: { - id: discussion.id, - resolve: true, - }, - }); - }); - }); - - it('emits openForm event on opening the form', () => { - createComponent(); - findReplyPlaceholder().vm.$emit('onClick'); - - expect(wrapper.emitted('openForm')).toBeTruthy(); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js deleted file mode 100644 index aa187cd1388..00000000000 --- a/spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js +++ /dev/null @@ -1,170 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { ApolloMutation } from 'vue-apollo'; -import DesignNote from '~/design_management_legacy/components/design_notes/design_note.vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue'; - -const scrollIntoViewMock = jest.fn(); -const note = { - id: 'gid://gitlab/DiffNote/123', - author: { - id: 'author-id', - }, - body: 'test', - userPermissions: { - adminNote: false, - }, -}; -HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; - -const $route = { - hash: '#note_123', -}; - -const mutate = jest.fn().mockResolvedValue({ data: { updateNote: {} } }); - -describe('Design note component', () => { - let wrapper; - - const findUserAvatar = () => wrapper.find(UserAvatarLink); - const findUserLink = () => wrapper.find('.js-user-link'); - const findReplyForm = () => wrapper.find(DesignReplyForm); - const findEditButton = () => wrapper.find('.js-note-edit'); - const findNoteContent = () => wrapper.find('.js-note-text'); - - function createComponent(props = {}, data = { isEditing: false }) { - wrapper = shallowMount(DesignNote, { - propsData: { - note: {}, - ...props, - }, - data() { - return { - ...data, - }; - }, - mocks: { - $route, - $apollo: { - mutate, - }, - }, - stubs: { - ApolloMutation, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('should match the snapshot', () => { - createComponent({ - note, - }); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('should render an author', () => { - createComponent({ - note, - }); - - expect(findUserAvatar().exists()).toBe(true); - expect(findUserLink().exists()).toBe(true); - }); - - it('should render a time ago tooltip if note has createdAt property', () => { - createComponent({ - note: { - ...note, - createdAt: '2019-07-26T15:02:20Z', - }, - }); - - expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true); - }); - - it('should trigger a scrollIntoView method', () => { - createComponent({ - note, - }); - - expect(scrollIntoViewMock).toHaveBeenCalled(); - }); - - it('should not render edit icon when user does not have a permission', () => { - createComponent({ - note, - }); - - expect(findEditButton().exists()).toBe(false); - }); - - describe('when user has a permission to edit note', () => { - it('should open an edit form on edit button click', () => { - createComponent({ - note: { - ...note, - userPermissions: { - adminNote: true, - }, - }, - }); - - findEditButton().trigger('click'); - - return wrapper.vm.$nextTick().then(() => { - expect(findReplyForm().exists()).toBe(true); - expect(findNoteContent().exists()).toBe(false); - }); - }); - - describe('when edit form is rendered', () => { - beforeEach(() => { - createComponent( - { - note: { - ...note, - userPermissions: { - adminNote: true, - }, - }, - }, - { isEditing: true }, - ); - }); - - it('should not render note content and should render reply form', () => { - expect(findNoteContent().exists()).toBe(false); - expect(findReplyForm().exists()).toBe(true); - }); - - it('hides the form on hideForm event', () => { - findReplyForm().vm.$emit('cancelForm'); - - return wrapper.vm.$nextTick().then(() => { - expect(findReplyForm().exists()).toBe(false); - expect(findNoteContent().exists()).toBe(true); - }); - }); - - it('calls a mutation on submitForm event and hides a form', () => { - findReplyForm().vm.$emit('submitForm'); - expect(mutate).toHaveBeenCalled(); - - return mutate() - .then(() => { - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findReplyForm().exists()).toBe(false); - expect(findNoteContent().exists()).toBe(true); - }); - }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js deleted file mode 100644 index 088a71b64af..00000000000 --- a/spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js +++ /dev/null @@ -1,184 +0,0 @@ -import { mount } from '@vue/test-utils'; -import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue'; - -const showModal = jest.fn(); - -const GlModal = { - template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>', - methods: { - show: showModal, - }, -}; - -describe('Design reply form component', () => { - let wrapper; - - const findTextarea = () => wrapper.find('textarea'); - const findSubmitButton = () => wrapper.find({ ref: 'submitButton' }); - const findCancelButton = () => wrapper.find({ ref: 'cancelButton' }); - const findModal = () => wrapper.find({ ref: 'cancelCommentModal' }); - - function createComponent(props = {}, mountOptions = {}) { - wrapper = mount(DesignReplyForm, { - propsData: { - value: '', - isSaving: false, - ...props, - }, - stubs: { GlModal }, - ...mountOptions, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('textarea has focus after component mount', () => { - // We need to attach to document, so that `document.activeElement` is properly set in jsdom - createComponent({}, { attachToDocument: true }); - - expect(findTextarea().element).toEqual(document.activeElement); - }); - - it('renders button text as "Comment" when creating a comment', () => { - createComponent(); - - expect(findSubmitButton().html()).toMatchSnapshot(); - }); - - it('renders button text as "Save comment" when creating a comment', () => { - createComponent({ isNewComment: false }); - - expect(findSubmitButton().html()).toMatchSnapshot(); - }); - - describe('when form has no text', () => { - beforeEach(() => { - createComponent({ - value: '', - }); - }); - - it('submit button is disabled', () => { - expect(findSubmitButton().attributes().disabled).toBeTruthy(); - }); - - it('does not emit submitForm event on textarea ctrl+enter keydown', () => { - findTextarea().trigger('keydown.enter', { - ctrlKey: true, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeFalsy(); - }); - }); - - it('does not emit submitForm event on textarea meta+enter keydown', () => { - findTextarea().trigger('keydown.enter', { - metaKey: true, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeFalsy(); - }); - }); - - it('emits cancelForm event on pressing escape button on textarea', () => { - findTextarea().trigger('keyup.esc'); - - expect(wrapper.emitted('cancelForm')).toBeTruthy(); - }); - - it('emits cancelForm event on clicking Cancel button', () => { - findCancelButton().vm.$emit('click'); - - expect(wrapper.emitted('cancelForm')).toHaveLength(1); - }); - }); - - describe('when form has text', () => { - beforeEach(() => { - createComponent({ - value: 'test', - }); - }); - - it('submit button is enabled', () => { - expect(findSubmitButton().attributes().disabled).toBeFalsy(); - }); - - it('emits submitForm event on Comment button click', () => { - findSubmitButton().vm.$emit('click'); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeTruthy(); - }); - }); - - it('emits submitForm event on textarea ctrl+enter keydown', () => { - findTextarea().trigger('keydown.enter', { - ctrlKey: true, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeTruthy(); - }); - }); - - it('emits submitForm event on textarea meta+enter keydown', () => { - findTextarea().trigger('keydown.enter', { - metaKey: true, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submitForm')).toBeTruthy(); - }); - }); - - it('emits input event on changing textarea content', () => { - findTextarea().setValue('test2'); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('input')).toBeTruthy(); - }); - }); - - it('emits cancelForm event on Escape key if text was not changed', () => { - findTextarea().trigger('keyup.esc'); - - expect(wrapper.emitted('cancelForm')).toBeTruthy(); - }); - - it('opens confirmation modal on Escape key when text has changed', () => { - wrapper.setProps({ value: 'test2' }); - - return wrapper.vm.$nextTick().then(() => { - findTextarea().trigger('keyup.esc'); - expect(showModal).toHaveBeenCalled(); - }); - }); - - it('emits cancelForm event on Cancel button click if text was not changed', () => { - findCancelButton().trigger('click'); - - expect(wrapper.emitted('cancelForm')).toBeTruthy(); - }); - - it('opens confirmation modal on Cancel button click when text has changed', () => { - wrapper.setProps({ value: 'test2' }); - - return wrapper.vm.$nextTick().then(() => { - findCancelButton().trigger('click'); - expect(showModal).toHaveBeenCalled(); - }); - }); - - it('emits cancelForm event on modal Ok button click', () => { - findTextarea().trigger('keyup.esc'); - findModal().vm.$emit('ok'); - - expect(wrapper.emitted('cancelForm')).toBeTruthy(); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js deleted file mode 100644 index acc7cbbca52..00000000000 --- a/spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlIcon, GlButton, GlLink } from '@gitlab/ui'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import ToggleRepliesWidget from '~/design_management_legacy/components/design_notes/toggle_replies_widget.vue'; -import notes from '../../mock_data/notes'; - -describe('Toggle replies widget component', () => { - let wrapper; - - const findToggleWrapper = () => wrapper.find('[data-testid="toggle-comments-wrapper"]'); - const findIcon = () => wrapper.find(GlIcon); - const findButton = () => wrapper.find(GlButton); - const findAuthorLink = () => wrapper.find(GlLink); - const findTimeAgo = () => wrapper.find(TimeAgoTooltip); - - function createComponent(props = {}) { - wrapper = shallowMount(ToggleRepliesWidget, { - propsData: { - collapsed: true, - replies: notes, - ...props, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - describe('when replies are collapsed', () => { - beforeEach(() => { - createComponent(); - }); - - it('should not have expanded class', () => { - expect(findToggleWrapper().classes()).not.toContain('expanded'); - }); - - it('should render chevron-right icon', () => { - expect(findIcon().props('name')).toBe('chevron-right'); - }); - - it('should have replies length on button', () => { - expect(findButton().text()).toBe('2 replies'); - }); - - it('should render a link to the last reply author', () => { - expect(findAuthorLink().exists()).toBe(true); - expect(findAuthorLink().text()).toBe(notes[1].author.name); - expect(findAuthorLink().attributes('href')).toBe(notes[1].author.webUrl); - }); - - it('should render correct time ago tooltip', () => { - expect(findTimeAgo().exists()).toBe(true); - expect(findTimeAgo().props('time')).toBe(notes[1].createdAt); - }); - }); - - describe('when replies are expanded', () => { - beforeEach(() => { - createComponent({ collapsed: false }); - }); - - it('should have expanded class', () => { - expect(findToggleWrapper().classes()).toContain('expanded'); - }); - - it('should render chevron-down icon', () => { - expect(findIcon().props('name')).toBe('chevron-down'); - }); - - it('should have Collapse replies text on button', () => { - expect(findButton().text()).toBe('Collapse replies'); - }); - - it('should not have a link to the last reply author', () => { - expect(findAuthorLink().exists()).toBe(false); - }); - - it('should not render time ago tooltip', () => { - expect(findTimeAgo().exists()).toBe(false); - }); - }); - - it('should emit toggle event on icon click', () => { - createComponent(); - findIcon().vm.$emit('click', new MouseEvent('click')); - - expect(wrapper.emitted('toggle')).toHaveLength(1); - }); - - it('should emit toggle event on button click', () => { - createComponent(); - findButton().vm.$emit('click', new MouseEvent('click')); - - expect(wrapper.emitted('toggle')).toHaveLength(1); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_overlay_spec.js b/spec/frontend/design_management_legacy/components/design_overlay_spec.js deleted file mode 100644 index c014f3479f4..00000000000 --- a/spec/frontend/design_management_legacy/components/design_overlay_spec.js +++ /dev/null @@ -1,410 +0,0 @@ -import { mount } from '@vue/test-utils'; -import DesignOverlay from '~/design_management_legacy/components/design_overlay.vue'; -import updateActiveDiscussion from '~/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql'; -import notes from '../mock_data/notes'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management_legacy/constants'; - -const mutate = jest.fn(() => Promise.resolve()); - -describe('Design overlay component', () => { - let wrapper; - - const mockDimensions = { width: 100, height: 100 }; - - const findOverlay = () => wrapper.find('.image-diff-overlay'); - const findAllNotes = () => wrapper.findAll('.js-image-badge'); - const findCommentBadge = () => wrapper.find('.comment-indicator'); - const findFirstBadge = () => findAllNotes().at(0); - const findSecondBadge = () => findAllNotes().at(1); - - const clickAndDragBadge = (elem, fromPoint, toPoint) => { - elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y }); - return wrapper.vm.$nextTick().then(() => { - elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y }); - return wrapper.vm.$nextTick(); - }); - }; - - function createComponent(props = {}, data = {}) { - wrapper = mount(DesignOverlay, { - propsData: { - dimensions: mockDimensions, - position: { - top: '0', - left: '0', - }, - resolvedDiscussionsExpanded: false, - ...props, - }, - data() { - return { - activeDiscussion: { - id: null, - source: null, - }, - ...data, - }; - }, - mocks: { - $apollo: { - mutate, - }, - }, - }); - } - - it('should have correct inline style', () => { - createComponent(); - - expect(wrapper.find('.image-diff-overlay').attributes().style).toBe( - 'width: 100px; height: 100px; top: 0px; left: 0px;', - ); - }); - - it('should emit `openCommentForm` when clicking on overlay', () => { - createComponent(); - const newCoordinates = { - x: 10, - y: 10, - }; - - wrapper - .find('.image-diff-overlay-add-comment') - .trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('openCommentForm')).toEqual([ - [{ x: newCoordinates.x, y: newCoordinates.y }], - ]); - }); - }); - - describe('with notes', () => { - it('should render only the first note', () => { - createComponent({ - notes, - }); - expect(findAllNotes()).toHaveLength(1); - }); - - describe('with resolved discussions toggle expanded', () => { - beforeEach(() => { - createComponent({ - notes, - resolvedDiscussionsExpanded: true, - }); - }); - - it('should render all notes', () => { - expect(findAllNotes()).toHaveLength(notes.length); - }); - - it('should have set the correct position for each note badge', () => { - expect(findFirstBadge().attributes().style).toBe('left: 10px; top: 15px;'); - expect(findSecondBadge().attributes().style).toBe('left: 50px; top: 50px;'); - }); - - it('should apply resolved class to the resolved note pin', () => { - expect(findSecondBadge().classes()).toContain('resolved'); - }); - - it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => { - wrapper.setData({ - activeDiscussion: { - id: notes[0].id, - source: 'discussion', - }, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(findSecondBadge().classes()).toContain('inactive'); - }); - }); - }); - - it('should recalculate badges positions on window resize', () => { - createComponent({ - notes, - dimensions: { - width: 400, - height: 400, - }, - }); - - expect(findFirstBadge().attributes().style).toBe('left: 40px; top: 60px;'); - - wrapper.setProps({ - dimensions: { - width: 200, - height: 200, - }, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;'); - }); - }); - - it('should call an update active discussion mutation when clicking a note without moving it', () => { - const note = notes[0]; - const { position } = note; - const mutationVariables = { - mutation: updateActiveDiscussion, - variables: { - id: note.id, - source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin, - }, - }; - - findFirstBadge().trigger('mousedown', { clientX: position.x, clientY: position.y }); - - return wrapper.vm.$nextTick().then(() => { - findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y }); - expect(mutate).toHaveBeenCalledWith(mutationVariables); - }); - }); - }); - - describe('when moving notes', () => { - it('should update badge style when note is being moved', () => { - createComponent({ - notes, - }); - - const { position } = notes[0]; - - return clickAndDragBadge( - findFirstBadge(), - { x: position.x, y: position.y }, - { x: 20, y: 20 }, - ).then(() => { - expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px; cursor: move;'); - }); - }); - - it('should emit `moveNote` event when note-moving action ends', () => { - createComponent({ notes }); - const note = notes[0]; - const { position } = note; - const newCoordinates = { x: 20, y: 20 }; - - wrapper.setData({ - movingNoteNewPosition: { - ...position, - ...newCoordinates, - }, - movingNoteStartPosition: { - noteId: notes[0].id, - discussionId: notes[0].discussion.id, - ...position, - }, - }); - - const badge = findFirstBadge(); - return clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates) - .then(() => { - badge.trigger('mouseup'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted('moveNote')).toEqual([ - [ - { - noteId: notes[0].id, - discussionId: notes[0].discussion.id, - coordinates: newCoordinates, - }, - ], - ]); - }); - }); - - describe('without [adminNote] permission', () => { - const mockNoteNotAuthorised = { - ...notes[0], - userPermissions: { - adminNote: false, - }, - }; - - const mockNoteCoordinates = { - x: mockNoteNotAuthorised.position.x, - y: mockNoteNotAuthorised.position.y, - }; - - it('should be unable to move a note', () => { - createComponent({ - dimensions: mockDimensions, - notes: [mockNoteNotAuthorised], - }); - - const badge = findAllNotes().at(0); - return clickAndDragBadge(badge, { ...mockNoteCoordinates }, { x: 20, y: 20 }).then(() => { - // note position should not change after a click-and-drag attempt - expect(findFirstBadge().attributes().style).toContain( - `left: ${mockNoteCoordinates.x}px; top: ${mockNoteCoordinates.y}px;`, - ); - }); - }); - }); - }); - - describe('with a new form', () => { - it('should render a new comment badge', () => { - createComponent({ - currentCommentForm: { - ...notes[0].position, - }, - }); - - expect(findCommentBadge().exists()).toBe(true); - expect(findCommentBadge().attributes().style).toBe('left: 10px; top: 15px;'); - }); - - describe('when moving the comment badge', () => { - it('should update badge style to reflect new position', () => { - const { position } = notes[0]; - - createComponent({ - currentCommentForm: { - ...position, - }, - }); - - return clickAndDragBadge( - findCommentBadge(), - { x: position.x, y: position.y }, - { x: 20, y: 20 }, - ).then(() => { - expect(findCommentBadge().attributes().style).toBe( - 'left: 20px; top: 20px; cursor: move;', - ); - }); - }); - - it('should update badge style when note-moving action ends', () => { - const { position } = notes[0]; - createComponent({ - currentCommentForm: { - ...position, - }, - }); - - const commentBadge = findCommentBadge(); - const toPoint = { x: 20, y: 20 }; - - return clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint) - .then(() => { - commentBadge.trigger('mouseup'); - // simulates the currentCommentForm being updated in index.vue component, and - // propagated back down to this prop - wrapper.setProps({ - currentCommentForm: { height: position.height, width: position.width, ...toPoint }, - }); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;'); - }); - }); - - it.each` - element | getElementFunc | event - ${'overlay'} | ${findOverlay} | ${'mouseleave'} - ${'comment badge'} | ${findCommentBadge} | ${'mouseup'} - `( - 'should emit `openCommentForm` event when $event fired on $element element', - ({ getElementFunc, event }) => { - createComponent({ - notes, - currentCommentForm: { - ...notes[0].position, - }, - }); - - const newCoordinates = { x: 20, y: 20 }; - wrapper.setData({ - movingNoteStartPosition: { - ...notes[0].position, - }, - movingNoteNewPosition: { - ...notes[0].position, - ...newCoordinates, - }, - }); - - getElementFunc().trigger(event); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]); - }); - }, - ); - }); - }); - - describe('getMovingNotePositionDelta', () => { - it('should calculate delta correctly from state', () => { - createComponent(); - - wrapper.setData({ - movingNoteStartPosition: { - clientX: 10, - clientY: 20, - }, - }); - - const mockMouseEvent = { - clientX: 30, - clientY: 10, - }; - - expect(wrapper.vm.getMovingNotePositionDelta(mockMouseEvent)).toEqual({ - deltaX: 20, - deltaY: -10, - }); - }); - }); - - describe('isPositionInOverlay', () => { - createComponent({ dimensions: mockDimensions }); - - it.each` - test | coordinates | expectedResult - ${'within overlay bounds'} | ${{ x: 50, y: 50 }} | ${true} - ${'outside overlay bounds'} | ${{ x: 101, y: 101 }} | ${false} - `('returns [$expectedResult] when position is $test', ({ coordinates, expectedResult }) => { - const position = { ...mockDimensions, ...coordinates }; - - expect(wrapper.vm.isPositionInOverlay(position)).toBe(expectedResult); - }); - }); - - describe('getNoteRelativePosition', () => { - it('calculates position correctly', () => { - createComponent({ dimensions: mockDimensions }); - const position = { x: 50, y: 50, width: 200, height: 200 }; - - expect(wrapper.vm.getNoteRelativePosition(position)).toEqual({ left: 25, top: 25 }); - }); - }); - - describe('canMoveNote', () => { - it.each` - adminNotePermission | canMoveNoteResult - ${true} | ${true} - ${false} | ${false} - ${undefined} | ${false} - `( - 'returns [$canMoveNoteResult] when [adminNote permission] is [$adminNotePermission]', - ({ adminNotePermission, canMoveNoteResult }) => { - createComponent(); - - const note = { - userPermissions: { - adminNote: adminNotePermission, - }, - }; - expect(wrapper.vm.canMoveNote(note)).toBe(canMoveNoteResult); - }, - ); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_presentation_spec.js b/spec/frontend/design_management_legacy/components/design_presentation_spec.js deleted file mode 100644 index ceff86b0549..00000000000 --- a/spec/frontend/design_management_legacy/components/design_presentation_spec.js +++ /dev/null @@ -1,553 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import DesignPresentation from '~/design_management_legacy/components/design_presentation.vue'; -import DesignOverlay from '~/design_management_legacy/components/design_overlay.vue'; - -const mockOverlayData = { - overlayDimensions: { - width: 100, - height: 100, - }, - overlayPosition: { - top: '0', - left: '0', - }, -}; - -describe('Design management design presentation component', () => { - let wrapper; - - function createComponent( - { - image, - imageName, - discussions = [], - isAnnotating = false, - resolvedDiscussionsExpanded = false, - } = {}, - data = {}, - stubs = {}, - ) { - wrapper = shallowMount(DesignPresentation, { - propsData: { - image, - imageName, - discussions, - isAnnotating, - resolvedDiscussionsExpanded, - }, - stubs, - }); - - wrapper.setData(data); - wrapper.element.scrollTo = jest.fn(); - } - - const findOverlayCommentButton = () => wrapper.find('.image-diff-overlay-add-comment'); - - /** - * Spy on $refs and mock given values - * @param {Object} viewportDimensions {width, height} - * @param {Object} childDimensions {width, height} - * @param {Float} scrollTopPerc 0 < x < 1 - * @param {Float} scrollLeftPerc 0 < x < 1 - */ - function mockRefDimensions( - ref, - viewportDimensions, - childDimensions, - scrollTopPerc, - scrollLeftPerc, - ) { - jest.spyOn(ref, 'scrollWidth', 'get').mockReturnValue(childDimensions.width); - jest.spyOn(ref, 'scrollHeight', 'get').mockReturnValue(childDimensions.height); - jest.spyOn(ref, 'offsetWidth', 'get').mockReturnValue(viewportDimensions.width); - jest.spyOn(ref, 'offsetHeight', 'get').mockReturnValue(viewportDimensions.height); - jest - .spyOn(ref, 'scrollLeft', 'get') - .mockReturnValue((childDimensions.width - viewportDimensions.width) * scrollLeftPerc); - jest - .spyOn(ref, 'scrollTop', 'get') - .mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc); - } - - function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) { - const event = useTouchEvents - ? { - mousedown: 'touchstart', - mousemove: 'touchmove', - mouseup: 'touchend', - } - : { - mousedown: 'mousedown', - mousemove: 'mousemove', - mouseup: 'mouseup', - }; - - const addCommentOverlay = findOverlayCommentButton(); - - // triggering mouse events on this element best simulates - // reality, as it is the lowest-level node that needs to - // respond to mouse events - addCommentOverlay.trigger(event.mousedown, { - clientX: startCoords.clientX, - clientY: startCoords.clientY, - }); - return wrapper.vm - .$nextTick() - .then(() => { - addCommentOverlay.trigger(event.mousemove, { - clientX: endCoords.clientX, - clientY: endCoords.clientY, - }); - - return wrapper.vm.$nextTick(); - }) - .then(() => { - if (mouseup) { - addCommentOverlay.trigger(event.mouseup); - return wrapper.vm.$nextTick(); - } - - return undefined; - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders image and overlay when image provided', () => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - mockOverlayData, - ); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders empty state when no image provided', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('openCommentForm event emits correct data', () => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - mockOverlayData, - ); - - wrapper.vm.openCommentForm({ x: 1, y: 1 }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('openCommentForm')).toEqual([ - [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }], - ]); - }); - }); - - describe('currentCommentForm', () => { - it('is null when isAnnotating is false', () => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - mockOverlayData, - ); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.currentCommentForm).toBeNull(); - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('is null when isAnnotating is true but annotation position is falsey', () => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - isAnnotating: true, - }, - mockOverlayData, - ); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.currentCommentForm).toBeNull(); - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('is equal to current annotation position when isAnnotating is true', () => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - isAnnotating: true, - }, - { - ...mockOverlayData, - currentAnnotationPosition: { - x: 1, - y: 1, - width: 100, - height: 100, - }, - }, - ); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.currentCommentForm).toEqual({ - x: 1, - y: 1, - width: 100, - height: 100, - }); - expect(wrapper.element).toMatchSnapshot(); - }); - }); - }); - - describe('setOverlayPosition', () => { - beforeEach(() => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - mockOverlayData, - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('sets overlay position correctly when overlay is smaller than viewport', () => { - jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200); - jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200); - - wrapper.vm.setOverlayPosition(); - expect(wrapper.vm.overlayPosition).toEqual({ - left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`, - top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`, - }); - }); - - it('sets overlay position correctly when overlay width is larger than viewports', () => { - jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(50); - jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200); - - wrapper.vm.setOverlayPosition(); - expect(wrapper.vm.overlayPosition).toEqual({ - left: '0', - top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`, - }); - }); - - it('sets overlay position correctly when overlay height is larger than viewports', () => { - jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200); - jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(50); - - wrapper.vm.setOverlayPosition(); - expect(wrapper.vm.overlayPosition).toEqual({ - left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`, - top: '0', - }); - }); - }); - - describe('getViewportCenter', () => { - beforeEach(() => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - mockOverlayData, - ); - }); - - it('calculate center correctly with no scroll', () => { - mockRefDimensions( - wrapper.vm.$refs.presentationViewport, - { width: 10, height: 10 }, - { width: 20, height: 20 }, - 0, - 0, - ); - - expect(wrapper.vm.getViewportCenter()).toEqual({ - x: 5, - y: 5, - }); - }); - - it('calculate center correctly with some scroll', () => { - mockRefDimensions( - wrapper.vm.$refs.presentationViewport, - { width: 10, height: 10 }, - { width: 20, height: 20 }, - 0.5, - 0.5, - ); - - expect(wrapper.vm.getViewportCenter()).toEqual({ - x: 10, - y: 10, - }); - }); - - it('Returns default case if no overflow (scrollWidth==offsetWidth, etc.)', () => { - mockRefDimensions( - wrapper.vm.$refs.presentationViewport, - { width: 20, height: 20 }, - { width: 20, height: 20 }, - 0.5, - 0.5, - ); - - expect(wrapper.vm.getViewportCenter()).toEqual({ - x: 10, - y: 10, - }); - }); - }); - - describe('scaleZoomFocalPoint', () => { - it('scales focal point correctly when zooming in', () => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - { - ...mockOverlayData, - zoomFocalPoint: { - x: 5, - y: 5, - width: 50, - height: 50, - }, - }, - ); - - wrapper.vm.scaleZoomFocalPoint(); - expect(wrapper.vm.zoomFocalPoint).toEqual({ - x: 10, - y: 10, - width: 100, - height: 100, - }); - }); - - it('scales focal point correctly when zooming out', () => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - { - ...mockOverlayData, - zoomFocalPoint: { - x: 10, - y: 10, - width: 200, - height: 200, - }, - }, - ); - - wrapper.vm.scaleZoomFocalPoint(); - expect(wrapper.vm.zoomFocalPoint).toEqual({ - x: 5, - y: 5, - width: 100, - height: 100, - }); - }); - }); - - describe('onImageResize', () => { - it('sets zoom focal point on initial load', () => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - mockOverlayData, - ); - - wrapper.setMethods({ - shiftZoomFocalPoint: jest.fn(), - scaleZoomFocalPoint: jest.fn(), - scrollToFocalPoint: jest.fn(), - }); - - wrapper.vm.onImageResize({ width: 10, height: 10 }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.shiftZoomFocalPoint).toHaveBeenCalled(); - expect(wrapper.vm.initialLoad).toBe(false); - }); - }); - - it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', () => { - wrapper.vm.onImageResize({ width: 10, height: 10 }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled(); - expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled(); - }); - }); - }); - - describe('onPresentationMousedown', () => { - it.each` - scenario | width | height - ${'width overflows'} | ${101} | ${100} - ${'height overflows'} | ${100} | ${101} - ${'width and height overflows'} | ${200} | ${200} - `('sets lastDragPosition when design $scenario', ({ width, height }) => { - createComponent(); - mockRefDimensions( - wrapper.vm.$refs.presentationViewport, - { width: 100, height: 100 }, - { width, height }, - ); - - const newLastDragPosition = { x: 2, y: 2 }; - wrapper.vm.onPresentationMousedown({ - clientX: newLastDragPosition.x, - clientY: newLastDragPosition.y, - }); - - expect(wrapper.vm.lastDragPosition).toStrictEqual(newLastDragPosition); - }); - - it('does not set lastDragPosition if design does not overflow', () => { - const lastDragPosition = { x: 1, y: 1 }; - - createComponent({}, { lastDragPosition }); - mockRefDimensions( - wrapper.vm.$refs.presentationViewport, - { width: 100, height: 100 }, - { width: 50, height: 50 }, - ); - - wrapper.vm.onPresentationMousedown({ clientX: 2, clientY: 2 }); - - // check lastDragPosition is unchanged - expect(wrapper.vm.lastDragPosition).toStrictEqual(lastDragPosition); - }); - }); - - describe('getAnnotationPositon', () => { - it.each` - coordinates | overlayDimensions | position - ${{ x: 100, y: 100 }} | ${{ width: 50, height: 50 }} | ${{ x: 100, y: 100, width: 50, height: 50 }} - ${{ x: 100.2, y: 100.5 }} | ${{ width: 50.6, height: 50.0 }} | ${{ x: 100, y: 101, width: 51, height: 50 }} - `('returns correct annotation position', ({ coordinates, overlayDimensions, position }) => { - createComponent(undefined, { - overlayDimensions: { - width: overlayDimensions.width, - height: overlayDimensions.height, - }, - }); - - expect(wrapper.vm.getAnnotationPositon(coordinates)).toStrictEqual(position); - }); - }); - - describe('when design is overflowing', () => { - beforeEach(() => { - createComponent( - { - image: 'test.jpg', - imageName: 'test', - }, - mockOverlayData, - { - 'design-overlay': DesignOverlay, - }, - ); - - // mock a design that overflows - mockRefDimensions( - wrapper.vm.$refs.presentationViewport, - { width: 10, height: 10 }, - { width: 20, height: 20 }, - 0, - 0, - ); - }); - - it('opens a comment form if design was not dragged', () => { - const addCommentOverlay = findOverlayCommentButton(); - const startCoords = { - clientX: 1, - clientY: 1, - }; - - addCommentOverlay.trigger('mousedown', { - clientX: startCoords.clientX, - clientY: startCoords.clientY, - }); - - return wrapper.vm - .$nextTick() - .then(() => { - addCommentOverlay.trigger('mouseup'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted('openCommentForm')).toBeDefined(); - }); - }); - - describe('when clicking and dragging', () => { - it.each` - description | useTouchEvents - ${'with touch events'} | ${true} - ${'without touch events'} | ${false} - `('calls scrollTo with correct arguments $description', ({ useTouchEvents }) => { - return clickDragExplore( - { clientX: 0, clientY: 0 }, - { clientX: 10, clientY: 10 }, - { useTouchEvents }, - ).then(() => { - expect(wrapper.element.scrollTo).toHaveBeenCalledTimes(1); - expect(wrapper.element.scrollTo).toHaveBeenCalledWith(-10, -10); - }); - }); - - it('does not open a comment form when drag position exceeds buffer', () => { - return clickDragExplore( - { clientX: 0, clientY: 0 }, - { clientX: 10, clientY: 10 }, - { mouseup: true }, - ).then(() => { - expect(wrapper.emitted('openCommentForm')).toBeFalsy(); - }); - }); - - it('opens a comment form when drag position is within buffer', () => { - return clickDragExplore( - { clientX: 0, clientY: 0 }, - { clientX: 1, clientY: 0 }, - { mouseup: true }, - ).then(() => { - expect(wrapper.emitted('openCommentForm')).toBeDefined(); - }); - }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_scaler_spec.js b/spec/frontend/design_management_legacy/components/design_scaler_spec.js deleted file mode 100644 index 30ef5ab159b..00000000000 --- a/spec/frontend/design_management_legacy/components/design_scaler_spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import DesignScaler from '~/design_management_legacy/components/design_scaler.vue'; - -describe('Design management design scaler component', () => { - let wrapper; - - function createComponent(propsData, data = {}) { - wrapper = shallowMount(DesignScaler, { - propsData, - }); - wrapper.setData(data); - } - - afterEach(() => { - wrapper.destroy(); - }); - - const getButton = type => { - const buttonTypeOrder = ['minus', 'reset', 'plus']; - const buttons = wrapper.findAll('button'); - return buttons.at(buttonTypeOrder.indexOf(type)); - }; - - it('emits @scale event when "plus" button clicked', () => { - createComponent(); - - getButton('plus').trigger('click'); - expect(wrapper.emitted('scale')).toEqual([[1.2]]); - }); - - it('emits @scale event when "reset" button clicked (scale > 1)', () => { - createComponent({}, { scale: 1.6 }); - return wrapper.vm.$nextTick().then(() => { - getButton('reset').trigger('click'); - expect(wrapper.emitted('scale')).toEqual([[1]]); - }); - }); - - it('emits @scale event when "minus" button clicked (scale > 1)', () => { - createComponent({}, { scale: 1.6 }); - - return wrapper.vm.$nextTick().then(() => { - getButton('minus').trigger('click'); - expect(wrapper.emitted('scale')).toEqual([[1.4]]); - }); - }); - - it('minus and reset buttons are disabled when scale === 1', () => { - createComponent(); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('minus and reset buttons are enabled when scale > 1', () => { - createComponent({}, { scale: 1.2 }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('plus button is disabled when scale === 2', () => { - createComponent({}, { scale: 2 }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/design_sidebar_spec.js b/spec/frontend/design_management_legacy/components/design_sidebar_spec.js deleted file mode 100644 index fc0f618c359..00000000000 --- a/spec/frontend/design_management_legacy/components/design_sidebar_spec.js +++ /dev/null @@ -1,236 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlCollapse, GlPopover } from '@gitlab/ui'; -import Cookies from 'js-cookie'; -import DesignSidebar from '~/design_management_legacy/components/design_sidebar.vue'; -import Participants from '~/sidebar/components/participants/participants.vue'; -import DesignDiscussion from '~/design_management_legacy/components/design_notes/design_discussion.vue'; -import design from '../mock_data/design'; -import updateActiveDiscussionMutation from '~/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql'; - -const updateActiveDiscussionMutationVariables = { - mutation: updateActiveDiscussionMutation, - variables: { - id: design.discussions.nodes[0].notes.nodes[0].id, - source: 'discussion', - }, -}; - -const $route = { - params: { - id: '1', - }, -}; - -const cookieKey = 'hide_design_resolved_comments_popover'; - -const mutate = jest.fn().mockResolvedValue(); - -describe('Design management design sidebar component', () => { - let wrapper; - - const findDiscussions = () => wrapper.findAll(DesignDiscussion); - const findFirstDiscussion = () => findDiscussions().at(0); - const findUnresolvedDiscussions = () => wrapper.findAll('[data-testid="unresolved-discussion"]'); - const findResolvedDiscussions = () => wrapper.findAll('[data-testid="resolved-discussion"]'); - const findParticipants = () => wrapper.find(Participants); - const findCollapsible = () => wrapper.find(GlCollapse); - const findToggleResolvedCommentsButton = () => wrapper.find('[data-testid="resolved-comments"]'); - const findPopover = () => wrapper.find(GlPopover); - const findNewDiscussionDisclaimer = () => - wrapper.find('[data-testid="new-discussion-disclaimer"]'); - - function createComponent(props = {}) { - wrapper = shallowMount(DesignSidebar, { - propsData: { - design, - resolvedDiscussionsExpanded: false, - markdownPreviewPath: '', - ...props, - }, - mocks: { - $route, - $apollo: { - mutate, - }, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders participants', () => { - createComponent(); - - expect(findParticipants().exists()).toBe(true); - }); - - it('passes the correct amount of participants to the Participants component', () => { - createComponent(); - - expect(findParticipants().props('participants')).toHaveLength(1); - }); - - describe('when has no discussions', () => { - beforeEach(() => { - createComponent({ - design: { - ...design, - discussions: { - nodes: [], - }, - }, - }); - }); - - it('does not render discussions', () => { - expect(findDiscussions().exists()).toBe(false); - }); - - it('renders a message about possibility to create a new discussion', () => { - expect(findNewDiscussionDisclaimer().exists()).toBe(true); - }); - }); - - describe('when has discussions', () => { - beforeEach(() => { - Cookies.set(cookieKey, true); - createComponent(); - }); - - it('renders correct amount of unresolved discussions', () => { - expect(findUnresolvedDiscussions()).toHaveLength(1); - }); - - it('renders correct amount of resolved discussions', () => { - expect(findResolvedDiscussions()).toHaveLength(1); - }); - - it('has resolved comments collapsible collapsed', () => { - expect(findCollapsible().attributes('visible')).toBeUndefined(); - }); - - it('emits toggleResolveComments event on resolve comments button click', () => { - findToggleResolvedCommentsButton().vm.$emit('click'); - expect(wrapper.emitted('toggleResolvedComments')).toHaveLength(1); - }); - - it('opens a collapsible when resolvedDiscussionsExpanded prop changes to true', () => { - expect(findCollapsible().attributes('visible')).toBeUndefined(); - wrapper.setProps({ - resolvedDiscussionsExpanded: true, - }); - return wrapper.vm.$nextTick().then(() => { - expect(findCollapsible().attributes('visible')).toBe('true'); - }); - }); - - it('does not popover about resolved comments', () => { - expect(findPopover().exists()).toBe(false); - }); - - it('sends a mutation to set an active discussion when clicking on a discussion', () => { - findFirstDiscussion().trigger('click'); - - expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables); - }); - - it('sends a mutation to reset an active discussion when clicking outside of discussion', () => { - wrapper.trigger('click'); - - expect(mutate).toHaveBeenCalledWith({ - ...updateActiveDiscussionMutationVariables, - variables: { id: undefined, source: 'discussion' }, - }); - }); - - it('emits correct event on discussion create note error', () => { - findFirstDiscussion().vm.$emit('createNoteError', 'payload'); - expect(wrapper.emitted('onDesignDiscussionError')).toEqual([['payload']]); - }); - - it('emits correct event on discussion update note error', () => { - findFirstDiscussion().vm.$emit('updateNoteError', 'payload'); - expect(wrapper.emitted('updateNoteError')).toEqual([['payload']]); - }); - - it('emits correct event on discussion resolve error', () => { - findFirstDiscussion().vm.$emit('resolveDiscussionError', 'payload'); - expect(wrapper.emitted('resolveDiscussionError')).toEqual([['payload']]); - }); - - it('changes prop correctly on opening discussion form', () => { - findFirstDiscussion().vm.$emit('openForm', 'some-id'); - - return wrapper.vm.$nextTick().then(() => { - expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id'); - }); - }); - }); - - describe('when all discussions are resolved', () => { - beforeEach(() => { - createComponent({ - design: { - ...design, - discussions: { - nodes: [ - { - id: 'discussion-id', - replyId: 'discussion-reply-id', - resolved: true, - notes: { - nodes: [ - { - id: 'note-id', - body: '123', - author: { - name: 'Administrator', - username: 'root', - webUrl: 'link-to-author', - avatarUrl: 'link-to-avatar', - }, - }, - ], - }, - }, - ], - }, - }, - }); - }); - - it('renders a message about possibility to create a new discussion', () => { - expect(findNewDiscussionDisclaimer().exists()).toBe(true); - }); - - it('does not render unresolved discussions', () => { - expect(findUnresolvedDiscussions()).toHaveLength(0); - }); - }); - - describe('when showing resolved discussions for the first time', () => { - beforeEach(() => { - Cookies.set(cookieKey, false); - createComponent(); - }); - - it('renders a popover if we show resolved comments collapsible for the first time', () => { - expect(findPopover().exists()).toBe(true); - }); - - it('dismisses a popover on the outside click', () => { - wrapper.trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(findPopover().exists()).toBe(false); - }); - }); - - it(`sets a ${cookieKey} cookie on clicking outside the popover`, () => { - jest.spyOn(Cookies, 'set'); - wrapper.trigger('click'); - expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/image_spec.js b/spec/frontend/design_management_legacy/components/image_spec.js deleted file mode 100644 index 265c91abb4e..00000000000 --- a/spec/frontend/design_management_legacy/components/image_spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlIcon } from '@gitlab/ui'; -import DesignImage from '~/design_management_legacy/components/image.vue'; - -describe('Design management large image component', () => { - let wrapper; - - function createComponent(propsData, data = {}) { - wrapper = shallowMount(DesignImage, { - propsData, - }); - wrapper.setData(data); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders loading state', () => { - createComponent({ - isLoading: true, - }); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('renders image', () => { - createComponent({ - isLoading: false, - image: 'test.jpg', - name: 'test', - }); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('sets correct classes and styles if imageStyle is set', () => { - createComponent( - { - isLoading: false, - image: 'test.jpg', - name: 'test', - }, - { - imageStyle: { - width: '100px', - height: '100px', - }, - }, - ); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders media broken icon on error', () => { - createComponent({ - isLoading: false, - image: 'test.jpg', - name: 'test', - }); - - const image = wrapper.find('img'); - image.trigger('error'); - return wrapper.vm.$nextTick().then(() => { - expect(image.isVisible()).toBe(false); - expect(wrapper.find(GlIcon).element).toMatchSnapshot(); - }); - }); - - describe('zoom', () => { - const baseImageWidth = 100; - const baseImageHeight = 100; - - beforeEach(() => { - createComponent( - { - isLoading: false, - image: 'test.jpg', - name: 'test', - }, - { - imageStyle: { - width: `${baseImageWidth}px`, - height: `${baseImageHeight}px`, - }, - baseImageSize: { - width: baseImageWidth, - height: baseImageHeight, - }, - }, - ); - - jest.spyOn(wrapper.vm.$refs.contentImg, 'offsetWidth', 'get').mockReturnValue(baseImageWidth); - jest - .spyOn(wrapper.vm.$refs.contentImg, 'offsetHeight', 'get') - .mockReturnValue(baseImageHeight); - }); - - it('emits @resize event on zoom', () => { - const zoomAmount = 2; - wrapper.vm.zoom(zoomAmount); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('resize')).toEqual([ - [{ width: baseImageWidth * zoomAmount, height: baseImageHeight * zoomAmount }], - ]); - }); - }); - - it('emits @resize event with base image size when scale=1', () => { - wrapper.vm.zoom(1); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('resize')).toEqual([ - [{ width: baseImageWidth, height: baseImageHeight }], - ]); - }); - }); - - it('sets image style when zoomed', () => { - const zoomAmount = 2; - wrapper.vm.zoom(zoomAmount); - expect(wrapper.vm.imageStyle).toEqual({ - width: `${baseImageWidth * zoomAmount}px`, - height: `${baseImageHeight * zoomAmount}px`, - }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap deleted file mode 100644 index 003c8bc3c16..00000000000 --- a/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap +++ /dev/null @@ -1,149 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management list item component when item appears in view after image is loaded renders media broken icon when image onerror triggered 1`] = ` -<gl-icon-stub - class="text-secondary" - name="media-broken" - size="32" -/> -`; - -exports[`Design management list item component with notes renders item with multiple comments 1`] = ` -<router-link-stub - class="card cursor-pointer text-plain js-design-list-item design-list-item" - to="[object Object]" -> - <div - class="card-body p-0 d-flex-center overflow-hidden position-relative" - > - <!----> - - <gl-intersection-observer-stub> - <!----> - - <img - alt="test" - class="block mx-auto mw-100 mh-100 design-img" - data-qa-selector="design_image" - src="" - /> - </gl-intersection-observer-stub> - </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" - > - test - </span> - - <span - class="str-truncated-100" - > - - Updated - <timeago-stub - cssclass="" - time="01-01-2019" - tooltipplacement="bottom" - /> - </span> - </div> - - <div - class="ml-auto d-flex align-items-center text-secondary" - > - <gl-icon-stub - class="ml-1" - name="comments" - size="16" - /> - - <span - aria-label="2 comments" - class="ml-1" - > - - 2 - - </span> - </div> - </div> -</router-link-stub> -`; - -exports[`Design management list item component with notes renders item with single comment 1`] = ` -<router-link-stub - class="card cursor-pointer text-plain js-design-list-item design-list-item" - to="[object Object]" -> - <div - class="card-body p-0 d-flex-center overflow-hidden position-relative" - > - <!----> - - <gl-intersection-observer-stub> - <!----> - - <img - alt="test" - class="block mx-auto mw-100 mh-100 design-img" - data-qa-selector="design_image" - src="" - /> - </gl-intersection-observer-stub> - </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" - > - test - </span> - - <span - class="str-truncated-100" - > - - Updated - <timeago-stub - cssclass="" - time="01-01-2019" - tooltipplacement="bottom" - /> - </span> - </div> - - <div - class="ml-auto d-flex align-items-center text-secondary" - > - <gl-icon-stub - class="ml-1" - name="comments" - size="16" - /> - - <span - aria-label="1 comment" - class="ml-1" - > - - 1 - - </span> - </div> - </div> -</router-link-stub> -`; diff --git a/spec/frontend/design_management_legacy/components/list/item_spec.js b/spec/frontend/design_management_legacy/components/list/item_spec.js deleted file mode 100644 index 02e52e4b9d1..00000000000 --- a/spec/frontend/design_management_legacy/components/list/item_spec.js +++ /dev/null @@ -1,168 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; -import VueRouter from 'vue-router'; -import Item from '~/design_management_legacy/components/list/item.vue'; - -const localVue = createLocalVue(); -localVue.use(VueRouter); -const router = new VueRouter(); - -// Referenced from: doc/api/graphql/reference/gitlab_schema.graphql:DesignVersionEvent -const DESIGN_VERSION_EVENT = { - CREATION: 'CREATION', - DELETION: 'DELETION', - MODIFICATION: 'MODIFICATION', - NO_CHANGE: 'NONE', -}; - -describe('Design management list item component', () => { - let wrapper; - - const findDesignEvent = () => wrapper.find('[data-testid="designEvent"]'); - const findEventIcon = () => findDesignEvent().find(GlIcon); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); - - function createComponent({ - notesCount = 0, - event = DESIGN_VERSION_EVENT.NO_CHANGE, - isUploading = false, - imageLoading = false, - } = {}) { - wrapper = shallowMount(Item, { - localVue, - router, - propsData: { - id: 1, - filename: 'test', - image: 'http://via.placeholder.com/300', - isUploading, - event, - notesCount, - updatedAt: '01-01-2019', - }, - data() { - return { - imageLoading, - }; - }, - stubs: ['router-link'], - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - describe('when item is not in view', () => { - it('image is not rendered', () => { - createComponent(); - - const image = wrapper.find('img'); - expect(image.attributes('src')).toBe(''); - }); - }); - - describe('when item appears in view', () => { - let image; - let glIntersectionObserver; - - beforeEach(() => { - createComponent(); - image = wrapper.find('img'); - glIntersectionObserver = wrapper.find(GlIntersectionObserver); - - glIntersectionObserver.vm.$emit('appear'); - return wrapper.vm.$nextTick(); - }); - - describe('before image is loaded', () => { - it('renders loading spinner', () => { - expect(wrapper.find(GlLoadingIcon)).toExist(); - }); - }); - - describe('after image is loaded', () => { - beforeEach(() => { - image.trigger('load'); - return wrapper.vm.$nextTick(); - }); - - it('renders an image', () => { - expect(image.attributes('src')).toBe('http://via.placeholder.com/300'); - expect(image.isVisible()).toBe(true); - }); - - it('renders media broken icon when image onerror triggered', () => { - image.trigger('error'); - return wrapper.vm.$nextTick().then(() => { - expect(image.isVisible()).toBe(false); - expect(wrapper.find(GlIcon).element).toMatchSnapshot(); - }); - }); - - describe('when imageV432x230 and image provided', () => { - it('renders imageV432x230 image', () => { - const mockSrc = 'mock-imageV432x230-url'; - wrapper.setProps({ imageV432x230: mockSrc }); - - return wrapper.vm.$nextTick().then(() => { - expect(image.attributes('src')).toBe(mockSrc); - }); - }); - }); - - describe('when image disappears from view and then reappears', () => { - beforeEach(() => { - glIntersectionObserver.vm.$emit('appear'); - return wrapper.vm.$nextTick(); - }); - - it('renders an image', () => { - expect(image.isVisible()).toBe(true); - }); - }); - }); - }); - - describe('with notes', () => { - it('renders item with single comment', () => { - createComponent({ notesCount: 1 }); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('renders item with multiple comments', () => { - createComponent({ notesCount: 2 }); - - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders loading spinner when isUploading is true', () => { - createComponent({ isUploading: true }); - - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('renders item with no status icon for none event', () => { - createComponent(); - - expect(findDesignEvent().exists()).toBe(false); - }); - - describe('with associated event', () => { - it.each` - event | icon | className - ${DESIGN_VERSION_EVENT.MODIFICATION} | ${'file-modified-solid'} | ${'text-primary-500'} - ${DESIGN_VERSION_EVENT.DELETION} | ${'file-deletion-solid'} | ${'text-danger-500'} - ${DESIGN_VERSION_EVENT.CREATION} | ${'file-addition-solid'} | ${'text-success-500'} - `('renders item with correct status icon for $event event', ({ event, icon, className }) => { - createComponent({ event }); - const eventIcon = findEventIcon(); - - expect(eventIcon.exists()).toBe(true); - expect(eventIcon.props('name')).toBe(icon); - expect(eventIcon.classes()).toContain(className); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap deleted file mode 100644 index 3d69821003a..00000000000 --- a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap +++ /dev/null @@ -1,61 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management toolbar component renders design and updated data 1`] = ` -<header - class="d-flex p-2 bg-white align-items-center js-design-header" -> - <a - aria-label="Go back to designs" - class="mr-3 text-plain d-flex justify-content-center align-items-center" - > - <gl-icon-stub - name="close" - size="18" - /> - </a> - - <div - class="overflow-hidden d-flex align-items-center" - > - <h2 - class="m-0 str-truncated-100 gl-font-base" - > - test.jpg - </h2> - - <small - class="text-secondary" - > - Updated 1 hour ago by Test Name - </small> - </div> - - <pagination-stub - class="ml-auto flex-shrink-0" - id="1" - /> - - <gl-deprecated-button-stub - class="mr-2" - href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d" - size="md" - variant="secondary" - > - <gl-icon-stub - name="download" - size="18" - /> - </gl-deprecated-button-stub> - - <delete-button-stub - buttonclass="" - buttonvariant="danger" - hasselecteddesigns="true" - > - <gl-icon-stub - name="remove" - size="18" - /> - </delete-button-stub> -</header> -`; diff --git a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap deleted file mode 100644 index 6bc24613140..00000000000 --- a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management pagination button component disables button when no design is passed 1`] = ` -<router-link-stub - aria-label="Test title" - class="btn btn-default disabled" - disabled="true" - to="[object Object]" -> - <gl-icon-stub - name="angle-right" - size="16" - /> -</router-link-stub> -`; - -exports[`Design management pagination button component renders router-link 1`] = ` -<router-link-stub - aria-label="Test title" - class="btn btn-default" - to="[object Object]" -> - <gl-icon-stub - name="angle-right" - size="16" - /> -</router-link-stub> -`; diff --git a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap deleted file mode 100644 index 0197b4bff79..00000000000 --- a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management pagination component hides components when designs are empty 1`] = `<!---->`; - -exports[`Design management pagination component renders pagination buttons 1`] = ` -<div - class="d-flex align-items-center" -> - - 0 of 2 - - <div - class="btn-group ml-3 mr-3" - > - <pagination-button-stub - class="js-previous-design" - iconname="angle-left" - title="Go to previous design" - /> - - <pagination-button-stub - class="js-next-design" - design="[object Object]" - iconname="angle-right" - title="Go to next design" - /> - </div> -</div> -`; diff --git a/spec/frontend/design_management_legacy/components/toolbar/index_spec.js b/spec/frontend/design_management_legacy/components/toolbar/index_spec.js deleted file mode 100644 index 8207cad4136..00000000000 --- a/spec/frontend/design_management_legacy/components/toolbar/index_spec.js +++ /dev/null @@ -1,123 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import VueRouter from 'vue-router'; -import { GlDeprecatedButton } from '@gitlab/ui'; -import Toolbar from '~/design_management_legacy/components/toolbar/index.vue'; -import DeleteButton from '~/design_management_legacy/components/delete_button.vue'; -import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants'; - -const localVue = createLocalVue(); -localVue.use(VueRouter); -const router = new VueRouter(); - -const RouterLinkStub = { - props: { - to: { - type: Object, - }, - }, - render(createElement) { - return createElement('a', {}, this.$slots.default); - }, -}; - -describe('Design management toolbar component', () => { - let wrapper; - - function createComponent(isLoading = false, createDesign = true, props) { - const updatedAt = new Date(); - updatedAt.setHours(updatedAt.getHours() - 1); - - wrapper = shallowMount(Toolbar, { - localVue, - router, - propsData: { - id: '1', - isLatestVersion: true, - isLoading, - isDeleting: false, - filename: 'test.jpg', - updatedAt: updatedAt.toString(), - updatedBy: { - name: 'Test Name', - }, - image: '/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d', - ...props, - }, - stubs: { - 'router-link': RouterLinkStub, - }, - }); - - wrapper.setData({ - permissions: { - createDesign, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders design and updated data', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('links back to designs list', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - const link = wrapper.find('a'); - - expect(link.props('to')).toEqual({ - name: DESIGNS_ROUTE_NAME, - query: { - version: undefined, - }, - }); - }); - }); - - it('renders delete button on latest designs version with logged in user', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DeleteButton).exists()).toBe(true); - }); - }); - - it('does not render delete button on non-latest version', () => { - createComponent(false, true, { isLatestVersion: false }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DeleteButton).exists()).toBe(false); - }); - }); - - it('does not render delete button when user is not logged in', () => { - createComponent(false, false); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DeleteButton).exists()).toBe(false); - }); - }); - - it('emits `delete` event on deleteButton `deleteSelectedDesigns` event', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - wrapper.find(DeleteButton).vm.$emit('deleteSelectedDesigns'); - expect(wrapper.emitted().delete).toBeTruthy(); - }); - }); - - it('renders download button with correct link', () => { - expect(wrapper.find(GlDeprecatedButton).attributes('href')).toBe( - '/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d', - ); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js b/spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js deleted file mode 100644 index d2153adca45..00000000000 --- a/spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import VueRouter from 'vue-router'; -import PaginationButton from '~/design_management_legacy/components/toolbar/pagination_button.vue'; -import { DESIGN_ROUTE_NAME } from '~/design_management_legacy/router/constants'; - -const localVue = createLocalVue(); -localVue.use(VueRouter); -const router = new VueRouter(); - -describe('Design management pagination button component', () => { - let wrapper; - - function createComponent(design = null) { - wrapper = shallowMount(PaginationButton, { - localVue, - router, - propsData: { - design, - title: 'Test title', - iconName: 'angle-right', - }, - stubs: ['router-link'], - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('disables button when no design is passed', () => { - createComponent(); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('renders router-link', () => { - createComponent({ id: '2' }); - - expect(wrapper.element).toMatchSnapshot(); - }); - - describe('designLink', () => { - it('returns empty link when design is null', () => { - createComponent(); - - expect(wrapper.vm.designLink).toEqual({}); - }); - - it('returns design link', () => { - createComponent({ id: '2', filename: 'test' }); - - wrapper.vm.$router.replace('/root/test-project/issues/1/designs/test?version=1'); - - expect(wrapper.vm.designLink).toEqual({ - name: DESIGN_ROUTE_NAME, - params: { id: 'test' }, - query: { version: '1' }, - }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js b/spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js deleted file mode 100644 index 21b55113a6e..00000000000 --- a/spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js +++ /dev/null @@ -1,79 +0,0 @@ -/* global Mousetrap */ -import 'mousetrap'; -import { shallowMount } from '@vue/test-utils'; -import Pagination from '~/design_management_legacy/components/toolbar/pagination.vue'; -import { DESIGN_ROUTE_NAME } from '~/design_management_legacy/router/constants'; - -const push = jest.fn(); -const $router = { - push, -}; - -const $route = { - path: '/designs/design-2', - query: {}, -}; - -describe('Design management pagination component', () => { - let wrapper; - - function createComponent() { - wrapper = shallowMount(Pagination, { - propsData: { - id: '2', - }, - mocks: { - $router, - $route, - }, - }); - } - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('hides components when designs are empty', () => { - expect(wrapper.element).toMatchSnapshot(); - }); - - it('renders pagination buttons', () => { - wrapper.setData({ - designs: [{ id: '1' }, { id: '2' }], - }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - describe('keyboard buttons navigation', () => { - beforeEach(() => { - wrapper.setData({ - designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }], - }); - }); - - it('routes to previous design on Left button', () => { - Mousetrap.trigger('left'); - expect(push).toHaveBeenCalledWith({ - name: DESIGN_ROUTE_NAME, - params: { id: '1' }, - query: {}, - }); - }); - - it('routes to next design on Right button', () => { - Mousetrap.trigger('right'); - expect(push).toHaveBeenCalledWith({ - name: DESIGN_ROUTE_NAME, - params: { id: '3' }, - query: {}, - }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap deleted file mode 100644 index 27c0ba589e6..00000000000 --- a/spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap +++ /dev/null @@ -1,79 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management upload button component renders inverted upload design button 1`] = ` -<div - isinverted="true" -> - <gl-deprecated-button-stub - size="md" - title="Adding a design with the same filename replaces the file in a new version." - variant="success" - > - - Upload designs - - <!----> - </gl-deprecated-button-stub> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> -</div> -`; - -exports[`Design management upload button component renders loading icon 1`] = ` -<div> - <gl-deprecated-button-stub - disabled="true" - size="md" - title="Adding a design with the same filename replaces the file in a new version." - variant="success" - > - - Upload designs - - <gl-loading-icon-stub - class="ml-1" - color="orange" - inline="true" - label="Loading" - size="sm" - /> - </gl-deprecated-button-stub> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> -</div> -`; - -exports[`Design management upload button component renders upload design button 1`] = ` -<div> - <gl-deprecated-button-stub - size="md" - title="Adding a design with the same filename replaces the file in a new version." - variant="success" - > - - Upload designs - - <!----> - </gl-deprecated-button-stub> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> -</div> -`; diff --git a/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap deleted file mode 100644 index 0737b9729a2..00000000000 --- a/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap +++ /dev/null @@ -1,455 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management dropzone component when dragging renders correct template when drag event contains files 1`] = ` -<div - class="w-100 position-relative" -> - <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3" - > - <div - class="d-flex-center flex-column text-center" - > - <gl-icon-stub - class="mb-4" - name="doc-new" - size="48" - /> - - <p> - <gl-sprintf-stub - message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}." - /> - </p> - </div> - </button> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> - - <transition-stub - name="design-dropzone-fade" - > - <div - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - style="" - > - <div - class="mw-50 text-center" - style="display: none;" - > - <h3> - 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 - class="mw-50 text-center" - style="" - > - <h3> - Incoming! - </h3> - - <span> - Drop your designs to start your upload. - </span> - </div> - </div> - </transition-stub> -</div> -`; - -exports[`Design management dropzone component when dragging renders correct template when drag event contains files and text 1`] = ` -<div - class="w-100 position-relative" -> - <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3" - > - <div - class="d-flex-center flex-column text-center" - > - <gl-icon-stub - class="mb-4" - name="doc-new" - size="48" - /> - - <p> - <gl-sprintf-stub - message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}." - /> - </p> - </div> - </button> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> - - <transition-stub - name="design-dropzone-fade" - > - <div - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - style="" - > - <div - class="mw-50 text-center" - style="display: none;" - > - <h3> - 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 - class="mw-50 text-center" - style="" - > - <h3> - Incoming! - </h3> - - <span> - Drop your designs to start your upload. - </span> - </div> - </div> - </transition-stub> -</div> -`; - -exports[`Design management dropzone component when dragging renders correct template when drag event contains text 1`] = ` -<div - class="w-100 position-relative" -> - <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3" - > - <div - class="d-flex-center flex-column text-center" - > - <gl-icon-stub - class="mb-4" - name="doc-new" - size="48" - /> - - <p> - <gl-sprintf-stub - message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}." - /> - </p> - </div> - </button> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> - - <transition-stub - name="design-dropzone-fade" - > - <div - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - style="" - > - <div - class="mw-50 text-center" - > - <h3> - 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 - class="mw-50 text-center" - style="display: none;" - > - <h3> - Incoming! - </h3> - - <span> - Drop your designs to start your upload. - </span> - </div> - </div> - </transition-stub> -</div> -`; - -exports[`Design management dropzone component when dragging renders correct template when drag event is empty 1`] = ` -<div - class="w-100 position-relative" -> - <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3" - > - <div - class="d-flex-center flex-column text-center" - > - <gl-icon-stub - class="mb-4" - name="doc-new" - size="48" - /> - - <p> - <gl-sprintf-stub - message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}." - /> - </p> - </div> - </button> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> - - <transition-stub - name="design-dropzone-fade" - > - <div - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - style="" - > - <div - class="mw-50 text-center" - > - <h3> - 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 - class="mw-50 text-center" - style="display: none;" - > - <h3> - Incoming! - </h3> - - <span> - Drop your designs to start your upload. - </span> - </div> - </div> - </transition-stub> -</div> -`; - -exports[`Design management dropzone component when dragging renders correct template when dragging stops 1`] = ` -<div - class="w-100 position-relative" -> - <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3" - > - <div - class="d-flex-center flex-column text-center" - > - <gl-icon-stub - class="mb-4" - name="doc-new" - size="48" - /> - - <p> - <gl-sprintf-stub - message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}." - /> - </p> - </div> - </button> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> - - <transition-stub - name="design-dropzone-fade" - > - <div - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - style="display: none;" - > - <div - class="mw-50 text-center" - > - <h3> - 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 - class="mw-50 text-center" - style="display: none;" - > - <h3> - Incoming! - </h3> - - <span> - Drop your designs to start your upload. - </span> - </div> - </div> - </transition-stub> -</div> -`; - -exports[`Design management dropzone component when no slot provided renders default dropzone card 1`] = ` -<div - class="w-100 position-relative" -> - <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3" - > - <div - class="d-flex-center flex-column text-center" - > - <gl-icon-stub - class="mb-4" - name="doc-new" - size="48" - /> - - <p> - <gl-sprintf-stub - message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}." - /> - </p> - </div> - </button> - - <input - accept="image/*" - class="hide" - multiple="multiple" - name="design_file" - type="file" - /> - - <transition-stub - name="design-dropzone-fade" - > - <div - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - style="display: none;" - > - <div - class="mw-50 text-center" - > - <h3> - 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 - class="mw-50 text-center" - style="display: none;" - > - <h3> - Incoming! - </h3> - - <span> - Drop your designs to start your upload. - </span> - </div> - </div> - </transition-stub> -</div> -`; - -exports[`Design management dropzone component when slot provided renders dropzone with slot content 1`] = ` -<div - class="w-100 position-relative" -> - <div> - dropzone slot - </div> - - <transition-stub - name="design-dropzone-fade" - > - <div - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - style="display: none;" - > - <div - class="mw-50 text-center" - > - <h3> - 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 - class="mw-50 text-center" - style="display: none;" - > - <h3> - Incoming! - </h3> - - <span> - Drop your designs to start your upload. - </span> - </div> - </div> - </transition-stub> -</div> -`; diff --git a/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap deleted file mode 100644 index d34b925f33d..00000000000 --- a/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap +++ /dev/null @@ -1,111 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management design version dropdown component renders design version dropdown button 1`] = ` -<gl-deprecated-dropdown-stub - class="design-version-dropdown" - issueiid="" - projectpath="" - text="Showing Latest Version" - variant="link" -> - <gl-deprecated-dropdown-item-stub> - <router-link-stub - class="d-flex js-version-link" - to="[object Object]" - > - <div - class="flex-grow-1 ml-2" - > - <div> - <strong> - Version 2 - - <span> - (latest) - </span> - </strong> - </div> - </div> - - <i - class="fa fa-check float-right gl-mr-2" - /> - </router-link-stub> - </gl-deprecated-dropdown-item-stub> - <gl-deprecated-dropdown-item-stub> - <router-link-stub - class="d-flex js-version-link" - to="[object Object]" - > - <div - class="flex-grow-1 ml-2" - > - <div> - <strong> - Version 1 - - <!----> - </strong> - </div> - </div> - - <!----> - </router-link-stub> - </gl-deprecated-dropdown-item-stub> -</gl-deprecated-dropdown-stub> -`; - -exports[`Design management design version dropdown component renders design version list 1`] = ` -<gl-deprecated-dropdown-stub - class="design-version-dropdown" - issueiid="" - projectpath="" - text="Showing Latest Version" - variant="link" -> - <gl-deprecated-dropdown-item-stub> - <router-link-stub - class="d-flex js-version-link" - to="[object Object]" - > - <div - class="flex-grow-1 ml-2" - > - <div> - <strong> - Version 2 - - <span> - (latest) - </span> - </strong> - </div> - </div> - - <i - class="fa fa-check float-right gl-mr-2" - /> - </router-link-stub> - </gl-deprecated-dropdown-item-stub> - <gl-deprecated-dropdown-item-stub> - <router-link-stub - class="d-flex js-version-link" - to="[object Object]" - > - <div - class="flex-grow-1 ml-2" - > - <div> - <strong> - Version 1 - - <!----> - </strong> - </div> - </div> - - <!----> - </router-link-stub> - </gl-deprecated-dropdown-item-stub> -</gl-deprecated-dropdown-stub> -`; diff --git a/spec/frontend/design_management_legacy/components/upload/button_spec.js b/spec/frontend/design_management_legacy/components/upload/button_spec.js deleted file mode 100644 index dde5c694194..00000000000 --- a/spec/frontend/design_management_legacy/components/upload/button_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import UploadButton from '~/design_management_legacy/components/upload/button.vue'; - -describe('Design management upload button component', () => { - let wrapper; - - function createComponent(isSaving = false, isInverted = false) { - wrapper = shallowMount(UploadButton, { - propsData: { - isSaving, - isInverted, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders upload design button', () => { - createComponent(); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('renders inverted upload design button', () => { - createComponent(false, true); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('renders loading icon', () => { - createComponent(true); - - expect(wrapper.element).toMatchSnapshot(); - }); - - describe('onFileUploadChange', () => { - it('emits upload event', () => { - createComponent(); - - wrapper.vm.onFileUploadChange({ target: { files: 'test' } }); - - expect(wrapper.emitted().upload[0]).toEqual(['test']); - }); - }); - - describe('openFileUpload', () => { - it('triggers click on input', () => { - createComponent(); - - const clickSpy = jest.spyOn(wrapper.find('input').element, 'click'); - - wrapper.vm.openFileUpload(); - - expect(clickSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js b/spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js deleted file mode 100644 index 1907a3124a6..00000000000 --- a/spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js +++ /dev/null @@ -1,132 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import DesignDropzone from '~/design_management_legacy/components/upload/design_dropzone.vue'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; - -jest.mock('~/flash'); - -describe('Design management dropzone component', () => { - let wrapper; - - const mockDragEvent = ({ types = ['Files'], files = [] }) => { - return { dataTransfer: { types, files } }; - }; - - const findDropzoneCard = () => wrapper.find('.design-dropzone-card'); - - function createComponent({ slots = {}, data = {} } = {}) { - wrapper = shallowMount(DesignDropzone, { - slots, - data() { - return data; - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - describe('when slot provided', () => { - it('renders dropzone with slot content', () => { - createComponent({ - slots: { - default: ['<div>dropzone slot</div>'], - }, - }); - - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - describe('when no slot provided', () => { - it('renders default dropzone card', () => { - createComponent(); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('triggers click event on file input element when clicked', () => { - createComponent(); - const clickSpy = jest.spyOn(wrapper.find('input').element, 'click'); - - findDropzoneCard().trigger('click'); - expect(clickSpy).toHaveBeenCalled(); - }); - }); - - describe('when dragging', () => { - it.each` - description | eventPayload - ${'is empty'} | ${{}} - ${'contains text'} | ${mockDragEvent({ types: ['text'] })} - ${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })} - ${'contains files'} | ${mockDragEvent({ types: ['Files'] })} - `('renders correct template when drag event $description', ({ eventPayload }) => { - createComponent(); - - wrapper.trigger('dragenter', eventPayload); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders correct template when dragging stops', () => { - createComponent(); - - wrapper.trigger('dragenter'); - return wrapper.vm - .$nextTick() - .then(() => { - wrapper.trigger('dragleave'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - }); - - describe('when dropping', () => { - it('emits upload event', () => { - createComponent(); - const mockFile = { name: 'test', type: 'image/jpg' }; - const mockEvent = mockDragEvent({ files: [mockFile] }); - - wrapper.trigger('dragenter', mockEvent); - return wrapper.vm - .$nextTick() - .then(() => { - wrapper.trigger('drop', mockEvent); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted().change[0]).toEqual([[mockFile]]); - }); - }); - }); - - describe('ondrop', () => { - const mockData = { dragCounter: 1, isDragDataValid: true }; - - describe('when drag data is valid', () => { - it('emits upload event for valid files', () => { - createComponent({ data: mockData }); - - const mockFile = { type: 'image/jpg' }; - const mockEvent = mockDragEvent({ files: [mockFile] }); - - wrapper.vm.ondrop(mockEvent); - expect(wrapper.emitted().change[0]).toEqual([[mockFile]]); - }); - - it('calls createFlash when files are invalid', () => { - createComponent({ data: mockData }); - - const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] }); - - wrapper.vm.ondrop(mockEvent); - expect(createFlash).toHaveBeenCalledTimes(1); - }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js deleted file mode 100644 index 7fb85f357c7..00000000000 --- a/spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js +++ /dev/null @@ -1,122 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; -import DesignVersionDropdown from '~/design_management_legacy/components/upload/design_version_dropdown.vue'; -import mockAllVersions from './mock_data/all_versions'; - -const LATEST_VERSION_ID = 3; -const PREVIOUS_VERSION_ID = 2; - -const designRouteFactory = versionId => ({ - path: `/designs?version=${versionId}`, - query: { - version: `${versionId}`, - }, -}); - -const MOCK_ROUTE = { - path: '/designs', - query: {}, -}; - -describe('Design management design version dropdown component', () => { - let wrapper; - - function createComponent({ maxVersions = -1, $route = MOCK_ROUTE } = {}) { - wrapper = shallowMount(DesignVersionDropdown, { - propsData: { - projectPath: '', - issueIid: '', - }, - mocks: { - $route, - }, - stubs: ['router-link'], - }); - - wrapper.setData({ - allVersions: maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - const findVersionLink = index => wrapper.findAll('.js-version-link').at(index); - - it('renders design version dropdown button', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders design version list', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - describe('selected version name', () => { - it('has "latest" on most recent version item', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - expect(findVersionLink(0).text()).toContain('latest'); - }); - }); - }); - - describe('versions list', () => { - it('displays latest version text by default', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe( - 'Showing Latest Version', - ); - }); - }); - - it('displays latest version text when only 1 version is present', () => { - createComponent({ maxVersions: 1 }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe( - 'Showing Latest Version', - ); - }); - }); - - it('displays version text when the current version is not the latest', () => { - createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(`Showing Version #1`); - }); - }); - - it('displays latest version text when the current version is the latest', () => { - createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe( - 'Showing Latest Version', - ); - }); - }); - - it('should have the same length as apollo query', () => { - createComponent(); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.findAll(GlDeprecatedDropdownItem)).toHaveLength( - wrapper.vm.allVersions.length, - ); - }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js b/spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js deleted file mode 100644 index e76bbd261bd..00000000000 --- a/spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js +++ /dev/null @@ -1,14 +0,0 @@ -export default [ - { - node: { - id: 'gid://gitlab/DesignManagement::Version/3', - sha: '0945756378e0b1588b9dd40d5a6b99e8b7198f55', - }, - }, - { - node: { - id: 'gid://gitlab/DesignManagement::Version/2', - sha: '5b063fef0cd7213b312db65b30e24f057df21b20', - }, - }, -]; diff --git a/spec/frontend/design_management_legacy/mock_data/all_versions.js b/spec/frontend/design_management_legacy/mock_data/all_versions.js deleted file mode 100644 index c389fdb8747..00000000000 --- a/spec/frontend/design_management_legacy/mock_data/all_versions.js +++ /dev/null @@ -1,8 +0,0 @@ -export default [ - { - node: { - id: 'gid://gitlab/DesignManagement::Version/1', - sha: 'b389071a06c153509e11da1f582005b316667001', - }, - }, -]; diff --git a/spec/frontend/design_management_legacy/mock_data/design.js b/spec/frontend/design_management_legacy/mock_data/design.js deleted file mode 100644 index 675198b9408..00000000000 --- a/spec/frontend/design_management_legacy/mock_data/design.js +++ /dev/null @@ -1,74 +0,0 @@ -export default { - id: 'design-id', - filename: 'test.jpg', - fullPath: 'full-design-path', - image: 'test.jpg', - updatedAt: '01-01-2019', - updatedBy: { - name: 'test', - }, - issue: { - title: 'My precious issue', - webPath: 'full-issue-path', - webUrl: 'full-issue-url', - participants: { - edges: [ - { - node: { - name: 'Administrator', - username: 'root', - webUrl: 'link-to-author', - avatarUrl: 'link-to-avatar', - }, - }, - ], - }, - }, - discussions: { - nodes: [ - { - id: 'discussion-id', - replyId: 'discussion-reply-id', - resolved: false, - notes: { - nodes: [ - { - id: 'note-id', - body: '123', - author: { - name: 'Administrator', - username: 'root', - webUrl: 'link-to-author', - avatarUrl: 'link-to-avatar', - }, - }, - ], - }, - }, - { - id: 'discussion-resolved', - replyId: 'discussion-reply-resolved', - resolved: true, - notes: { - nodes: [ - { - id: 'note-resolved', - body: '123', - author: { - name: 'Administrator', - username: 'root', - webUrl: 'link-to-author', - avatarUrl: 'link-to-avatar', - }, - }, - ], - }, - }, - ], - }, - diffRefs: { - headSha: 'headSha', - baseSha: 'baseSha', - startSha: 'startSha', - }, -}; diff --git a/spec/frontend/design_management_legacy/mock_data/designs.js b/spec/frontend/design_management_legacy/mock_data/designs.js deleted file mode 100644 index 07f5c1b7457..00000000000 --- a/spec/frontend/design_management_legacy/mock_data/designs.js +++ /dev/null @@ -1,17 +0,0 @@ -import design from './design'; - -export default { - project: { - issue: { - designCollection: { - designs: { - edges: [ - { - node: design, - }, - ], - }, - }, - }, - }, -}; diff --git a/spec/frontend/design_management_legacy/mock_data/no_designs.js b/spec/frontend/design_management_legacy/mock_data/no_designs.js deleted file mode 100644 index 9db0ffcade2..00000000000 --- a/spec/frontend/design_management_legacy/mock_data/no_designs.js +++ /dev/null @@ -1,11 +0,0 @@ -export default { - project: { - issue: { - designCollection: { - designs: { - edges: [], - }, - }, - }, - }, -}; diff --git a/spec/frontend/design_management_legacy/mock_data/notes.js b/spec/frontend/design_management_legacy/mock_data/notes.js deleted file mode 100644 index 80cb3944786..00000000000 --- a/spec/frontend/design_management_legacy/mock_data/notes.js +++ /dev/null @@ -1,46 +0,0 @@ -export default [ - { - id: 'note-id-1', - index: 1, - position: { - height: 100, - width: 100, - x: 10, - y: 15, - }, - author: { - name: 'John', - webUrl: 'link-to-john-profile', - }, - createdAt: '2020-05-08T07:10:45Z', - userPermissions: { - adminNote: true, - }, - discussion: { - id: 'discussion-id-1', - }, - resolved: false, - }, - { - id: 'note-id-2', - index: 2, - position: { - height: 50, - width: 50, - x: 25, - y: 25, - }, - author: { - name: 'Mary', - webUrl: 'link-to-mary-profile', - }, - createdAt: '2020-05-08T07:10:45Z', - userPermissions: { - adminNote: true, - }, - discussion: { - id: 'discussion-id-2', - }, - resolved: true, - }, -]; diff --git a/spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap deleted file mode 100644 index 3ba63fd14f0..00000000000 --- a/spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap +++ /dev/null @@ -1,263 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management index page designs does not render toolbar when there is no permission 1`] = ` -<div> - <!----> - - <div - class="mt-4" - > - <ol - class="list-unstyled row" - > - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub - class="design-list-item" - /> - </li> - - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub> - <design-stub - event="NONE" - filename="design-1-name" - id="design-1" - image="design-1-image" - notescount="0" - /> - </design-dropzone-stub> - - <!----> - </li> - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub> - <design-stub - event="NONE" - filename="design-2-name" - id="design-2" - image="design-2-image" - notescount="1" - /> - </design-dropzone-stub> - - <!----> - </li> - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub> - <design-stub - event="NONE" - filename="design-3-name" - id="design-3" - image="design-3-image" - notescount="0" - /> - </design-dropzone-stub> - - <!----> - </li> - </ol> - </div> - - <router-view-stub - name="default" - /> -</div> -`; - -exports[`Design management index page designs renders designs list and header with upload button 1`] = ` -<div> - <header - class="row-content-block border-top-0 p-2 d-flex" - > - <div - class="d-flex justify-content-between align-items-center w-100" - > - <design-version-dropdown-stub /> - - <div - class="qa-selector-toolbar d-flex" - > - <gl-deprecated-button-stub - class="mr-2 js-select-all" - size="md" - variant="link" - > - Select all - </gl-deprecated-button-stub> - - <div> - <delete-button-stub - buttonclass="btn-danger btn-inverted mr-2" - buttonvariant="" - > - - Delete selected - - <!----> - </delete-button-stub> - </div> - - <upload-button-stub /> - </div> - </div> - </header> - - <div - class="mt-4" - > - <ol - class="list-unstyled row" - > - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub - class="design-list-item" - /> - </li> - - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub> - <design-stub - event="NONE" - filename="design-1-name" - id="design-1" - image="design-1-image" - notescount="0" - /> - </design-dropzone-stub> - - <input - class="design-checkbox" - type="checkbox" - /> - </li> - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub> - <design-stub - event="NONE" - filename="design-2-name" - id="design-2" - image="design-2-image" - notescount="1" - /> - </design-dropzone-stub> - - <input - class="design-checkbox" - type="checkbox" - /> - </li> - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub> - <design-stub - event="NONE" - filename="design-3-name" - id="design-3" - image="design-3-image" - notescount="0" - /> - </design-dropzone-stub> - - <input - class="design-checkbox" - type="checkbox" - /> - </li> - </ol> - </div> - - <router-view-stub - name="default" - /> -</div> -`; - -exports[`Design management index page designs renders error 1`] = ` -<div> - <!----> - - <div - class="mt-4" - > - <gl-alert-stub - dismisslabel="Dismiss" - primarybuttonlink="" - primarybuttontext="" - secondarybuttonlink="" - secondarybuttontext="" - title="" - variant="danger" - > - - An error occurred while loading designs. Please try again. - - </gl-alert-stub> - </div> - - <router-view-stub - name="default" - /> -</div> -`; - -exports[`Design management index page designs renders loading icon 1`] = ` -<div> - <!----> - - <div - class="mt-4" - > - <gl-loading-icon-stub - color="orange" - label="Loading" - size="md" - /> - </div> - - <router-view-stub - name="default" - /> -</div> -`; - -exports[`Design management index page when has no designs renders empty text 1`] = ` -<div> - <!----> - - <div - class="mt-4" - > - <ol - class="list-unstyled row" - > - <li - class="col-md-6 col-lg-4 mb-3" - > - <design-dropzone-stub - class="design-list-item" - /> - </li> - - </ol> - </div> - - <router-view-stub - name="default" - /> -</div> -`; diff --git a/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap deleted file mode 100644 index dc5baf37fc6..00000000000 --- a/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap +++ /dev/null @@ -1,216 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Design management design index page renders design index 1`] = ` -<div - class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row" -> - <div - class="d-flex overflow-hidden flex-grow-1 flex-column position-relative" - > - <design-destroyer-stub - filenames="test.jpg" - iid="1" - projectpath="" - /> - - <!----> - - <design-presentation-stub - discussions="[object Object],[object Object]" - image="test.jpg" - imagename="test.jpg" - scale="1" - /> - - <div - class="design-scaler-wrapper position-absolute mb-4 d-flex-center" - > - <design-scaler-stub /> - </div> - </div> - - <div - class="image-notes" - > - <h2 - class="gl-font-weight-bold gl-mt-0" - > - - My precious issue - - </h2> - - <a - class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block" - href="full-issue-url" - > - ull-issue-path - </a> - - <participants-stub - class="gl-mb-4" - numberoflessparticipants="7" - participants="[object Object]" - /> - - <!----> - - <design-discussion-stub - data-testid="unresolved-discussion" - designid="test" - discussion="[object Object]" - discussionwithopenform="" - markdownpreviewpath="//preview_markdown?target_type=Issue" - noteableid="design-id" - /> - - <gl-button-stub - category="primary" - class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4" - data-testid="resolved-comments" - icon="chevron-right" - id="resolved-comments" - size="medium" - variant="link" - > - Resolved Comments (1) - - </gl-button-stub> - - <gl-popover-stub - container="popovercontainer" - cssclasses="" - placement="top" - show="true" - target="resolved-comments" - title="Resolved Comments" - > - <p> - - Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below - - </p> - - <a - href="#" - rel="noopener noreferrer" - target="_blank" - > - Learn more about resolving comments - </a> - </gl-popover-stub> - - <gl-collapse-stub - class="gl-mt-3" - > - <design-discussion-stub - data-testid="resolved-discussion" - designid="test" - discussion="[object Object]" - discussionwithopenform="" - markdownpreviewpath="//preview_markdown?target_type=Issue" - noteableid="design-id" - /> - </gl-collapse-stub> - - </div> -</div> -`; - -exports[`Design management design index page sets loading state 1`] = ` -<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-stub - class="align-self-center" - color="orange" - label="Loading" - size="xl" - /> -</div> -`; - -exports[`Design management design index page with error GlAlert is rendered in correct position with correct content 1`] = ` -<div - class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row" -> - <div - class="d-flex overflow-hidden flex-grow-1 flex-column position-relative" - > - <design-destroyer-stub - filenames="test.jpg" - iid="1" - projectpath="" - /> - - <div - class="p-3" - > - <gl-alert-stub - dismissible="true" - dismisslabel="Dismiss" - primarybuttonlink="" - primarybuttontext="" - secondarybuttonlink="" - secondarybuttontext="" - title="" - variant="danger" - > - - woops - - </gl-alert-stub> - </div> - - <design-presentation-stub - discussions="" - image="test.jpg" - imagename="test.jpg" - scale="1" - /> - - <div - class="design-scaler-wrapper position-absolute mb-4 d-flex-center" - > - <design-scaler-stub /> - </div> - </div> - - <div - class="image-notes" - > - <h2 - class="gl-font-weight-bold gl-mt-0" - > - - My precious issue - - </h2> - - <a - class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block" - href="full-issue-url" - > - ull-issue-path - </a> - - <participants-stub - class="gl-mb-4" - numberoflessparticipants="7" - participants="[object Object]" - /> - - <h2 - class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4" - data-testid="new-discussion-disclaimer" - > - - Click the image where you'd like to start a new discussion - - </h2> - - <!----> - - </div> -</div> -`; diff --git a/spec/frontend/design_management_legacy/pages/design/index_spec.js b/spec/frontend/design_management_legacy/pages/design/index_spec.js deleted file mode 100644 index 5eb4158c715..00000000000 --- a/spec/frontend/design_management_legacy/pages/design/index_spec.js +++ /dev/null @@ -1,291 +0,0 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import VueRouter from 'vue-router'; -import { GlAlert } from '@gitlab/ui'; -import { ApolloMutation } from 'vue-apollo'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import DesignIndex from '~/design_management_legacy/pages/design/index.vue'; -import DesignSidebar from '~/design_management_legacy/components/design_sidebar.vue'; -import DesignPresentation from '~/design_management_legacy/components/design_presentation.vue'; -import createImageDiffNoteMutation from '~/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql'; -import design from '../../mock_data/design'; -import mockResponseWithDesigns from '../../mock_data/designs'; -import mockResponseNoDesigns from '../../mock_data/no_designs'; -import mockAllVersions from '../../mock_data/all_versions'; -import { - DESIGN_NOT_FOUND_ERROR, - DESIGN_VERSION_NOT_EXIST_ERROR, -} from '~/design_management_legacy/utils/error_messages'; -import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants'; -import createRouter from '~/design_management_legacy/router'; -import * as utils from '~/design_management_legacy/utils/design_management_utils'; -import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_legacy/constants'; - -jest.mock('~/flash'); -jest.mock('mousetrap', () => ({ - bind: jest.fn(), - unbind: jest.fn(), -})); - -const focusInput = jest.fn(); - -const DesignReplyForm = { - template: '<div><textarea ref="textarea"></textarea></div>', - methods: { - focusInput, - }, -}; - -const localVue = createLocalVue(); -localVue.use(VueRouter); - -describe('Design management design index page', () => { - let wrapper; - let router; - - const newComment = 'new comment'; - const annotationCoordinates = { - x: 10, - y: 10, - width: 100, - height: 100, - }; - const createDiscussionMutationVariables = { - mutation: createImageDiffNoteMutation, - update: expect.anything(), - variables: { - input: { - body: newComment, - noteableId: design.id, - position: { - headSha: 'headSha', - baseSha: 'baseSha', - startSha: 'startSha', - paths: { - newPath: 'full-design-path', - }, - ...annotationCoordinates, - }, - }, - }, - }; - - const mutate = jest.fn().mockResolvedValue(); - - const findDiscussionForm = () => wrapper.find(DesignReplyForm); - const findSidebar = () => wrapper.find(DesignSidebar); - const findDesignPresentation = () => wrapper.find(DesignPresentation); - - function createComponent(loading = false, data = {}) { - const $apollo = { - queries: { - design: { - loading, - }, - }, - mutate, - }; - - router = createRouter(); - - wrapper = shallowMount(DesignIndex, { - propsData: { id: '1' }, - mocks: { $apollo }, - stubs: { - ApolloMutation, - DesignSidebar, - DesignReplyForm, - }, - data() { - return { - issueIid: '1', - activeDiscussion: { - id: null, - source: null, - }, - ...data, - }; - }, - localVue, - router, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - describe('when navigating', () => { - it('applies fullscreen layout', () => { - const mockEl = { - classList: { - add: jest.fn(), - remove: jest.fn(), - }, - }; - jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockEl); - createComponent(true); - - wrapper.vm.$router.push('/designs/test'); - expect(mockEl.classList.add).toHaveBeenCalledTimes(1); - expect(mockEl.classList.add).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST); - }); - }); - - it('sets loading state', () => { - createComponent(true); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('renders design index', () => { - createComponent(false, { design }); - - expect(wrapper.element).toMatchSnapshot(); - expect(wrapper.find(GlAlert).exists()).toBe(false); - }); - - it('passes correct props to sidebar component', () => { - createComponent(false, { design }); - - expect(findSidebar().props()).toEqual({ - design, - markdownPreviewPath: '//preview_markdown?target_type=Issue', - resolvedDiscussionsExpanded: false, - }); - }); - - it('opens a new discussion form', () => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], - }, - }, - }); - - findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 }); - - return wrapper.vm.$nextTick().then(() => { - expect(findDiscussionForm().exists()).toBe(true); - }); - }); - - it('keeps new discussion form focused', () => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], - }, - }, - annotationCoordinates, - }); - - findDesignPresentation().vm.$emit('openCommentForm', { x: 10, y: 10 }); - - expect(focusInput).toHaveBeenCalled(); - }); - - it('sends a mutation on submitting form and closes form', () => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], - }, - }, - annotationCoordinates, - comment: newComment, - }); - - findDiscussionForm().vm.$emit('submitForm'); - expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables); - - return wrapper.vm - .$nextTick() - .then(() => { - return mutate({ variables: createDiscussionMutationVariables }); - }) - .then(() => { - expect(findDiscussionForm().exists()).toBe(false); - }); - }); - - it('closes the form and clears the comment on canceling form', () => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], - }, - }, - annotationCoordinates, - comment: newComment, - }); - - findDiscussionForm().vm.$emit('cancelForm'); - - expect(wrapper.vm.comment).toBe(''); - - return wrapper.vm.$nextTick().then(() => { - expect(findDiscussionForm().exists()).toBe(false); - }); - }); - - describe('with error', () => { - beforeEach(() => { - createComponent(false, { - design: { - ...design, - discussions: { - nodes: [], - }, - }, - errorMessage: 'woops', - }); - }); - - it('GlAlert is rendered in correct position with correct content', () => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - describe('onDesignQueryResult', () => { - describe('with no designs', () => { - it('redirects to /designs', () => { - createComponent(true); - router.push = jest.fn(); - - wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false }); - return wrapper.vm.$nextTick().then(() => { - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith(DESIGN_NOT_FOUND_ERROR); - expect(router.push).toHaveBeenCalledTimes(1); - expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME }); - }); - }); - }); - - describe('when no design exists for given version', () => { - it('redirects to /designs', () => { - createComponent(true); - wrapper.setData({ - allVersions: mockAllVersions, - }); - - // attempt to query for a version of the design that doesn't exist - router.push({ query: { version: '999' } }); - router.push = jest.fn(); - - wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false }); - return wrapper.vm.$nextTick().then(() => { - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith(DESIGN_VERSION_NOT_EXIST_ERROR); - expect(router.push).toHaveBeenCalledTimes(1); - expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME }); - }); - }); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/pages/index_spec.js b/spec/frontend/design_management_legacy/pages/index_spec.js deleted file mode 100644 index fed1f986bad..00000000000 --- a/spec/frontend/design_management_legacy/pages/index_spec.js +++ /dev/null @@ -1,536 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { ApolloMutation } from 'vue-apollo'; -import VueRouter from 'vue-router'; -import { GlEmptyState } from '@gitlab/ui'; -import Index from '~/design_management_legacy/pages/index.vue'; -import uploadDesignQuery from '~/design_management_legacy/graphql/mutations/upload_design.mutation.graphql'; -import DesignDestroyer from '~/design_management_legacy/components/design_destroyer.vue'; -import DesignDropzone from '~/design_management_legacy/components/upload/design_dropzone.vue'; -import DeleteButton from '~/design_management_legacy/components/delete_button.vue'; -import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants'; -import { - EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE, - EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE, -} from '~/design_management_legacy/utils/error_messages'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import createRouter from '~/design_management_legacy/router'; -import * as utils from '~/design_management_legacy/utils/design_management_utils'; -import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_legacy/constants'; - -jest.mock('~/flash.js'); -const mockPageEl = { - classList: { - remove: jest.fn(), - }, -}; -jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageEl); - -const localVue = createLocalVue(); -const router = createRouter(); -localVue.use(VueRouter); - -const mockDesigns = [ - { - id: 'design-1', - image: 'design-1-image', - filename: 'design-1-name', - event: 'NONE', - notesCount: 0, - }, - { - id: 'design-2', - image: 'design-2-image', - filename: 'design-2-name', - event: 'NONE', - notesCount: 1, - }, - { - id: 'design-3', - image: 'design-3-image', - filename: 'design-3-name', - event: 'NONE', - notesCount: 0, - }, -]; - -const mockVersion = { - node: { - id: 'gid://gitlab/DesignManagement::Version/1', - }, -}; - -describe('Design management index page', () => { - let mutate; - let wrapper; - - const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox'); - const findSelectAllButton = () => wrapper.find('.js-select-all'); - const findToolbar = () => wrapper.find('.qa-selector-toolbar'); - const findDeleteButton = () => wrapper.find(DeleteButton); - const findDropzone = () => wrapper.findAll(DesignDropzone).at(0); - const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1); - - function createComponent({ - loading = false, - designs = [], - allVersions = [], - createDesign = true, - stubs = {}, - mockMutate = jest.fn().mockResolvedValue(), - } = {}) { - mutate = mockMutate; - const $apollo = { - queries: { - designs: { - loading, - }, - permissions: { - loading, - }, - }, - mutate, - }; - - wrapper = shallowMount(Index, { - mocks: { $apollo }, - localVue, - router, - stubs: { DesignDestroyer, ApolloMutation, ...stubs }, - attachToDocument: true, - }); - - wrapper.setData({ - designs, - allVersions, - issueIid: '1', - permissions: { - createDesign, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - describe('designs', () => { - it('renders loading icon', () => { - createComponent({ loading: true }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders error', () => { - createComponent(); - - wrapper.setData({ error: true }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders a toolbar with buttons when there are designs', () => { - createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); - - return wrapper.vm.$nextTick().then(() => { - expect(findToolbar().exists()).toBe(true); - }); - }); - - it('renders designs list and header with upload button', () => { - createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('does not render toolbar when there is no permission', () => { - createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); - }); - - describe('when has no designs', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders empty text', () => - wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - })); - - it('does not render a toolbar with buttons', () => - wrapper.vm.$nextTick().then(() => { - expect(findToolbar().exists()).toBe(false); - })); - }); - - describe('uploading designs', () => { - it('calls mutation on upload', () => { - createComponent({ stubs: { GlEmptyState } }); - - const mutationVariables = { - update: expect.anything(), - context: { - hasUpload: true, - }, - mutation: uploadDesignQuery, - variables: { - files: [{ name: 'test' }], - projectPath: '', - iid: '1', - }, - optimisticResponse: { - __typename: 'Mutation', - designManagementUpload: { - __typename: 'DesignManagementUploadPayload', - designs: [ - { - __typename: 'Design', - id: expect.anything(), - image: '', - imageV432x230: '', - filename: 'test', - fullPath: '', - event: 'NONE', - notesCount: 0, - diffRefs: { - __typename: 'DiffRefs', - baseSha: '', - startSha: '', - headSha: '', - }, - discussions: { - __typename: 'DesignDiscussion', - nodes: [], - }, - versions: { - __typename: 'DesignVersionConnection', - edges: { - __typename: 'DesignVersionEdge', - node: { - __typename: 'DesignVersion', - id: expect.anything(), - sha: expect.anything(), - }, - }, - }, - }, - ], - skippedDesigns: [], - errors: [], - }, - }, - }; - - return wrapper.vm.$nextTick().then(() => { - findDropzone().vm.$emit('change', [{ name: 'test' }]); - expect(mutate).toHaveBeenCalledWith(mutationVariables); - expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]); - expect(wrapper.vm.isSaving).toBeTruthy(); - }); - }); - - it('sets isSaving', () => { - createComponent(); - - const uploadDesign = wrapper.vm.onUploadDesign([ - { - name: 'test', - }, - ]); - - expect(wrapper.vm.isSaving).toBe(true); - - return uploadDesign.then(() => { - expect(wrapper.vm.isSaving).toBe(false); - }); - }); - - it('updates state appropriately after upload complete', () => { - createComponent({ stubs: { GlEmptyState } }); - wrapper.setData({ filesToBeSaved: [{ name: 'test' }] }); - - wrapper.vm.onUploadDesignDone(); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.filesToBeSaved).toEqual([]); - expect(wrapper.vm.isSaving).toBeFalsy(); - expect(wrapper.vm.isLatestVersion).toBe(true); - }); - }); - - it('updates state appropriately after upload error', () => { - createComponent({ stubs: { GlEmptyState } }); - wrapper.setData({ filesToBeSaved: [{ name: 'test' }] }); - - wrapper.vm.onUploadDesignError(); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.filesToBeSaved).toEqual([]); - expect(wrapper.vm.isSaving).toBeFalsy(); - expect(createFlash).toHaveBeenCalled(); - - createFlash.mockReset(); - }); - }); - - it('does not call mutation if createDesign is false', () => { - createComponent({ createDesign: false }); - - wrapper.vm.onUploadDesign([]); - - expect(mutate).not.toHaveBeenCalled(); - }); - - describe('upload count limit', () => { - const MAXIMUM_FILE_UPLOAD_LIMIT = 10; - - afterEach(() => { - createFlash.mockReset(); - }); - - it('does not warn when the max files are uploaded', () => { - createComponent(); - - wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT).fill(mockDesigns[0])); - - expect(createFlash).not.toHaveBeenCalled(); - }); - - it('warns when too many files are uploaded', () => { - createComponent(); - - wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT + 1).fill(mockDesigns[0])); - - expect(createFlash).toHaveBeenCalled(); - }); - }); - - it('flashes warning if designs are skipped', () => { - createComponent({ - mockMutate: () => - Promise.resolve({ - data: { designManagementUpload: { skippedDesigns: [{ filename: 'test.jpg' }] } }, - }), - }); - - const uploadDesign = wrapper.vm.onUploadDesign([ - { - name: 'test', - }, - ]); - - return uploadDesign.then(() => { - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith( - 'Upload skipped. test.jpg did not change.', - 'warning', - ); - }); - }); - - describe('dragging onto an existing design', () => { - beforeEach(() => { - createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); - }); - - it('calls onUploadDesign with valid upload', () => { - wrapper.setMethods({ - onUploadDesign: jest.fn(), - }); - - const mockUploadPayload = [ - { - name: mockDesigns[0].filename, - }, - ]; - - const designDropzone = findFirstDropzoneWithDesign(); - designDropzone.vm.$emit('change', mockUploadPayload); - - expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1); - expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith(mockUploadPayload); - }); - - it.each` - description | eventPayload | message - ${'> 1 file'} | ${[{ name: 'test' }, { name: 'test-2' }]} | ${EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE} - ${'different filename'} | ${[{ name: 'wrong-name' }]} | ${EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE} - `('calls createFlash when upload has $description', ({ eventPayload, message }) => { - const designDropzone = findFirstDropzoneWithDesign(); - designDropzone.vm.$emit('change', eventPayload); - - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith(message); - }); - }); - }); - - describe('on latest version when has designs', () => { - beforeEach(() => { - createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); - }); - - it('renders design checkboxes', () => { - expect(findDesignCheckboxes()).toHaveLength(mockDesigns.length); - }); - - it('renders toolbar buttons', () => { - expect(findToolbar().exists()).toBe(true); - expect(findToolbar().classes()).toContain('d-flex'); - expect(findToolbar().classes()).not.toContain('d-none'); - }); - - it('adds two designs to selected designs when their checkboxes are checked', () => { - findDesignCheckboxes() - .at(0) - .trigger('click'); - - return wrapper.vm - .$nextTick() - .then(() => { - findDesignCheckboxes() - .at(1) - .trigger('click'); - - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findDeleteButton().exists()).toBe(true); - expect(findSelectAllButton().text()).toBe('Deselect all'); - findDeleteButton().vm.$emit('deleteSelectedDesigns'); - const [{ variables }] = mutate.mock.calls[0]; - expect(variables.filenames).toStrictEqual([ - mockDesigns[0].filename, - mockDesigns[1].filename, - ]); - }); - }); - - it('adds all designs to selected designs when Select All button is clicked', () => { - findSelectAllButton().vm.$emit('click'); - - return wrapper.vm.$nextTick().then(() => { - expect(findDeleteButton().props().hasSelectedDesigns).toBe(true); - expect(findSelectAllButton().text()).toBe('Deselect all'); - expect(wrapper.vm.selectedDesigns).toEqual(mockDesigns.map(design => design.filename)); - }); - }); - - it('removes all designs from selected designs when at least one design was selected', () => { - findDesignCheckboxes() - .at(0) - .trigger('click'); - - return wrapper.vm - .$nextTick() - .then(() => { - findSelectAllButton().vm.$emit('click'); - }) - .then(() => { - expect(findDeleteButton().props().hasSelectedDesigns).toBe(false); - expect(findSelectAllButton().text()).toBe('Select all'); - expect(wrapper.vm.selectedDesigns).toEqual([]); - }); - }); - }); - - it('on latest version when has no designs does not render toolbar buttons', () => { - createComponent({ designs: [], allVersions: [mockVersion] }); - expect(findToolbar().exists()).toBe(false); - }); - - describe('on non-latest version', () => { - beforeEach(() => { - createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); - }); - - it('does not render design checkboxes', async () => { - await router.replace({ - name: DESIGNS_ROUTE_NAME, - query: { - version: '2', - }, - }); - expect(findDesignCheckboxes()).toHaveLength(0); - }); - - it('does not render Delete selected button', () => { - expect(findDeleteButton().exists()).toBe(false); - }); - - it('does not render Select All button', () => { - expect(findSelectAllButton().exists()).toBe(false); - }); - }); - - describe('pasting a design', () => { - let event; - beforeEach(() => { - createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); - - wrapper.setMethods({ - onUploadDesign: jest.fn(), - }); - - event = new Event('paste'); - }); - - it('calls onUploadDesign with valid paste', async () => { - event.clipboardData = { - files: [{ name: 'image.png', type: 'image/png' }], - getData: () => 'test.png', - }; - - document.dispatchEvent(event); - - expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1); - expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([ - new File([{ name: 'image.png' }], 'test.png'), - ]); - }); - - it('renames a design if it has an image.png filename', () => { - event.clipboardData = { - files: [{ name: 'image.png', type: 'image/png' }], - getData: () => 'image.png', - }; - - document.dispatchEvent(event); - - expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1); - expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([ - new File([{ name: 'image.png' }], `design_${Date.now()}.png`), - ]); - }); - - it('does not call onUploadDesign with invalid paste', () => { - event.clipboardData = { - items: [{ type: 'text/plain' }, { type: 'text' }], - files: [], - }; - - document.dispatchEvent(event); - - expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled(); - }); - }); - - describe('when navigating', () => { - it('ensures fullscreen layout is not applied', async () => { - createComponent(true); - - await wrapper.vm.$router.replace('/'); - await wrapper.vm.$router.replace('/designs'); - expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(2); - expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/router_spec.js b/spec/frontend/design_management_legacy/router_spec.js deleted file mode 100644 index 5f62793a243..00000000000 --- a/spec/frontend/design_management_legacy/router_spec.js +++ /dev/null @@ -1,82 +0,0 @@ -import { mount, createLocalVue } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import VueRouter from 'vue-router'; -import App from '~/design_management_legacy/components/app.vue'; -import Designs from '~/design_management_legacy/pages/index.vue'; -import DesignDetail from '~/design_management_legacy/pages/design/index.vue'; -import createRouter from '~/design_management_legacy/router'; -import { - ROOT_ROUTE_NAME, - DESIGNS_ROUTE_NAME, - DESIGN_ROUTE_NAME, -} from '~/design_management_legacy/router/constants'; -import '~/commons/bootstrap'; - -function factory(routeArg) { - const localVue = createLocalVue(); - localVue.use(VueRouter); - - window.gon = { sprite_icons: '' }; - - const router = createRouter('/'); - if (routeArg !== undefined) { - router.push(routeArg); - } - - return mount(App, { - localVue, - router, - mocks: { - $apollo: { - queries: { - designs: { loading: true }, - design: { loading: true }, - permissions: { loading: true }, - }, - mutate: jest.fn(), - }, - }, - }); -} - -jest.mock('mousetrap', () => ({ - bind: jest.fn(), - unbind: jest.fn(), -})); - -describe('Design management router', () => { - afterEach(() => { - window.location.hash = ''; - }); - - describe.each([['/'], [{ name: ROOT_ROUTE_NAME }]])('root route', routeArg => { - it('pushes home component', () => { - const wrapper = factory(routeArg); - - expect(wrapper.find(Designs).exists()).toBe(true); - }); - }); - - describe.each([['/designs'], [{ name: DESIGNS_ROUTE_NAME }]])('designs route', routeArg => { - it('pushes designs root component', () => { - const wrapper = factory(routeArg); - - expect(wrapper.find(Designs).exists()).toBe(true); - }); - }); - - describe.each([['/designs/1'], [{ name: DESIGN_ROUTE_NAME, params: { id: '1' } }]])( - 'designs detail route', - routeArg => { - it('pushes designs detail component', () => { - const wrapper = factory(routeArg); - - return nextTick().then(() => { - const detail = wrapper.find(DesignDetail); - expect(detail.exists()).toBe(true); - expect(detail.props('id')).toEqual('1'); - }); - }); - }, - ); -}); diff --git a/spec/frontend/design_management_legacy/utils/cache_update_spec.js b/spec/frontend/design_management_legacy/utils/cache_update_spec.js deleted file mode 100644 index dce91b5e59b..00000000000 --- a/spec/frontend/design_management_legacy/utils/cache_update_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import { InMemoryCache } from 'apollo-cache-inmemory'; -import { - updateStoreAfterDesignsDelete, - updateStoreAfterAddDiscussionComment, - updateStoreAfterAddImageDiffNote, - updateStoreAfterUploadDesign, - updateStoreAfterUpdateImageDiffNote, -} from '~/design_management_legacy/utils/cache_update'; -import { - designDeletionError, - ADD_DISCUSSION_COMMENT_ERROR, - ADD_IMAGE_DIFF_NOTE_ERROR, - UPDATE_IMAGE_DIFF_NOTE_ERROR, -} from '~/design_management_legacy/utils/error_messages'; -import design from '../mock_data/design'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; - -jest.mock('~/flash.js'); - -describe('Design Management cache update', () => { - const mockErrors = ['code red!']; - - let mockStore; - - beforeEach(() => { - mockStore = new InMemoryCache(); - }); - - describe('error handling', () => { - it.each` - fnName | subject | errorMessage | extraArgs - ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]} - ${'updateStoreAfterAddDiscussionComment'} | ${updateStoreAfterAddDiscussionComment} | ${ADD_DISCUSSION_COMMENT_ERROR} | ${[]} - ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]} - ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]} - ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]} - `('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => { - expect(createFlash).not.toHaveBeenCalled(); - expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow(); - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith(errorMessage); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/utils/design_management_utils_spec.js b/spec/frontend/design_management_legacy/utils/design_management_utils_spec.js deleted file mode 100644 index 97e85a24a35..00000000000 --- a/spec/frontend/design_management_legacy/utils/design_management_utils_spec.js +++ /dev/null @@ -1,176 +0,0 @@ -import { - extractCurrentDiscussion, - extractDiscussions, - findVersionId, - designUploadOptimisticResponse, - updateImageDiffNoteOptimisticResponse, - isValidDesignFile, - extractDesign, -} from '~/design_management_legacy/utils/design_management_utils'; -import mockResponseNoDesigns from '../mock_data/no_designs'; -import mockResponseWithDesigns from '../mock_data/designs'; -import mockDesign from '../mock_data/design'; - -jest.mock('lodash/uniqueId', () => () => 1); - -describe('extractCurrentDiscussion', () => { - let discussions; - - beforeEach(() => { - discussions = { - nodes: [ - { id: 101, payload: 'w' }, - { id: 102, payload: 'x' }, - { id: 103, payload: 'y' }, - { id: 104, payload: 'z' }, - ], - }; - }); - - it('finds the relevant discussion if it exists', () => { - const id = 103; - expect(extractCurrentDiscussion(discussions, id)).toEqual({ id, payload: 'y' }); - }); - - it('returns null if the relevant discussion does not exist', () => { - expect(extractCurrentDiscussion(discussions, 0)).not.toBeDefined(); - }); -}); - -describe('extractDiscussions', () => { - let discussions; - - beforeEach(() => { - discussions = { - nodes: [ - { id: 1, notes: { nodes: ['a'] } }, - { id: 2, notes: { nodes: ['b'] } }, - { id: 3, notes: { nodes: ['c'] } }, - { id: 4, notes: { nodes: ['d'] } }, - ], - }; - }); - - it('discards the edges.node artifacts of GraphQL', () => { - expect(extractDiscussions(discussions)).toEqual([ - { id: 1, notes: ['a'], index: 1 }, - { id: 2, notes: ['b'], index: 2 }, - { id: 3, notes: ['c'], index: 3 }, - { id: 4, notes: ['d'], index: 4 }, - ]); - }); -}); - -describe('version parser', () => { - it('correctly extracts version ID from a valid version string', () => { - const testVersionId = '123'; - const testVersionString = `gid://gitlab/DesignManagement::Version/${testVersionId}`; - - expect(findVersionId(testVersionString)).toEqual(testVersionId); - }); - - it('fails to extract version ID from an invalid version string', () => { - const testInvalidVersionString = `gid://gitlab/DesignManagement::Version`; - - expect(findVersionId(testInvalidVersionString)).toBeUndefined(); - }); -}); - -describe('optimistic responses', () => { - it('correctly generated for designManagementUpload', () => { - const expectedResponse = { - __typename: 'Mutation', - designManagementUpload: { - __typename: 'DesignManagementUploadPayload', - designs: [ - { - __typename: 'Design', - id: -1, - image: '', - imageV432x230: '', - filename: 'test', - fullPath: '', - notesCount: 0, - event: 'NONE', - diffRefs: { __typename: 'DiffRefs', baseSha: '', startSha: '', headSha: '' }, - discussions: { __typename: 'DesignDiscussion', nodes: [] }, - versions: { - __typename: 'DesignVersionConnection', - edges: { - __typename: 'DesignVersionEdge', - node: { __typename: 'DesignVersion', id: -1, sha: -1 }, - }, - }, - }, - ], - errors: [], - skippedDesigns: [], - }, - }; - expect(designUploadOptimisticResponse([{ name: 'test' }])).toEqual(expectedResponse); - }); - - it('correctly generated for updateImageDiffNoteOptimisticResponse', () => { - const mockNote = { - id: 'test-note-id', - }; - - const mockPosition = { - x: 10, - y: 10, - width: 10, - height: 10, - }; - - const expectedResponse = { - __typename: 'Mutation', - updateImageDiffNote: { - __typename: 'UpdateImageDiffNotePayload', - note: { - ...mockNote, - position: mockPosition, - }, - errors: [], - }, - }; - expect(updateImageDiffNoteOptimisticResponse(mockNote, { position: mockPosition })).toEqual( - expectedResponse, - ); - }); -}); - -describe('isValidDesignFile', () => { - // test every filetype that Design Management supports - // https://docs.gitlab.com/ee/user/project/issues/design_management.html#limitations - it.each` - mimetype | isValid - ${'image/svg'} | ${true} - ${'image/png'} | ${true} - ${'image/jpg'} | ${true} - ${'image/jpeg'} | ${true} - ${'image/gif'} | ${true} - ${'image/bmp'} | ${true} - ${'image/tiff'} | ${true} - ${'image/ico'} | ${true} - ${'image/svg'} | ${true} - ${'video/mpeg'} | ${false} - ${'audio/midi'} | ${false} - ${'application/octet-stream'} | ${false} - `('returns $isValid for file type $mimetype', ({ mimetype, isValid }) => { - expect(isValidDesignFile({ type: mimetype })).toBe(isValid); - }); -}); - -describe('extractDesign', () => { - describe('with no designs', () => { - it('returns undefined', () => { - expect(extractDesign(mockResponseNoDesigns)).toBeUndefined(); - }); - }); - - describe('with designs', () => { - it('returns the first design available', () => { - expect(extractDesign(mockResponseWithDesigns)).toEqual(mockDesign); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/utils/error_messages_spec.js b/spec/frontend/design_management_legacy/utils/error_messages_spec.js deleted file mode 100644 index 489ac23da4e..00000000000 --- a/spec/frontend/design_management_legacy/utils/error_messages_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import { - designDeletionError, - designUploadSkippedWarning, -} from '~/design_management_legacy/utils/error_messages'; - -const mockFilenames = n => - Array(n) - .fill(0) - .map((_, i) => ({ filename: `${i + 1}.jpg` })); - -describe('Error message', () => { - describe('designDeletionError', () => { - const singularMsg = 'Could not delete a design. Please try again.'; - const pluralMsg = 'Could not delete designs. Please try again.'; - - describe('when [singular=true]', () => { - it.each([[undefined], [true]])('uses singular grammar', singularOption => { - expect(designDeletionError({ singular: singularOption })).toEqual(singularMsg); - }); - }); - - describe('when [singular=false]', () => { - it('uses plural grammar', () => { - expect(designDeletionError({ singular: false })).toEqual(pluralMsg); - }); - }); - }); - - describe.each([ - [[], [], null], - [mockFilenames(1), mockFilenames(1), 'Upload skipped. 1.jpg did not change.'], - [ - mockFilenames(2), - mockFilenames(2), - 'Upload skipped. The designs you tried uploading did not change.', - ], - [ - mockFilenames(2), - mockFilenames(1), - 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg.', - ], - [ - mockFilenames(6), - mockFilenames(5), - 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg.', - ], - [ - mockFilenames(7), - mockFilenames(6), - 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 1 more.', - ], - [ - mockFilenames(8), - mockFilenames(7), - 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 2 more.', - ], - ])('designUploadSkippedWarning', (uploadedFiles, skippedFiles, expected) => { - it('returns expected warning message', () => { - expect(designUploadSkippedWarning(uploadedFiles, skippedFiles)).toBe(expected); - }); - }); -}); diff --git a/spec/frontend/design_management_legacy/utils/tracking_spec.js b/spec/frontend/design_management_legacy/utils/tracking_spec.js deleted file mode 100644 index a59cf80c906..00000000000 --- a/spec/frontend/design_management_legacy/utils/tracking_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import { mockTracking } from 'helpers/tracking_helper'; -import { trackDesignDetailView } from '~/design_management_legacy/utils/tracking'; - -function getTrackingSpy(key) { - return mockTracking(key, undefined, jest.spyOn); -} - -describe('Tracking Events', () => { - describe('trackDesignDetailView', () => { - const eventKey = 'projects:issues:design'; - const eventName = 'view_design'; - - it('trackDesignDetailView fires a tracking event when called', () => { - const trackingSpy = getTrackingSpy(eventKey); - - trackDesignDetailView(); - - expect(trackingSpy).toHaveBeenCalledWith( - eventKey, - eventName, - expect.objectContaining({ - label: eventName, - context: { - schema: expect.any(String), - data: { - 'design-version-number': 1, - 'design-is-current-version': false, - 'internal-object-referrer': '', - 'design-collection-owner': '', - }, - }, - }), - ); - }); - - it('trackDesignDetailView allows to customize the value payload', () => { - const trackingSpy = getTrackingSpy(eventKey); - - trackDesignDetailView('from-a-test', 'test', 100, true); - - expect(trackingSpy).toHaveBeenCalledWith( - eventKey, - eventName, - expect.objectContaining({ - label: eventName, - context: { - schema: expect.any(String), - data: { - 'design-version-number': 100, - 'design-is-current-version': true, - 'internal-object-referrer': 'from-a-test', - 'design-collection-owner': 'test', - }, - }, - }), - ); - }); - }); -}); diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js index a80c9c25dbf..702937d61a7 100644 --- a/spec/frontend/incidents/components/incidents_list_spec.js +++ b/spec/frontend/incidents/components/incidents_list_spec.js @@ -78,6 +78,7 @@ describe('Incidents List', () => { stubs: { GlButton: true, GlAvatar: true, + GlEmptyState: true, }, }); } @@ -96,12 +97,30 @@ describe('Incidents List', () => { expect(findLoader().exists()).toBe(true); }); - it('shows empty state', () => { - mountComponent({ - data: { incidents: { list: [] }, incidentsCount: {} }, - loading: false, - }); - expect(findEmptyState().exists()).toBe(true); + describe('empty state', () => { + const { + emptyState: { title, emptyClosedTabTitle, description }, + } = I18N; + + it.each` + statusFilter | all | closed | expectedTitle | expectedDescription + ${'all'} | ${2} | ${1} | ${title} | ${description} + ${'open'} | ${2} | ${0} | ${title} | ${description} + ${'closed'} | ${0} | ${0} | ${title} | ${description} + ${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${undefined} + `( + `when active tab is $statusFilter and there are $all incidents in total and $closed closed incidents, the empty state + has title: $expectedTitle and description: $expectedDescription`, + ({ statusFilter, all, closed, expectedTitle, expectedDescription }) => { + mountComponent({ + data: { incidents: { list: [] }, incidentsCount: { all, closed }, statusFilter }, + loading: false, + }); + expect(findEmptyState().exists()).toBe(true); + expect(findEmptyState().attributes('title')).toBe(expectedTitle); + expect(findEmptyState().attributes('description')).toBe(expectedDescription); + }, + ); }); it('shows error state', () => { @@ -188,6 +207,14 @@ describe('Incidents List', () => { expect(findCreateIncidentBtn().attributes('loading')).toBe('true'); }); }); + + it("doesn't show the button when list is empty", () => { + mountComponent({ + data: { incidents: { list: [] }, incidentsCount: {} }, + loading: false, + }); + expect(findCreateIncidentBtn().exists()).toBe(false); + }); }); describe('Pagination', () => { @@ -313,7 +340,7 @@ describe('Incidents List', () => { describe('Status Filter Tabs', () => { beforeEach(() => { mountComponent({ - data: { incidents: mockIncidents, incidentsCount }, + data: { incidents: { list: mockIncidents }, incidentsCount }, loading: false, stubs: { GlTab: true, @@ -345,7 +372,7 @@ describe('Incidents List', () => { describe('sorting the incident list by column', () => { beforeEach(() => { mountComponent({ - data: { incidents: mockIncidents, incidentsCount }, + data: { incidents: { list: mockIncidents }, incidentsCount }, loading: false, }); }); diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js index 9db03c49994..53a20ac69cb 100644 --- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js +++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js @@ -1,7 +1,11 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { shallowMount } from '@vue/test-utils'; -import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui'; +import { + GlEmptyState, + GlPagination, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, +} from '@gitlab/ui'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'helpers/test_constants'; import { deprecatedCreateFlash as flash } from '~/flash'; diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js index e757fe98661..502a1053663 100644 --- a/spec/frontend/releases/components/app_show_spec.js +++ b/spec/frontend/releases/components/app_show_spec.js @@ -1,6 +1,6 @@ import Vuex from 'vuex'; import { shallowMount } from '@vue/test-utils'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import ReleaseShowApp from '~/releases/components/app_show.vue'; import { release as originalRelease } from '../mock_data'; import ReleaseBlock from '~/releases/components/release_block.vue'; diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js index 6f3e12a0789..1b8bbd5af6b 100644 --- a/spec/frontend/repository/components/table/index_spec.js +++ b/spec/frontend/repository/components/table/index_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlSkeletonLoading, GlButton } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui'; import Table from '~/repository/components/table/index.vue'; import TableRow from '~/repository/components/table/row.vue'; diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js index 7dfb265c035..7fe6b44ecc7 100644 --- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js +++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js @@ -1,4 +1,4 @@ -import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { invalidPlanWithName, plans, validPlanWithName } from './mock_data'; diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index cd85a097460..b43bb6b10e0 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -1,4 +1,4 @@ -import { GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; diff --git a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb deleted file mode 100644 index efa88d53f36..00000000000 --- a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do - describe '#process' do - subject { described_class.new } - - context 'when there is no GRPC exception' do - let(:data) { { fingerprint: ['ArgumentError', 'Missing arguments'] } } - - it 'leaves data unchanged' do - expect(subject.process(data)).to eq(data) - end - end - - context 'when there is a GPRC exception with a debug string' do - let(:data) do - { - exception: { - values: [ - { - value: "GRPC::DeadlineExceeded: 4:DeadlineExceeded. debug_error_string:{\"hello\":1}" - } - ] - }, - extra: { - caller: 'test' - }, - message: "GRPC::DeadlineExceeded: 4:DeadlineExceeded. debug_error_string:{\"hello\":1}", - fingerprint: [ - "GRPC::DeadlineExceeded", - "4:Deadline Exceeded. debug_error_string:{\"created\":\"@1598938192.005782000\",\"description\":\"Error received from peer unix:/home/git/gitalypraefect.socket\",\"file\":\"src/core/lib/surface/call.cc\",\"file_line\":1055,\"grpc_message\":\"Deadline Exceeded\",\"grpc_status\":4}" - ] - } - end - - let(:expected) do - { - message: "GRPC::DeadlineExceeded: 4:DeadlineExceeded.", - fingerprint: [ - "GRPC::DeadlineExceeded", - "4:Deadline Exceeded." - ], - exception: { - values: [ - { - value: "GRPC::DeadlineExceeded: 4:DeadlineExceeded." - } - ] - }, - extra: { - caller: 'test', - grpc_debug_error_string: "{\"hello\":1}" - } - } - end - - it 'removes the debug error string and stores it as an extra field' do - expect(subject.process(data)).to eq(expected) - end - - context 'with no custom fingerprint' do - before do - data.delete(:fingerprint) - expected.delete(:fingerprint) - end - - it 'removes the debug error string and stores it as an extra field' do - expect(subject.process(data)).to eq(expected) - end - end - end - end -end diff --git a/spec/lib/gitlab/git/base_error_spec.rb b/spec/lib/gitlab/git/base_error_spec.rb new file mode 100644 index 00000000000..851cfa16512 --- /dev/null +++ b/spec/lib/gitlab/git/base_error_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +RSpec.describe Gitlab::Git::BaseError do + using RSpec::Parameterized::TableSyntax + + subject { described_class.new(message).to_s } + + where(:message, :result) do + "GRPC::DeadlineExceeded: 4:DeadlineExceeded. debug_error_string:{\"hello\":1}" | "GRPC::DeadlineExceeded: 4:DeadlineExceeded." + "GRPC::DeadlineExceeded: 4:DeadlineExceeded." | "GRPC::DeadlineExceeded: 4:DeadlineExceeded." + "GRPC::DeadlineExceeded: 4:DeadlineExceeded. debug_error_string:{\"created\":\"@1598978902.544524530\",\"description\":\"Error received from peer ipv4: debug_error_string:test\"}" | "GRPC::DeadlineExceeded: 4:DeadlineExceeded." + "9:Multiple lines\nTest line. debug_error_string:{\"created\":\"@1599074877.106467000\"}" | "9:Multiple lines\nTest line." + "other message" | "other message" + nil | "Gitlab::Git::BaseError" + end + + with_them do + it { is_expected.to eq(result) } + end +end diff --git a/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb new file mode 100644 index 00000000000..8f9a3e0cd9e --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::KubernetesAgentCounter do + it_behaves_like 'a redis usage counter', 'Kubernetes Agent', :gitops_sync + + it_behaves_like 'a redis usage counter with totals', :kubernetes_agent, gitops_sync: 1 + + describe '.increment_gitops_sync' do + it 'increments the gtops_sync counter by the new increment amount' do + described_class.increment_gitops_sync(7) + described_class.increment_gitops_sync(2) + described_class.increment_gitops_sync(0) + + expect(described_class.totals).to eq(kubernetes_agent_gitops_sync: 9) + end + + it 'raises for negative numbers' do + expect { described_class.increment_gitops_sync(-1) }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb index be528b081c5..d4f6110b3df 100644 --- a/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/redis_counter_spec.rb @@ -11,23 +11,47 @@ RSpec.describe Gitlab::UsageDataCounters::RedisCounter, :clean_gitlab_redis_shar stub_application_setting(usage_ping_enabled: setting_value) end - context 'when usage_ping is disabled' do - let(:setting_value) { false } + describe '.increment' do + context 'when usage_ping is disabled' do + let(:setting_value) { false } + + it 'counter is not increased' do + expect do + subject.increment(redis_key) + end.not_to change { subject.total_count(redis_key) } + end + end + + context 'when usage_ping is enabled' do + let(:setting_value) { true } - it 'counter is not increased' do - expect do - subject.increment(redis_key) - end.not_to change { subject.total_count(redis_key) } + it 'counter is increased' do + expect do + subject.increment(redis_key) + end.to change { subject.total_count(redis_key) }.by(1) + end end end - context 'when usage_ping is enabled' do - let(:setting_value) { true } + describe '.increment_by' do + context 'when usage_ping is disabled' do + let(:setting_value) { false } + + it 'counter is not increased' do + expect do + subject.increment_by(redis_key, 3) + end.not_to change { subject.total_count(redis_key) } + end + end + + context 'when usage_ping is enabled' do + let(:setting_value) { true } - it 'counter is increased' do - expect do - subject.increment(redis_key) - end.to change { subject.total_count(redis_key) }.by(1) + it 'counter is increased' do + expect do + subject.increment_by(redis_key, 3) + end.to change { subject.total_count(redis_key) }.by(3) + end end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index dc7bfc6e870..c80e15055c6 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -575,6 +575,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end + describe '.usage_counters' do + subject { described_class.usage_counters } + + it { is_expected.to include(:kubernetes_agent_gitops_sync) } + end + describe '.usage_data_counters' do subject { described_class.usage_data_counters } diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index a802e3a858a..ae5b6a9c4c6 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -15,11 +15,7 @@ RSpec.describe API::Internal::Kubernetes do allow(Gitlab::Kas).to receive(:secret).and_return(jwt_secret) end - describe "GET /internal/kubernetes/agent_info" do - def send_request(headers: {}, params: {}) - get api('/internal/kubernetes/agent_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers) - end - + shared_examples 'authorization' do context 'not authenticated' do it 'returns 401' do send_request(headers: { Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => '' }) @@ -28,6 +24,20 @@ RSpec.describe API::Internal::Kubernetes do end end + context 'authenticated' do + it 'returns 403 if Authorization header not sent' do + send_request + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 404 if Authorization is for non-existent agent' do + send_request(headers: { 'Authorization' => 'Bearer NONEXISTENT' }) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + context 'kubernetes_agent_internal_api feature flag disabled' do before do stub_feature_flags(kubernetes_agent_internal_api: false) @@ -39,12 +49,50 @@ RSpec.describe API::Internal::Kubernetes do expect(response).to have_gitlab_http_status(:not_found) end end + end + + describe 'POST /internal/kubernetes/usage_metrics' do + def send_request(headers: {}, params: {}) + post api('/internal/kubernetes/usage_metrics'), params: params, headers: headers.reverse_merge(jwt_auth_headers) + end - it 'returns 403 if Authorization header not sent' do - send_request + include_examples 'authorization' - expect(response).to have_gitlab_http_status(:forbidden) + context 'is authenticated for an agent' do + let!(:agent_token) { create(:cluster_agent_token) } + + it 'returns no_content for valid gitops_sync_count' do + send_request(params: { gitops_sync_count: 10 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) + + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'returns no_content 0 gitops_sync_count' do + send_request(params: { gitops_sync_count: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) + + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'returns 400 for non number' do + send_request(params: { gitops_sync_count: 'string' }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 400 for negative number' do + send_request(params: { gitops_sync_count: '-1' }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) + + expect(response).to have_gitlab_http_status(:bad_request) + end end + end + + describe "GET /internal/kubernetes/agent_info" do + def send_request(headers: {}, params: {}) + get api('/internal/kubernetes/agent_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers) + end + + include_examples 'authorization' context 'an agent is found' do let!(:agent_token) { create(:cluster_agent_token) } @@ -77,14 +125,6 @@ RSpec.describe API::Internal::Kubernetes do ) end end - - context 'no such agent exists' do - it 'returns 404' do - send_request(headers: { 'Authorization' => 'Bearer ABCD' }) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end end describe 'GET /internal/kubernetes/project_info' do @@ -92,39 +132,7 @@ RSpec.describe API::Internal::Kubernetes do get api('/internal/kubernetes/project_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers) end - context 'not authenticated' do - it 'returns 401' do - send_request(headers: { Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => '' }) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'kubernetes_agent_internal_api feature flag disabled' do - before do - stub_feature_flags(kubernetes_agent_internal_api: false) - end - - it 'returns 404' do - send_request - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - it 'returns 403 if Authorization header not sent' do - send_request - - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'no such agent exists' do - it 'returns 404' do - send_request(headers: { 'Authorization' => 'Bearer ABCD' }) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end + include_examples 'authorization' context 'an agent is found' do let!(:agent_token) { create(:cluster_agent_token) } |