diff options
Diffstat (limited to 'app/assets/javascripts/work_items/components')
33 files changed, 552 insertions, 351 deletions
diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue index 7903adea9bd..31cfe387b6e 100644 --- a/app/assets/javascripts/work_items/components/notes/system_note.vue +++ b/app/assets/javascripts/work_items/components/notes/system_note.vue @@ -26,6 +26,11 @@ import { __ } from '~/locale'; import NoteHeader from '~/notes/components/note_header.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +const ALLOWED_ICONS = ['issue-close']; +const ICON_COLORS = { + 'issue-close': 'gl-bg-blue-100! gl-text-blue-700', +}; + export default { i18n: { deleteButtonLabel: __('Remove description history'), @@ -66,6 +71,12 @@ export default { noteAnchorId() { return `note_${this.noteId}`; }, + getIconColor() { + return ICON_COLORS[this.note.systemNoteIconName] || ''; + }, + isAllowedIcon() { + return ALLOWED_ICONS.includes(this.note.systemNoteIconName); + }, isTargetNote() { return this.targetNoteHash === this.noteAnchorId; }, @@ -102,9 +113,16 @@ export default { class="note system-note note-wrapper" > <div - class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600" + :class="[ + getIconColor, + { + 'gl-bg-gray-50 gl-text-gray-600 system-note-icon': isAllowedIcon, + 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon, + }, + ]" + class="gl-float-left gl--flex-center gl-rounded-full gl-relative" > - <gl-icon :name="note.systemNoteIconName" /> + <gl-icon v-if="isAllowedIcon" :size="12" :name="note.systemNoteIconName" /> </div> <div class="timeline-content"> <div class="note-header"> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue index c867e53dc30..c3b7b7a2953 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue @@ -1,5 +1,5 @@ <script> -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import Tracking from '~/tracking'; import { ASC } from '~/notes/constants'; import { __ } from '~/locale'; @@ -105,7 +105,7 @@ export default { }; }, update(data) { - return data.workspace.workItems.nodes[0]; + return data.workspace.workItems.nodes[0] ?? {}; }, skip() { return !this.workItemIid; @@ -150,13 +150,13 @@ export default { }; }, isProjectArchived() { - return this.workItem?.project?.archived; + return this.workItem.archived; }, canCreateNote() { - return this.workItem?.userPermissions?.createNote; + return this.workItem.userPermissions?.createNote; }, workItemState() { - return this.workItem?.state; + return this.workItem.state; }, commentButtonText() { return this.isNewDiscussion ? __('Comment') : __('Reply'); diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue index c7d8a50f402..1e6bd9ff1ac 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue @@ -8,7 +8,7 @@ import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; -import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue'; +import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue'; import CommentFieldLayout from '~/notes/components/comment_field_layout.vue'; export default { @@ -29,7 +29,7 @@ export default { MarkdownEditor, GlFormCheckbox, GlIcon, - WorkItemStateToggleButton, + WorkItemStateToggle, }, directives: { GlTooltip: GlTooltipDirective, @@ -195,7 +195,6 @@ export default { :autocomplete-data-sources="autocompleteDataSources" :form-field-props="formFieldProps" :add-spacing-classes="false" - data-testid="work-item-add-comment" use-bottom-toolbar supports-quick-actions :autofocus="autofocus" @@ -230,7 +229,7 @@ export default { @click="$emit('submitForm', { commentText, isNoteInternal })" >{{ commentButtonTextComputed }} </gl-button> - <work-item-state-toggle-button + <work-item-state-toggle v-if="isNewDiscussion" class="gl-ml-3" :work-item-id="workItemId" diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue index f4c654f054c..11aecc65803 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue @@ -1,6 +1,6 @@ <script> import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import toast from '~/vue_shared/plugins/global_toast'; import { __ } from '~/locale'; import Tracking from '~/tracking'; @@ -96,6 +96,7 @@ export default { data() { return { isEditing: false, + workItem: {}, }; }, computed: { @@ -163,13 +164,13 @@ export default { return this.authorId === this.currentUserId; }, isWorkItemAuthor() { - return getIdFromGraphQLId(this.workItem?.author?.id) === this.authorId; + return getIdFromGraphQLId(this.workItem.author?.id) === this.authorId; }, projectName() { - return this.workItem?.project?.name; + return this.workItem.namespace?.name; }, isWorkItemConfidential() { - return this.workItem?.confidential; + return this.workItem.confidential; }, }, apollo: { @@ -184,7 +185,7 @@ export default { }; }, update(data) { - return data.workspace?.workItems?.nodes[0]; + return data.workspace?.workItems?.nodes[0] ?? {}; }, skip() { return !this.workItemIid; diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue index 2cdf8b5ea9d..cb9a560f9e1 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue @@ -5,7 +5,7 @@ import { GlDisclosureDropdown, GlDisclosureDropdownItem, } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { __, sprintf } from '~/locale'; import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; @@ -207,7 +207,6 @@ export default { <gl-button v-if="showEdit" v-gl-tooltip - data-testid="edit-work-item-note" data-track-action="click_button" data-track-label="edit_button" category="tertiary" @@ -219,7 +218,6 @@ export default { <gl-disclosure-dropdown ref="dropdown" v-gl-tooltip - data-testid="work-item-note-actions" icon="ellipsis_v" text-sr-only placement="right" diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue index 17d22e66530..75a8a7b29c0 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue @@ -1,5 +1,5 @@ <script> -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import AwardsList from '~/vue_shared/components/awards_list.vue'; import { getMutation, optimisticAwardUpdate } from '../../notes/award_utils'; diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue index bccbec903b4..e073fddeddb 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue @@ -27,5 +27,8 @@ export default { </script> <template> - <div v-safe-html="signedOutText" class="disabled-comment gl-text-center gl-relative"></div> + <div + v-safe-html="signedOutText" + class="disabled-comment gl-text-center gl-text-secondary gl-relative" + ></div> </template> diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue index 49813edf6fc..cbe7de4abcd 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue @@ -1,6 +1,6 @@ <script> -import { GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlLabel, GlLink, GlIcon, GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; import { isScopedLabel } from '~/lib/utils/common_utils'; import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue'; @@ -15,21 +15,21 @@ import { WIDGET_TYPE_LABELS, WORK_ITEM_NAME_TO_ICON_MAP, } from '../../constants'; -import WorkItemLinksMenu from './work_item_links_menu.vue'; export default { i18n: { confidential: __('Confidential'), created: __('Created'), closed: __('Closed'), + remove: s__('WorkItem|Remove'), }, components: { GlLabel, GlLink, GlIcon, + GlButton, RichTimestampTooltip, WorkItemLinkChildMetadata, - WorkItemLinksMenu, }, directives: { GlTooltip: GlTooltipDirective, @@ -52,6 +52,16 @@ export default { required: false, default: false, }, + showLabels: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + isFocused: false, + }; }, computed: { labels() { @@ -106,6 +116,12 @@ export default { } return false; }, + showRemove() { + return this.canUpdate && this.isFocused; + }, + displayLabels() { + return this.showLabels && this.labels.length; + }, }, methods: { showScopedLabel(label) { @@ -117,8 +133,12 @@ export default { <template> <div - class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base" + class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base gl-gap-3" data-testid="links-child" + @mouseover="isFocused = true" + @mouseleave="isFocused = false" + @focusin="isFocused = true" + @focusout="isFocused = false" > <div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0"> <div @@ -168,7 +188,7 @@ export default { class="gl-ml-6 ml-xl-0" /> </div> - <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6"> + <div v-if="displayLabels" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6"> <gl-label v-for="label in labels" :key="label.id" @@ -181,10 +201,16 @@ export default { /> </div> </div> - <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex"> - <work-item-links-menu - data-testid="links-menu" - @removeChild="$emit('removeChild', childItem)" + <div v-if="canUpdate"> + <gl-button + :class="{ 'gl-visibility-visible': showRemove }" + class="gl-visibility-hidden" + category="tertiary" + size="small" + icon="close" + :aria-label="$options.i18n.remove" + data-testid="remove-work-item-link" + @click="$emit('removeChild', childItem)" /> </div> </div> diff --git a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue deleted file mode 100644 index 12b7bade31d..00000000000 --- a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue +++ /dev/null @@ -1,28 +0,0 @@ -<script> -import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; - -export default { - components: { - GlDisclosureDropdownItem, - GlDisclosureDropdown, - }, -}; -</script> - -<template> - <div class="gl-ml-5"> - <gl-disclosure-dropdown - category="tertiary" - toggle-class="btn-icon btn-sm" - icon="ellipsis_v" - data-testid="work_items_links_menu" - :aria-label="__(`More actions`)" - text-sr-only - no-caret - > - <gl-disclosure-dropdown-item @action="$emit('removeChild')"> - <template #list-item>{{ s__('WorkItem|Remove') }}</template> - </gl-disclosure-dropdown-item> - </gl-disclosure-dropdown> - </div> -</template> diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue index 3595ab631df..c122db6c902 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue @@ -1,20 +1,29 @@ <script> -import { GlTokenSelector } from '@gitlab/ui'; +import { GlTokenSelector, GlAlert } from '@gitlab/ui'; import { debounce } from 'lodash'; + import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { isNumeric } from '~/lib/utils/number_utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { highlighter } from 'ee_else_ce/gfm_auto_complete'; +import groupWorkItemsQuery from '../../graphql/group_work_items.query.graphql'; import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql'; import { WORK_ITEMS_TYPE_MAP, I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, + I18N_WORK_ITEM_SEARCH_ERROR, sprintfWorkItem, } from '../../constants'; export default { components: { GlTokenSelector, + GlAlert, }, + directives: { SafeHtml }, + inject: ['isGroup'], props: { value: { type: Array, @@ -47,30 +56,37 @@ export default { }, apollo: { availableWorkItems: { - query: projectWorkItemsQuery, + query() { + return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery; + }, variables() { return { fullPath: this.fullPath, - searchTerm: this.search?.title || this.search, + searchTerm: '', types: this.childrenType ? [this.childrenType] : [], - in: this.search ? 'TITLE' : undefined, + isNumber: false, }; }, skip() { return !this.searchStarted; }, update(data) { - return data.workspace.workItems.nodes.filter( - (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id, - ); + return [ + ...this.filterItems(data.workspace.workItemsByIid?.nodes), + ...this.filterItems(data.workspace.workItems.nodes), + ]; + }, + error() { + this.error = sprintfWorkItem(I18N_WORK_ITEM_SEARCH_ERROR, this.childrenTypeName); }, }, }, data() { return { availableWorkItems: [], - search: '', + query: '', searchStarted: false, + error: '', }; }, computed: { @@ -101,7 +117,24 @@ export default { methods: { getIdFromGraphQLId, setSearchKey(value) { - this.search = value; + this.query = value; + + // Query parameters for searching by text + const variables = { + searchTerm: value, + in: value ? 'TITLE' : undefined, + iid: null, + isNumber: false, + }; + + // Check if it is a number, add iid as query parameter + if (isNumeric(value) && value) { + variables.iid = value; + variables.isNumber = true; + } + + // Fetch combined results of search by iid and search by title. + this.$apollo.queries.availableWorkItems.refetch(variables); }, handleFocus() { this.searchStarted = true; @@ -125,33 +158,58 @@ export default { } }); }, + formatResults(input) { + if (!this.query) { + return input; + } + + return highlighter(`<span class="gl-text-black-normal">${input}</span>`, this.query); + }, + unsetError() { + this.error = ''; + }, + filterItems(items) { + return ( + items?.filter( + (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id, + ) || [] + ); + }, }, }; </script> <template> - <gl-token-selector - ref="tokenSelector" - v-model="workItemsToAdd" - :dropdown-items="availableWorkItems" - :loading="isLoading" - :placeholder="addInputPlaceholder" - menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!" - :container-class="tokenSelectorContainerClass" - data-testid="work-item-token-select-input" - @text-input="debouncedSearchKeyUpdate" - @focus="handleFocus" - @mouseover.native="handleMouseOver" - @mouseout.native="handleMouseOut" - @token-add="focusInputText" - @token-remove="focusInputText" - @blur="handleBlur" - > - <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template> - <template #dropdown-item-content="{ dropdownItem }"> - <div class="gl-display-flex"> - <div class="gl-text-secondary gl-font-sm gl-mr-4">{{ dropdownItem.iid }}</div> - <div class="gl-text-truncate">{{ dropdownItem.title }}</div> - </div> - </template> - </gl-token-selector> + <div> + <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError"> + {{ error }} + </gl-alert> + <gl-token-selector + ref="tokenSelector" + v-model="workItemsToAdd" + :dropdown-items="availableWorkItems" + :loading="isLoading" + :placeholder="addInputPlaceholder" + menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!" + :container-class="tokenSelectorContainerClass" + data-testid="work-item-token-select-input" + @text-input="debouncedSearchKeyUpdate" + @focus="handleFocus" + @mouseover.native="handleMouseOver" + @mouseout.native="handleMouseOut" + @token-add="focusInputText" + @token-remove="focusInputText" + @blur="handleBlur" + > + <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template> + <template #dropdown-item-content="{ dropdownItem }"> + <div class="gl-display-flex"> + <div + v-safe-html="formatResults(dropdownItem.iid)" + class="gl-text-secondary gl-font-sm gl-mr-4" + ></div> + <div v-safe-html="formatResults(dropdownItem.title)" class="gl-text-truncate"></div> + </div> + </template> + </gl-token-selector> + </div> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue index 02d2ea24ca0..0a71fbc9a34 100644 --- a/app/assets/javascripts/work_items/components/work_item_actions.vue +++ b/app/assets/javascripts/work_items/components/work_item_actions.vue @@ -8,7 +8,7 @@ import { GlToggle, } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { __, s__ } from '~/locale'; import Tracking from '~/tracking'; @@ -20,12 +20,12 @@ import { I18N_WORK_ITEM_DELETE, I18N_WORK_ITEM_ARE_YOU_SURE_DELETE, TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION, - TEST_ID_NOTIFICATIONS_TOGGLE_ACTION, TEST_ID_NOTIFICATIONS_TOGGLE_FORM, TEST_ID_DELETE_ACTION, TEST_ID_PROMOTE_ACTION, TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION, TEST_ID_COPY_REFERENCE_ACTION, + TEST_ID_TOGGLE_ACTION, I18N_WORK_ITEM_ERROR_CONVERTING, WORK_ITEM_TYPE_VALUE_KEY_RESULT, WORK_ITEM_TYPE_VALUE_OBJECTIVE, @@ -36,11 +36,12 @@ import { import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql'; import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql'; import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql'; +import WorkItemStateToggle from './work_item_state_toggle.vue'; export default { i18n: { - enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'), - disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'), + enableConfidentiality: s__('WorkItem|Turn on confidentiality'), + disableConfidentiality: s__('WorkItem|Turn off confidentiality'), notifications: s__('WorkItem|Notifications'), notificationOn: s__('WorkItem|Notifications turned on.'), notificationOff: s__('WorkItem|Notifications turned off.'), @@ -54,25 +55,30 @@ export default { GlDropdownDivider, GlModal, GlToggle, + WorkItemStateToggle, }, directives: { GlModal: GlModalDirective, }, mixins: [Tracking.mixin({ label: 'actions_menu' })], isLoggedIn: isLoggedIn(), - notificationsToggleTestId: TEST_ID_NOTIFICATIONS_TOGGLE_ACTION, notificationsToggleFormTestId: TEST_ID_NOTIFICATIONS_TOGGLE_FORM, confidentialityTestId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION, copyReferenceTestId: TEST_ID_COPY_REFERENCE_ACTION, copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION, deleteActionTestId: TEST_ID_DELETE_ACTION, promoteActionTestId: TEST_ID_PROMOTE_ACTION, + stateToggleTestId: TEST_ID_TOGGLE_ACTION, inject: ['isGroup'], props: { fullPath: { type: String, required: true, }, + workItemState: { + type: String, + required: true, + }, workItemId: { type: String, required: false, @@ -128,6 +134,11 @@ export default { required: false, default: false, }, + workItemParentId: { + type: String, + required: false, + default: null, + }, }, apollo: { workItemTypes: { @@ -165,6 +176,11 @@ export default { canPromoteToObjective() { return this.canUpdate && this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT; }, + confidentialItemText() { + return this.isConfidential + ? this.$options.i18n.disableConfidentiality + : this.$options.i18n.enableConfidentiality; + }, objectiveWorkItemTypeId() { return this.workItemTypes.find((type) => type.name === WORK_ITEM_TYPE_VALUE_OBJECTIVE).id; }, @@ -267,7 +283,7 @@ export default { icon="ellipsis_v" data-testid="work-item-actions-dropdown" text-sr-only - :text="__('More actions')" + :toggle-text="__('More actions')" category="tertiary" :auto-close="false" no-caret @@ -282,7 +298,6 @@ export default { <gl-toggle :value="subscribedToNotifications" :label="$options.i18n.notifications" - :data-testid="$options.notificationsToggleTestId" class="work-item-notification-toggle" label-position="left" @change="toggleNotifications($event)" @@ -299,49 +314,56 @@ export default { > <template #list-item>{{ __('Promote to objective') }}</template> </gl-disclosure-dropdown-item> - <template v-if="canUpdate && !isParentConfidential"> - <gl-disclosure-dropdown-item - :data-testid="$options.confidentialityTestId" - @action="handleToggleWorkItemConfidentiality" - ><template #list-item>{{ - isConfidential - ? $options.i18n.disableTaskConfidentiality - : $options.i18n.enableTaskConfidentiality - }}</template></gl-disclosure-dropdown-item - > - </template> + + <gl-disclosure-dropdown-item + v-if="canUpdate && !isParentConfidential" + :data-testid="$options.confidentialityTestId" + @action="handleToggleWorkItemConfidentiality" + > + <template #list-item>{{ confidentialItemText }}</template> + </gl-disclosure-dropdown-item> + + <work-item-state-toggle + v-if="canUpdate" + :data-testid="$options.stateToggleTestId" + :work-item-id="workItemId" + :work-item-state="workItemState" + :work-item-parent-id="workItemParentId" + :work-item-type="workItemType" + show-as-dropdown-item + /> + <gl-disclosure-dropdown-item - ref="workItemReference" :data-testid="$options.copyReferenceTestId" :data-clipboard-text="workItemReference" @action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)" - ><template #list-item>{{ - $options.i18n.copyReference - }}</template></gl-disclosure-dropdown-item > - <template v-if="$options.isLoggedIn && workItemCreateNoteEmail"> - <gl-disclosure-dropdown-item - ref="workItemCreateNoteEmail" - :data-testid="$options.copyCreateNoteEmailTestId" - :data-clipboard-text="workItemCreateNoteEmail" - @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)" - ><template #list-item>{{ - i18n.copyCreateNoteEmail - }}</template></gl-disclosure-dropdown-item - > - </template> - <gl-dropdown-divider v-if="canDelete" /> + <template #list-item>{{ $options.i18n.copyReference }}</template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item - v-if="canDelete" - :data-testid="$options.deleteActionTestId" - variant="danger" - @action="handleDelete" + v-if="$options.isLoggedIn && workItemCreateNoteEmail" + :data-testid="$options.copyCreateNoteEmailTestId" + :data-clipboard-text="workItemCreateNoteEmail" + @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)" > - <template #list-item - ><span class="text-danger">{{ i18n.deleteWorkItem }}</span></template - > + <template #list-item>{{ i18n.copyCreateNoteEmail }}</template> </gl-disclosure-dropdown-item> + + <template v-if="canDelete"> + <gl-dropdown-divider /> + <gl-disclosure-dropdown-item + :data-testid="$options.deleteActionTestId" + variant="danger" + @action="handleDelete" + > + <template #list-item> + <span class="text-danger">{{ i18n.deleteWorkItem }}</span> + </template> + </gl-disclosure-dropdown-item> + </template> </gl-disclosure-dropdown> + <gl-modal ref="modal" modal-id="work-item-confirm-delete" diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue index fd01d855782..7d09a003926 100644 --- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue +++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue @@ -13,6 +13,7 @@ import { WIDGET_TYPE_WEIGHT, WORK_ITEM_TYPE_VALUE_KEY_RESULT, WORK_ITEM_TYPE_VALUE_OBJECTIVE, + WORK_ITEM_TYPE_VALUE_TASK, } from '../constants'; import WorkItemDueDate from './work_item_due_date.vue'; import WorkItemAssignees from './work_item_assignees.vue'; @@ -98,7 +99,8 @@ export default { showWorkItemParent() { return ( this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE || - this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT + this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT || + this.workItemType === WORK_ITEM_TYPE_VALUE_TASK ); }, workItemParent() { diff --git a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue index 44bd17b59a2..f806946509f 100644 --- a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue +++ b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue @@ -1,13 +1,14 @@ <script> -import * as Sentry from '@sentry/browser'; import { produce } from 'immer'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import AwardsList from '~/vue_shared/components/awards_list.vue'; import { isLoggedIn } from '~/lib/utils/common_utils'; import { TYPENAME_USER } from '~/graphql_shared/constants'; -import workItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql'; +import groupWorkItemAwardEmojiQuery from '../graphql/group_award_emoji.query.graphql'; +import projectWorkItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql'; import updateAwardEmojiMutation from '../graphql/update_award_emoji.mutation.graphql'; import { EMOJI_THUMBSDOWN, @@ -23,6 +24,7 @@ export default { components: { AwardsList, }, + inject: ['isGroup'], props: { workItemId: { type: String, @@ -75,7 +77,9 @@ export default { }, apollo: { awardEmoji: { - query: workItemAwardEmojiQuery, + query() { + return this.isGroup ? groupWorkItemAwardEmojiQuery : projectWorkItemAwardEmojiQuery; + }, variables() { return { iid: this.workItemIid, @@ -116,7 +120,7 @@ export default { after: this.pageInfo?.endCursor, }, }); - } catch (error) { + } catch { this.$emit('error', I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR); } }, @@ -139,7 +143,7 @@ export default { return this.awardEmoji.nodes; } - // else make a copy of unmutable list and return the list after adding the new emoji + // else make a copy of immutable list and return the list after adding the new emoji const awardEmojiNodes = [...this.awardEmoji.nodes]; awardEmojiNodes.push({ name, @@ -162,7 +166,7 @@ export default { }, updateWorkItemAwardEmojiWidgetCache({ cache, name, toggledOn }) { const query = { - query: workItemAwardEmojiQuery, + query: this.isGroup ? groupWorkItemAwardEmojiQuery : projectWorkItemAwardEmojiQuery, variables: { fullPath: this.workItemFullpath, iid: this.workItemIid, @@ -234,7 +238,6 @@ export default { <template> <div v-if="!isLoading" class="gl-mt-3"> <awards-list - data-testid="work-item-award-list" :awards="awards" :can-award-emoji="$options.isLoggedIn" :current-user-id="currentUserId" diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue index 460b5d35187..d352d66196a 100644 --- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue +++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue @@ -86,7 +86,7 @@ export default { </script> <template> - <div class="gl-mb-3 gl-text-gray-700"> + <div class="gl-mb-3 gl-text-gray-700 gl-mt-3"> <work-item-state-badge v-if="workItemState" :work-item-state="workItemState" /> <gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" /> <confidentiality-badge diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index b7f3ac93cdb..77c573b47e4 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -1,6 +1,6 @@ <script> import { GlAlert, GlButton, GlForm, GlFormGroup } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { helpPagePath } from '~/helpers/help_page_helper'; import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; @@ -244,13 +244,7 @@ export default { @keydown.ctrl.enter="updateWorkItem" /> <div class="gl-display-flex"> - <gl-alert - v-if="hasConflicts" - :dismissible="false" - variant="danger" - class="gl-w-full" - data-testid="work-item-description-conflicts" - > + <gl-alert v-if="hasConflicts" :dismissible="false" variant="danger" class="gl-w-full"> <p> {{ s__( diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue index 07e03eba1d1..124e05db431 100644 --- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue +++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue @@ -114,7 +114,7 @@ export default { v-else ref="gfm-content" v-safe-html="descriptionHtml" - class="md gl-mb-5 gl-min-h-8" + class="md gl-mb-5 gl-min-h-8 gl-clearfix" data-testid="work-item-description" @change="toggleCheckboxes" ></div> diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 53929775684..45d3aa564a5 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -50,7 +50,6 @@ import WorkItemDescription from './work_item_description.vue'; import WorkItemNotes from './work_item_notes.vue'; import WorkItemDetailModal from './work_item_detail_modal.vue'; import WorkItemAwardEmoji from './work_item_award_emoji.vue'; -import WorkItemStateToggleButton from './work_item_state_toggle_button.vue'; import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue'; import WorkItemTypeIcon from './work_item_type_icon.vue'; @@ -61,7 +60,6 @@ export default { }, isLoggedIn: isLoggedIn(), components: { - WorkItemStateToggleButton, GlAlert, GlButton, GlLoadingIcon, @@ -146,9 +144,9 @@ export default { if (isEmpty(this.workItem)) { this.setEmptyState(); } - if (!this.isModal && this.workItem.project) { - const path = this.workItem.project?.fullPath - ? ` · ${this.workItem.project.fullPath}` + if (!this.isModal && this.workItem.namespace) { + const path = this.workItem.namespace.fullPath + ? ` · ${this.workItem.namespace.fullPath}` : ''; document.title = `${this.workItem.title} · ${this.workItem?.workItemType?.name}${path}`; @@ -181,19 +179,19 @@ export default { return this.workItemType ? `#${this.workItem.iid}` : ''; }, canUpdate() { - return this.workItem?.userPermissions?.updateWorkItem; + return this.workItem.userPermissions?.updateWorkItem; }, canDelete() { - return this.workItem?.userPermissions?.deleteWorkItem; + return this.workItem.userPermissions?.deleteWorkItem; }, canSetWorkItemMetadata() { - return this.workItem?.userPermissions?.setWorkItemMetadata; + return this.workItem.userPermissions?.setWorkItemMetadata; }, canAssignUnassignUser() { return this.workItemAssignees && this.canSetWorkItemMetadata; }, projectFullPath() { - return this.workItem?.project?.fullPath; + return this.workItem.namespace?.fullPath; }, workItemsMvc2Enabled() { return this.glFeatures.workItemsMvc2; @@ -222,7 +220,7 @@ export default { return this.parentWorkItem?.webUrl; }, workItemIconName() { - return this.workItem?.workItemType?.iconName; + return this.workItem.workItemType?.iconName; }, noAccessSvgPath() { return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`; @@ -274,6 +272,18 @@ export default { showWorkItemLinkedItems() { return this.hasLinkedWorkItems && this.workItemLinkedItems; }, + titleClassHeader() { + return { + 'gl-sm-display-none!': this.parentWorkItem, + 'gl-w-full': !this.parentWorkItem, + }; + }, + titleClassComponent() { + return { + 'gl-sm-display-block!': !this.parentWorkItem, + 'gl-display-none gl-sm-display-block!': this.parentWorkItem, + }; + }, }, mounted() { if (this.modalWorkItemIid) { @@ -285,7 +295,7 @@ export default { }, methods: { isWidgetPresent(type) { - return this.workItem?.widgets?.find((widget) => widget.type === type); + return this.workItem.widgets?.find((widget) => widget.type === type); }, toggleConfidentiality(confidentialStatus) { this.updateInProgress = true; @@ -409,7 +419,20 @@ export default { </gl-skeleton-loader> </div> <template v-else> - <div class="gl-display-flex gl-align-items-center" data-testid="work-item-body"> + <div class="gl-sm-display-none! gl-display-flex"> + <gl-button + v-if="isModal" + class="gl-ml-auto" + category="tertiary" + data-testid="work-item-close" + icon="close" + :aria-label="__('Close')" + @click="$emit('close')" + /> + </div> + <div + class="gl-display-block gl-sm-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3" + > <ul v-if="parentWorkItem" class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0" @@ -440,53 +463,55 @@ export default { </li> </ul> <div - v-else-if="!error && !workItemLoading" - class="gl-mr-auto" + v-if="!error && !workItemLoading" + :class="titleClassHeader" data-testid="work-item-type" > - <work-item-type-icon - :work-item-icon-name="workItemIconName" + <work-item-title + v-if="workItem.title" + ref="title" + class="gl-sm-display-block!" + :work-item-id="workItem.id" + :work-item-title="workItem.title" :work-item-type="workItemType" - show-text + :work-item-parent-id="workItemParentId" + :can-update="canUpdate" + @error="updateError = $event" + /> + </div> + <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3"> + <work-item-todos + v-if="showWorkItemCurrentUserTodos" + :work-item-id="workItem.id" + :work-item-iid="workItemIid" + :work-item-fullpath="projectFullPath" + :current-user-todos="currentUserTodos" + @error="updateError = $event" + /> + <work-item-actions + :full-path="fullPath" + :work-item-id="workItem.id" + :subscribed-to-notifications="workItemNotificationsSubscribed" + :work-item-type="workItemType" + :work-item-type-id="workItemTypeId" + :can-delete="canDelete" + :can-update="canUpdate" + :is-confidential="workItem.confidential" + :is-parent-confidential="parentWorkItemConfidentiality" + :work-item-reference="workItem.reference" + :work-item-create-note-email="workItem.createNoteEmail" + :is-modal="isModal" + :work-item-state="workItem.state" + :work-item-parent-id="workItemParentId" + @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })" + @toggleWorkItemConfidentiality="toggleConfidentiality" + @error="updateError = $event" + @promotedToObjective="$emit('promotedToObjective', workItemIid)" /> - {{ workItemBreadcrumbReference }} </div> - <work-item-state-toggle-button - v-if="canUpdate" - :work-item-id="workItem.id" - :work-item-state="workItem.state" - :work-item-parent-id="workItemParentId" - :work-item-type="workItemType" - @error="updateError = $event" - /> - <work-item-todos - v-if="showWorkItemCurrentUserTodos" - :work-item-id="workItem.id" - :work-item-iid="workItemIid" - :work-item-fullpath="projectFullPath" - :current-user-todos="currentUserTodos" - @error="updateError = $event" - /> - <work-item-actions - :full-path="fullPath" - :work-item-id="workItem.id" - :subscribed-to-notifications="workItemNotificationsSubscribed" - :work-item-type="workItemType" - :work-item-type-id="workItemTypeId" - :can-delete="canDelete" - :can-update="canUpdate" - :is-confidential="workItem.confidential" - :is-parent-confidential="parentWorkItemConfidentiality" - :work-item-reference="workItem.reference" - :work-item-create-note-email="workItem.createNoteEmail" - :is-modal="isModal" - @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })" - @toggleWorkItemConfidentiality="toggleConfidentiality" - @error="updateError = $event" - @promotedToObjective="$emit('promotedToObjective', workItemIid)" - /> <gl-button v-if="isModal" + class="gl-display-none gl-sm-display-block!" category="tertiary" data-testid="work-item-close" icon="close" @@ -496,8 +521,9 @@ export default { </div> <div> <work-item-title - v-if="workItem.title" + v-if="workItem.title && parentWorkItem" ref="title" + :class="titleClassComponent" :work-item-id="workItem.id" :work-item-title="workItem.title" :work-item-type="workItemType" diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue index 1aa62a2b906..704fe6fb11d 100644 --- a/app/assets/javascripts/work_items/components/work_item_due_date.vue +++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlDatepicker, GlFormGroup } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility'; import { s__ } from '~/locale'; import Tracking from '~/tracking'; diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue index 3cdbf816421..7a5d3b1155f 100644 --- a/app/assets/javascripts/work_items/components/work_item_labels.vue +++ b/app/assets/javascripts/work_items/components/work_item_labels.vue @@ -3,7 +3,8 @@ import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui'; import { debounce, uniqueId, without } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import Tracking from '~/tracking'; -import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql'; +import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql'; +import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql'; import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; @@ -90,7 +91,9 @@ export default { }, }, searchLabels: { - query: labelSearchQuery, + query() { + return this.isGroup ? groupLabelsQuery : projectLabelsQuery; + }, variables() { return { fullPath: this.fullPath, diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue index f4de7c1dddc..b6ea09edbd4 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue @@ -1,7 +1,7 @@ <script> -import * as Sentry from '@sentry/browser'; import produce from 'immer'; import Draggable from 'vuedraggable'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { isLoggedIn } from '~/lib/utils/common_utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; @@ -50,6 +50,11 @@ export default { required: false, default: false, }, + showLabels: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -151,9 +156,6 @@ export default { update(data) { return data.workspace.workItems.nodes[0]; }, - context: { - isSingleRequest: true, - }, }); }, prefetchWorkItem({ iid }) { @@ -280,6 +282,7 @@ export default { :confidential="child.confidential" :work-item-type="workItemType" :has-indirect-children="hasIndirectChildren" + :show-labels="showLabels" @mouseover="prefetchWorkItem(child)" @mouseout="clearPrefetching" @removeChild="removeChild" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue index 847a3585ac4..49454c3d9f3 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue @@ -1,7 +1,7 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { cloneDeep } from 'lodash'; -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { __, s__ } from '~/locale'; import { isScopedLabel } from '~/lib/utils/common_utils'; import { createAlert } from '~/alert'; @@ -49,6 +49,11 @@ export default { required: false, default: '', }, + showLabels: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -231,6 +236,7 @@ export default { :can-update="canUpdate" :parent-work-item-id="issuableGid" :work-item-type="workItemType" + :show-labels="showLabels" @click="$emit('click', $event)" @removeChild="$emit('removeChild', childItem)" /> @@ -241,6 +247,7 @@ export default { :work-item-id="issuableGid" :work-item-type="workItemType" :children="children" + :show-labels="showLabels" @removeChild="removeChild" @click="$emit('click', $event)" /> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index 7fa6ac2c57f..dd0a26c0b9c 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -5,6 +5,7 @@ import { GlIcon, GlLoadingIcon, GlTooltipDirective, + GlToggle, } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { s__ } from '~/locale'; @@ -15,7 +16,12 @@ import { isMetaKey } from '~/lib/utils/common_utils'; import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; -import { FORM_TYPES, WIDGET_ICONS, WORK_ITEM_STATUS_TEXT } from '../../constants'; +import { + FORM_TYPES, + WIDGET_ICONS, + WORK_ITEM_STATUS_TEXT, + I18N_WORK_ITEM_SHOW_LABELS, +} from '../../constants'; import { findHierarchyWidgetChildren } from '../../utils'; import { removeHierarchyChild } from '../../graphql/cache_utils'; import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql'; @@ -36,6 +42,7 @@ export default { WorkItemDetailModal, AbuseCategorySelector, WorkItemChildrenWrapper, + GlToggle, }, directives: { GlTooltip: GlTooltipDirective, @@ -65,9 +72,6 @@ export default { update(data) { return data.workspace.workItems.nodes[0] ?? {}; }, - context: { - isSingleRequest: true, - }, skip() { return !this.iid; }, @@ -107,6 +111,7 @@ export default { reportedUserId: 0, reportedUrl: '', widgetName: 'tasks', + showLabels: true, }; }, computed: { @@ -204,6 +209,7 @@ export default { addChildButtonLabel: s__('WorkItem|Add'), addChildOptionLabel: s__('WorkItem|Existing task'), createChildOptionLabel: s__('WorkItem|New task'), + showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS, }, WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK, WORK_ITEM_STATUS_TEXT, @@ -227,6 +233,14 @@ export default { </span> </template> <template #header-right> + <gl-toggle + class="gl-mr-4" + :value="showLabels" + :label="$options.i18n.showLabelsLabel" + label-position="left" + label-id="relationship-toggle-labels" + @change="showLabels = $event" + /> <gl-disclosure-dropdown v-if="canUpdate && canAddTask" placement="right" @@ -282,6 +296,7 @@ export default { :full-path="fullPath" :work-item-id="issuableGid" :work-item-iid="iid" + :show-labels="showLabels" @error="error = $event" @show-modal="openChild" /> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index b61b3b2e0d3..3d09a90169c 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -1,10 +1,12 @@ <script> +import { GlToggle } from '@gitlab/ui'; import { FORM_TYPES, WIDGET_TYPE_HIERARCHY, WORK_ITEMS_TREE_TEXT_MAP, WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_KEY_RESULT, + I18N_WORK_ITEM_SHOW_LABELS, } from '../../constants'; import WidgetWrapper from '../widget_wrapper.vue'; import OkrActionsSplitButton from './okr_actions_split_button.vue'; @@ -21,6 +23,7 @@ export default { WidgetWrapper, WorkItemLinksForm, WorkItemChildrenWrapper, + GlToggle, }, props: { fullPath: { @@ -68,6 +71,7 @@ export default { formType: null, childType: null, widgetName: 'tasks', + showLabels: true, }; }, computed: { @@ -99,6 +103,9 @@ export default { this.$emit('show-modal', { event, modalWorkItem: child }); }, }, + i18n: { + showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS, + }, }; </script> @@ -114,6 +121,14 @@ export default { {{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }} </template> <template #header-right> + <gl-toggle + class="gl-mr-4" + :value="showLabels" + :label="$options.i18n.showLabelsLabel" + label-position="left" + label-id="relationship-toggle-labels" + @change="showLabels = $event" + /> <okr-actions-split-button v-if="canUpdate" @showCreateObjectiveForm=" @@ -160,6 +175,7 @@ export default { :work-item-id="workItemId" :work-item-iid="workItemIid" :work-item-type="workItemType" + :show-labels="showLabels" @error="error = $event" @show-modal="showModal" /> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue index 401223c3593..af181fa4e7e 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue @@ -22,6 +22,11 @@ export default { required: false, default: false, }, + showLabels: { + type: Boolean, + required: false, + default: true, + }, }, }; </script> @@ -35,6 +40,7 @@ export default { :issuable-gid="workItemId" :child-item="child" :work-item-type="workItemType" + :show-labels="showLabels" @removeChild="$emit('removeChild', $event)" @click="$emit('click', Object.assign($event, { childItem: child }))" /> diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue index a2cbb7f7598..9c6fa158169 100644 --- a/app/assets/javascripts/work_items/components/work_item_milestone.vue +++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue @@ -1,15 +1,7 @@ <script> -import { - GlFormGroup, - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - GlSkeletonLoader, - GlSearchBoxByType, - GlDropdownText, -} from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; +import { GlCollapsibleListbox, GlFormGroup, GlSkeletonLoader } from '@gitlab/ui'; import { debounce } from 'lodash'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import Tracking from '~/tracking'; import { s__, __ } from '~/locale'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; @@ -22,7 +14,8 @@ import { TRACKING_CATEGORY_SHOW, } from '../constants'; -const noMilestoneId = 'no-milestone-id'; +export const noMilestoneId = 'no-milestone-id'; +const noMilestoneItem = { text: s__('WorkItem|No milestone'), value: noMilestoneId }; export default { i18n: { @@ -37,13 +30,9 @@ export default { EXPIRED_TEXT: __('(expired)'), }, components: { + GlCollapsibleListbox, GlFormGroup, - GlDropdown, - GlDropdownItem, - GlDropdownDivider, GlSkeletonLoader, - GlSearchBoxByType, - GlDropdownText, }, mixins: [Tracking.mixin()], props: { @@ -74,11 +63,23 @@ export default { data() { return { localMilestone: this.workItemMilestone, + localMilestoneId: this.workItemMilestone?.id, searchTerm: '', shouldFetch: false, updateInProgress: false, - isFocused: false, milestones: [], + dropdownGroups: [ + { + text: this.$options.i18n.NO_MILESTONE, + textSrOnly: true, + options: [noMilestoneItem], + }, + { + text: __('Milestones'), + textSrOnly: true, + options: [], + }, + ], }; }, computed: { @@ -103,23 +104,29 @@ export default { isLoadingMilestones() { return this.$apollo.queries.milestones.loading; }, - isNoMilestone() { - return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id; + milestonesList() { + return ( + this.milestones.map(({ id, title, expired }) => { + return { + value: id, + text: title, + expired, + }; + }) ?? [] + ); }, - dropdownClasses() { - return { - 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone, - 'is-not-focused': !this.isFocused, - 'gl-min-w-20': true, - }; + toggleClasses() { + const toggleClasses = ['gl-max-w-full']; + + if (this.localMilestoneId === noMilestoneId) { + toggleClasses.push('gl-text-gray-500!'); + } + return toggleClasses; }, }, watch: { - workItemMilestone: { - handler(newVal) { - this.localMilestone = newVal; - }, - deep: true, + milestones() { + this.dropdownGroups[1].options = this.milestonesList; }, }, created() { @@ -152,15 +159,11 @@ export default { this.localMilestone = milestone; }, onDropdownShown() { - this.$refs.search.focusInput(); this.shouldFetch = true; - this.isFocused = true; }, onDropdownHide() { - this.isFocused = false; this.searchTerm = ''; this.shouldFetch = false; - this.updateMilestone(); }, setSearchKey(value) { this.searchTerm = value; @@ -169,6 +172,9 @@ export default { return this.localMilestone?.id === milestone?.id; }, updateMilestone() { + this.localMilestone = + this.milestones.find(({ id }) => id === this.localMilestoneId) ?? noMilestoneItem; + if (this.workItemMilestone?.id === this.localMilestone?.id) { return; } @@ -182,8 +188,7 @@ export default { input: { id: this.workItemId, milestoneWidget: { - milestoneId: - this.localMilestone?.id === 'no-milestone-id' ? null : this.localMilestone?.id, + milestoneId: this.localMilestoneId === noMilestoneId ? null : this.localMilestoneId, }, }, }, @@ -222,50 +227,45 @@ export default { > {{ dropdownText }} </span> - <gl-dropdown + + <gl-collapsible-listbox v-else id="milestone-value" + v-model="localMilestoneId" + :items="dropdownGroups" + category="tertiary" data-testid="work-item-milestone-dropdown" - class="gl-pl-0 gl-max-w-full work-item-field-value" - :toggle-class="dropdownClasses" - :text="dropdownText" + class="gl-max-w-full" + :toggle-text="dropdownText" :loading="updateInProgress" + :toggle-class="toggleClasses" + searchable + @select="updateMilestone" @shown="onDropdownShown" - @hide="onDropdownHide" + @hidden="onDropdownHide" + @search="debouncedSearchKeyUpdate" > - <template #header> - <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" /> + <template #list-item="{ item }"> + {{ item.text }} + <span v-if="item.expired">{{ $options.i18n.EXPIRED_TEXT }}</span> </template> - <gl-dropdown-item - data-testid="no-milestone" - is-check-item - :is-checked="isNoMilestone" - @click="handleMilestoneClick({ id: 'no-milestone-id' })" - > - {{ $options.i18n.NO_MILESTONE }} - </gl-dropdown-item> - <gl-dropdown-divider /> - <gl-dropdown-text v-if="isLoadingMilestones"> - <gl-skeleton-loader :height="90"> + <template #footer> + <gl-skeleton-loader v-if="isLoadingMilestones" :height="90"> <rect width="380" height="10" x="10" y="15" rx="4" /> <rect width="280" height="10" x="10" y="30" rx="4" /> <rect width="380" height="10" x="10" y="50" rx="4" /> <rect width="280" height="10" x="10" y="65" rx="4" /> </gl-skeleton-loader> - </gl-dropdown-text> - <template v-else-if="milestones.length"> - <gl-dropdown-item - v-for="milestone in milestones" - :key="milestone.id" - is-check-item - :is-checked="isMilestoneChecked(milestone)" - @click="handleMilestoneClick(milestone)" + + <div + v-else-if="!milestones.length" + aria-live="assertive" + class="gl-pl-7 gl-pr-5 gl-py-3 gl-font-base gl-text-gray-600" + data-testid="no-results-text" > - {{ milestone.title }} - <template v-if="milestone.expired">{{ $options.i18n.EXPIRED_TEXT }}</template> - </gl-dropdown-item> + {{ $options.i18n.NO_MATCHING_RESULTS }} + </div> </template> - <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text> - </gl-dropdown> + </gl-collapsible-listbox> </gl-form-group> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue index fe8aea99f53..6756acd4495 100644 --- a/app/assets/javascripts/work_items/components/work_item_notes.vue +++ b/app/assets/javascripts/work_items/components/work_item_notes.vue @@ -1,7 +1,7 @@ <script> import { GlSkeletonLoader, GlModal } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; import { uniqueId } from 'lodash'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { __ } from '~/locale'; import { scrollToTargetOnResize } from '~/lib/utils/resize_observer'; import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants'; @@ -170,9 +170,6 @@ export default { apollo: { workItemNotes: { query: workItemNotesByIidQuery, - context: { - isSingleRequest: true, - }, variables() { return { fullPath: this.fullPath, diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue index e16299f482f..ce30f7985cf 100644 --- a/app/assets/javascripts/work_items/components/work_item_parent.vue +++ b/app/assets/javascripts/work_items/components/work_item_parent.vue @@ -1,18 +1,20 @@ <script> import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; import { debounce } from 'lodash'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import { removeHierarchyChild } from '../graphql/cache_utils'; +import groupWorkItemsQuery from '../graphql/group_work_items.query.graphql'; import projectWorkItemsQuery from '../graphql/project_work_items.query.graphql'; import { I18N_WORK_ITEM_ERROR_UPDATING, sprintfWorkItem, - WORK_ITEM_TYPE_ENUM_OBJECTIVE, + SUPPORTED_PARENT_TYPE_MAP, } from '../constants'; export default { @@ -31,7 +33,7 @@ export default { GlCollapsibleListbox, }, mixins: [glFeatureFlagMixin()], - inject: ['fullPath'], + inject: ['fullPath', 'isGroup'], props: { workItemId: { type: String, @@ -60,7 +62,7 @@ export default { searchStarted: false, availableWorkItems: [], localSelectedItem: this.parent?.id, - isNotFocused: true, + oldParent: this.parent, }; }, computed: { @@ -80,13 +82,8 @@ export default { workItems() { return this.availableWorkItems.map(({ id, title }) => ({ text: title, value: id })); }, - listboxCategory() { - return this.searchStarted ? 'secondary' : 'tertiary'; - }, - listboxClasses() { - return { - 'is-not-focused': this.isNotFocused && !this.searchStarted, - }; + parentType() { + return SUPPORTED_PARENT_TYPE_MAP[this.workItemType]; }, }, watch: { @@ -101,13 +98,17 @@ export default { }, apollo: { availableWorkItems: { - query: projectWorkItemsQuery, + query() { + return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery; + }, variables() { return { fullPath: this.fullPath, searchTerm: this.search, - types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE], + types: this.parentType, in: this.search ? 'TITLE' : undefined, + iid: null, + isNumber: false, }; }, skip() { @@ -146,6 +147,14 @@ export default { }, }, }, + update: (cache) => + removeHierarchyChild({ + cache, + fullPath: this.fullPath, + iid: this.oldParent?.iid, + isGroup: this.isGroup, + workItem: { id: this.workItemId }, + }), }); if (errors.length) { @@ -171,19 +180,10 @@ export default { }, onListboxShown() { this.searchStarted = true; - this.isNotFocused = false; }, onListboxHide() { this.searchStarted = false; this.search = ''; - this.isNotFocused = true; - }, - setListboxFocused() { - // This is to match the caret behaviour of parent listbox - // to the other dropdown fields of work items - if (document.activeElement.parentElement.id !== 'work-item-parent-listbox-value') { - this.isNotFocused = true; - } }, }, }; @@ -206,30 +206,20 @@ export default { > {{ listboxText }} </span> - <div - v-else - :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }" - @mouseover="isNotFocused = false" - @mouseleave="setListboxFocused" - @focusout="isNotFocused = true" - @focusin="isNotFocused = false" - > + <div v-else :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }"> <gl-collapsible-listbox id="work-item-parent-listbox-value" class="gl-max-w-max-content" data-testid="work-item-parent-listbox" - block searchable - :no-caret="isNotFocused && !searchStarted" is-check-centered - :category="listboxCategory" + category="tertiary" :searching="isLoading" :header-text="$options.i18n.assignParentLabel" :no-results-text="$options.i18n.noMatchingResults" :loading="updateInProgress" :items="workItems" :toggle-text="listboxText" - :toggle-class="listboxClasses" :selected="localSelectedItem" :reset-button-label="$options.i18n.unAssign" @reset="unAssignParent" diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue index d242db95896..c98bd6ce1e9 100644 --- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue @@ -4,6 +4,7 @@ import { GlFormGroup, GlForm, GlFormRadioGroup, GlButton, GlAlert } from '@gitla import { __, s__ } from '~/locale'; import WorkItemTokenInput from '../shared/work_item_token_input.vue'; import addLinkedItemsMutation from '../../graphql/add_linked_items.mutation.graphql'; +import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; import { LINK_ITEM_FORM_HEADER_LABEL, @@ -23,6 +24,7 @@ export default { GlAlert, WorkItemTokenInput, }, + inject: ['isGroup'], props: { workItemId: { type: String, @@ -121,7 +123,7 @@ export default { }, ) => { const queryArgs = { - query: workItemByIidQuery, + query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery, variables: { fullPath: this.workItemFullPath, iid: this.workItemIid }, }; const sourceData = cache.readQuery(queryArgs); diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue index 002c1786044..e70c79ea68f 100644 --- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue +++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue @@ -19,6 +19,11 @@ export default { type: Boolean, required: true, }, + showLabels: { + type: Boolean, + required: false, + default: true, + }, }, }; </script> @@ -42,6 +47,7 @@ export default { :child-item="linkedItem.workItem" :can-update="canUpdate" :show-task-icon="true" + :show-labels="showLabels" @click="$emit('showModal', { event: $event, child: linkedItem.workItem })" @removeChild="$emit('removeLinkedItem', linkedItem.workItem)" /> diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue index 20427fe96c4..790804a8934 100644 --- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue +++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue @@ -1,6 +1,6 @@ <script> import { produce } from 'immer'; -import { GlLoadingIcon, GlIcon, GlButton, GlLink } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon, GlButton, GlLink, GlToggle } from '@gitlab/ui'; import { s__ } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; @@ -8,7 +8,11 @@ import { helpPagePath } from '~/helpers/help_page_helper'; import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; import removeLinkedItemsMutation from '../../graphql/remove_linked_items.mutation.graphql'; -import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants'; +import { + WIDGET_TYPE_LINKED_ITEMS, + LINKED_CATEGORIES_MAP, + I18N_WORK_ITEM_SHOW_LABELS, +} from '../../constants'; import WidgetWrapper from '../widget_wrapper.vue'; import WorkItemRelationshipList from './work_item_relationship_list.vue'; @@ -24,6 +28,7 @@ export default { WidgetWrapper, WorkItemRelationshipList, WorkItemAddRelationshipForm, + GlToggle, }, inject: ['isGroup'], props: { @@ -60,9 +65,6 @@ export default { update(data) { return data.workspace.workItems.nodes[0] ?? {}; }, - context: { - isSingleRequest: true, - }, skip() { return !this.workItemIid; }, @@ -97,6 +99,7 @@ export default { linksBlocks: [], isShownLinkItemForm: false, widgetName: 'linkeditems', + showLabels: true, }; }, computed: { @@ -150,7 +153,7 @@ export default { return; } const queryArgs = { - query: workItemByIidQuery, + query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery, variables: { fullPath: this.workItemFullPath, iid: this.workItemIid }, }; const sourceData = cache.readQuery(queryArgs); @@ -200,6 +203,7 @@ export default { blockingTitle: s__('WorkItem|Blocking'), blockedByTitle: s__('WorkItem|Blocked by'), addLinkedWorkItemButtonLabel: s__('WorkItem|Add'), + showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS, }, }; </script> @@ -222,11 +226,18 @@ export default { </div> </template> <template #header-right> + <gl-toggle + :value="showLabels" + :label="$options.i18n.showLabelsLabel" + label-position="left" + label-id="relationship-toggle-labels" + @change="showLabels = $event" + /> <gl-button v-if="canAdminWorkItemLink" data-testid="link-item-add-button" size="small" - class="gl-ml-3" + class="gl-ml-4" @click="showLinkItemForm" > <slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot> @@ -264,6 +275,7 @@ export default { :linked-items="linksBlocks" :heading="$options.i18n.blockingTitle" :can-update="canAdminWorkItemLink" + :show-labels="showLabels" @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })" @removeLinkedItem="removeLinkedItem" /> @@ -276,6 +288,7 @@ export default { :linked-items="linksIsBlockedBy" :heading="$options.i18n.blockedByTitle" :can-update="canAdminWorkItemLink" + :show-labels="showLabels" @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })" @removeLinkedItem="removeLinkedItem" /> @@ -284,6 +297,7 @@ export default { :linked-items="linksRelatesTo" :heading="$options.i18n.relatedToTitle" :can-update="canAdminWorkItemLink" + :show-labels="showLabels" @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })" @removeLinkedItem="removeLinkedItem" /> diff --git a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue index 0ea30845466..581ef9ec945 100644 --- a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue +++ b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue @@ -1,9 +1,8 @@ <script> -import { GlButton } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; +import { GlButton, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import Tracking from '~/tracking'; -import { __, sprintf } from '~/locale'; -import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item'; import { sprintfWorkItem, @@ -17,6 +16,8 @@ import { export default { components: { GlButton, + GlDisclosureDropdownItem, + GlLoadingIcon, }, mixins: [Tracking.mixin()], props: { @@ -37,6 +38,11 @@ export default { required: false, default: null, }, + showAsDropdownItem: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -51,9 +57,7 @@ export default { const baseText = this.isWorkItemOpen ? __('Close %{workItemType}') : __('Reopen %{workItemType}'); - return capitalizeFirstCharacter( - sprintf(baseText, { workItemType: this.workItemType.toLowerCase() }), - ); + return sprintfWorkItem(baseText, this.workItemType); }, tracking() { return { @@ -62,6 +66,12 @@ export default { property: `type_${this.workItemType}`, }; }, + toggleInProgressText() { + const baseText = this.isWorkItemOpen + ? __('Closing %{workItemType}') + : __('Reopening %{workItemType}'); + return sprintfWorkItem(baseText, this.workItemType); + }, }, methods: { async updateWorkItem() { @@ -104,10 +114,18 @@ export default { </script> <template> - <gl-button - :loading="updateInProgress" - data-testid="work-item-state-toggle" - @click="updateWorkItem" - >{{ toggleWorkItemStateText }}</gl-button - > + <gl-disclosure-dropdown-item v-if="showAsDropdownItem" @action="updateWorkItem"> + <template #list-item> + <template v-if="updateInProgress"> + <gl-loading-icon inline size="sm" /> + {{ toggleInProgressText }} + </template> + <template v-else> + {{ toggleWorkItemStateText }} + </template> + </template> + </gl-disclosure-dropdown-item> + <gl-button v-else :loading="updateInProgress" @click="updateWorkItem">{{ + toggleWorkItemStateText + }}</gl-button> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue index c52a6854fad..9b5803421dd 100644 --- a/app/assets/javascripts/work_items/components/work_item_title.vue +++ b/app/assets/javascripts/work_items/components/work_item_title.vue @@ -1,10 +1,12 @@ <script> -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import Tracking from '~/tracking'; import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_UPDATING, TRACKING_CATEGORY_SHOW, + WORK_ITEM_TITLE_MAX_LENGTH, + I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE, } from '../constants'; import { getUpdateWorkItemMutation } from './update_work_item'; import ItemTitle from './item_title.vue'; @@ -56,6 +58,11 @@ export default { return; } + if (updatedTitle.length > WORK_ITEM_TITLE_MAX_LENGTH) { + this.$emit('error', sprintfWorkItem(I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE)); + return; + } + const input = { id: this.workItemId, title: updatedTitle, diff --git a/app/assets/javascripts/work_items/components/work_item_todos.vue b/app/assets/javascripts/work_items/components/work_item_todos.vue index e6d7f2067ba..62518616398 100644 --- a/app/assets/javascripts/work_items/components/work_item_todos.vue +++ b/app/assets/javascripts/work_items/components/work_item_todos.vue @@ -175,17 +175,12 @@ export default { <template> <gl-button v-gl-tooltip.hover - data-testid="work-item-todos-action" :loading="isLoading" :title="buttonLabel" - category="tertiary" + category="secondary" :aria-label="buttonLabel" @click="onToggle" > - <gl-icon - data-testid="work-item-todos-icon" - :class="{ 'gl-fill-blue-500': pendingTodo }" - :name="buttonIcon" - /> + <gl-icon :class="{ 'gl-fill-blue-500': pendingTodo }" :name="buttonIcon" /> </gl-button> </template> |