diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-27 06:10:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-27 06:10:19 +0300 |
commit | 0a0dcc392ca69b7f0567bff6bc1040ded035a11b (patch) | |
tree | 1a892633e20f593140612e700a31c4460b2a08c0 /app/assets/javascripts/work_items | |
parent | 272c39ac05e0d68444114aed58ef2b44e1af30d6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/work_items')
10 files changed, 342 insertions, 18 deletions
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 7b38e838033..3595ab631df 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 @@ -7,7 +7,6 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql'; import { WORK_ITEMS_TYPE_MAP, - WORK_ITEM_TYPE_ENUM_TASK, I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, sprintfWorkItem, } from '../../constants'; @@ -29,7 +28,7 @@ export default { childrenType: { type: String, required: false, - default: WORK_ITEM_TYPE_ENUM_TASK, + default: '', }, childrenIds: { type: Array, @@ -53,7 +52,7 @@ export default { return { fullPath: this.fullPath, searchTerm: this.search?.title || this.search, - types: [this.childrenType], + types: this.childrenType ? [this.childrenType] : [], in: this.search ? 'TITLE' : undefined, }; }, @@ -106,6 +105,7 @@ export default { }, handleFocus() { this.searchStarted = true; + this.$emit('searching', true); }, handleMouseOver() { this.timeout = setTimeout(() => { @@ -115,11 +115,22 @@ export default { handleMouseOut() { clearTimeout(this.timeout); }, + handleBlur() { + this.$emit('searching', false); + }, + focusInputText() { + this.$nextTick(() => { + if (this.areWorkItemsToAddValid) { + this.$refs.tokenSelector.$el.querySelector('input[type="text"]').focus(); + } + }); + }, }, }; </script> <template> <gl-token-selector + ref="tokenSelector" v-model="workItemsToAdd" :dropdown-items="availableWorkItems" :loading="isLoading" @@ -131,13 +142,14 @@ export default { @focus="handleFocus" @mouseover.native="handleMouseOver" @mouseout.native="handleMouseOut" + @token-add="focusInputText" + @token-remove="focusInputText" + @blur="handleBlur" > - <template #token-content="{ token }"> - {{ token.title }} - </template> + <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-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div> + <div class="gl-text-secondary gl-font-sm gl-mr-4">{{ dropdownItem.iid }}</div> <div class="gl-text-truncate">{{ dropdownItem.title }}</div> </div> </template> 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 edecd7addcc..b0a6a3f39d5 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -605,8 +605,10 @@ export default { /> <work-item-relationships v-if="showWorkItemLinkedItems" + :work-item-id="workItem.id" :work-item-iid="workItemIid" :work-item-full-path="workItem.project.fullPath" + :work-item-type="workItem.workItemType.name" @showModal="openInModal" /> <work-item-notes diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index 55440e1603c..456dee8dab1 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -225,7 +225,6 @@ export default { this.error = null; }, addChild() { - this.searchStarted = false; this.$apollo .mutate({ mutation: updateWorkItemMutation, 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 new file mode 100644 index 00000000000..757c186e2e9 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue @@ -0,0 +1,245 @@ +<script> +import { produce } from 'immer'; +import { GlFormGroup, GlForm, GlFormRadioGroup, GlButton, GlAlert } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import WorkItemTokenInput from '../shared/work_item_token_input.vue'; +import addLinkedItemsMutation from '../../graphql/add_linked_items.mutation.graphql'; +import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; +import { + LINK_ITEM_FORM_HEADER_LABEL, + WIDGET_TYPE_LINKED_ITEMS, + LINKED_ITEM_TYPE_VALUE, +} from '../../constants'; + +export default { + components: { + GlForm, + GlButton, + GlFormGroup, + GlFormRadioGroup, + GlAlert, + WorkItemTokenInput, + }, + props: { + workItemId: { + type: String, + required: false, + default: null, + }, + workItemIid: { + type: String, + required: false, + default: null, + }, + workItemFullPath: { + type: String, + required: false, + default: null, + }, + workItemType: { + type: String, + required: false, + default: null, + }, + childrenIds: { + type: Array, + required: false, + default: () => [], + }, + }, + data() { + return { + linkedItemType: LINKED_ITEM_TYPE_VALUE.RELATED, + linkedItemTypes: [ + { + text: this.$options.i18n.relatedToLabel, + value: LINKED_ITEM_TYPE_VALUE.RELATED, + }, + { + text: this.$options.i18n.blockingLabel, + value: LINKED_ITEM_TYPE_VALUE.BLOCKS, + }, + { + text: this.$options.i18n.blockedByLabel, + value: LINKED_ITEM_TYPE_VALUE.BLOCKED_BY, + }, + ], + workItemsToAdd: [], + error: null, + showWorkItemsToAddInvalidMessage: false, + isSubmitting: false, + searchInProgress: false, + }; + }, + computed: { + linkItemFormHeaderLabel() { + return LINK_ITEM_FORM_HEADER_LABEL[this.workItemType]; + }, + workItemsToAddInvalidMessage() { + return this.$options.i18n.addChildErrorMessage; + }, + isSubmitButtonDisabled() { + return this.workItemsToAdd.length <= 0 || !this.areWorkItemsToAddValid; + }, + areWorkItemsToAddValid() { + return this.workItemsToAdd.length < 4; + }, + errorMessage() { + return !this.areWorkItemsToAddValid ? this.$options.i18n.max3ItemsErrorMessage : ''; + }, + }, + methods: { + async linkWorkItem() { + try { + if (this.searchInProgress) { + return; + } + this.isSubmitting = true; + const { + data: { + workItemAddLinkedItems: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: addLinkedItemsMutation, + variables: { + input: { + id: this.workItemId, + linkType: this.linkedItemType, + workItemsIds: this.workItemsToAdd.map((wi) => wi.id), + }, + }, + update: ( + cache, + { + data: { + workItemAddLinkedItems: { workItem }, + }, + }, + ) => { + const queryArgs = { + query: workItemByIidQuery, + variables: { fullPath: this.workItemFullPath, iid: this.workItemIid }, + }; + const sourceData = cache.readQuery(queryArgs); + + if (!sourceData) { + return; + } + + cache.writeQuery({ + ...queryArgs, + data: produce(sourceData, (draftState) => { + const linkedItemsWidget = draftState.workspace.workItems.nodes[0].widgets?.find( + (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS, + ); + + linkedItemsWidget.linkedItems = workItem.widgets?.find( + (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS, + ).linkedItems; + }), + }); + }, + }); + + if (errors.length > 0) { + [this.error] = errors; + return; + } + + this.workItemsToAdd = []; + this.unsetError(); + this.showWorkItemsToAddInvalidMessage = false; + this.linkedItemType = LINKED_ITEM_TYPE_VALUE.RELATED; + this.$emit('submitted'); + } catch (e) { + this.error = this.$options.i18n.addLinkedItemErrorMessage; + } finally { + this.isSubmitting = false; + } + }, + unsetError() { + this.error = null; + }, + }, + i18n: { + addButtonLabel: __('Add'), + relatedToLabel: s__('WorkItem|relates to'), + blockingLabel: s__('WorkItem|blocks'), + blockedByLabel: s__('WorkItem|is blocked by'), + max3ItemsNoteLabel: s__('WorkItem|Add a maximum of 3 items at a time.'), + linkItemInputLabel: s__('WorkItem|the following item(s)'), + addLinkedItemErrorMessage: s__( + 'WorkItem|Something went wrong when trying to link a item. Please try again.', + ), + max3ItemsErrorMessage: s__('WorkItem|Only 3 items can be added at a time.'), + }, +}; +</script> + +<template> + <gl-form + class="gl-new-card-add-form" + data-testid="link-work-item-form" + @submit.stop.prevent="linkWorkItem" + > + <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError"> + {{ error }} + </gl-alert> + <gl-form-group + :label="linkItemFormHeaderLabel" + label-for="linked-item-type-radio" + label-class="label-bold" + class="gl-mb-3" + > + <gl-form-radio-group + id="linked-item-type-radio" + v-model="linkedItemType" + :options="linkedItemTypes" + :checked="linkedItemType" + /> + </gl-form-group> + <p class="gl-font-weight-bold gl-mb-2"> + {{ $options.i18n.linkItemInputLabel }} + </p> + <div class="gl-mb-5"> + <work-item-token-input + v-model="workItemsToAdd" + class="gl-mb-2" + :parent-work-item-id="workItemId" + :children-ids="childrenIds" + :are-work-items-to-add-valid="areWorkItemsToAddValid" + :full-path="workItemFullPath" + :max-selection-limit="3" + @searching="searchInProgress = $event" + /> + <div v-if="errorMessage" class="gl-mb-2 gl-text-red-500"> + {{ $options.i18n.max3ItemsErrorMessage }} + </div> + <div v-if="!errorMessage" data-testid="max-work-item-note" class="gl-text-gray-500"> + {{ $options.i18n.max3ItemsNoteLabel }} + </div> + <div + v-if="showWorkItemsToAddInvalidMessage" + class="gl-text-red-500" + data-testid="work-items-invalid" + > + {{ workItemsToAddInvalidMessage }} + </div> + </div> + <gl-button + data-testid="link-work-item-button" + category="primary" + variant="confirm" + size="small" + type="submit" + :disabled="isSubmitButtonDisabled" + :loading="isSubmitting" + class="gl-mr-2" + > + {{ $options.i18n.addButtonLabel }} + </gl-button> + <gl-button category="secondary" size="small" @click="$emit('cancel')"> + {{ s__('WorkItem|Cancel') }} + </gl-button> + </gl-form> +</template> 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 cbe830f9565..279e6ad01b3 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 @@ -33,7 +33,7 @@ export default { }; </script> <template> - <div> + <div data-testid="work-item-linked-items-list"> <h4 v-if="heading" data-testid="work-items-list-heading" 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 4f6879e9605..d32e3d3a5e5 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 @@ -8,6 +8,7 @@ import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants import WidgetWrapper from '../widget_wrapper.vue'; import WorkItemRelationshipList from './work_item_relationship_list.vue'; +import WorkItemAddRelationshipForm from './work_item_add_relationship_form.vue'; export default { components: { @@ -16,8 +17,14 @@ export default { GlButton, WidgetWrapper, WorkItemRelationshipList, + WorkItemAddRelationshipForm, }, props: { + workItemId: { + type: String, + required: false, + default: null, + }, workItemIid: { type: String, required: true, @@ -26,6 +33,11 @@ export default { type: String, required: true, }, + workItemType: { + type: String, + required: false, + default: null, + }, }, apollo: { workItem: { @@ -74,13 +86,13 @@ export default { linksRelatesTo: [], linksIsBlockedBy: [], linksBlocks: [], + isShownLinkItemForm: false, widgetName: 'linkeditems', }; }, computed: { - canUpdate() { - // This will be false untill we implement remove item mutation - return false; + canAdminWorkItemLink() { + return this.workItem?.userPermissions?.adminWorkItemLink; }, isLoading() { return this.$apollo.queries.workItem.loading; @@ -91,11 +103,22 @@ export default { linkedWorkItems() { return this.linkedWorkItemsWidget?.linkedItems?.nodes || []; }, + childrenIds() { + return this.linkedWorkItems.map((item) => item.workItem.id); + }, linkedWorkItemsCount() { return this.linkedWorkItems.length; }, isEmptyRelatedWorkItems() { - return !this.error && this.linkedWorkItems.length === 0; + return !this.isShownLinkItemForm && !this.error && this.linkedWorkItems.length === 0; + }, + }, + methods: { + showLinkItemForm() { + this.isShownLinkItemForm = true; + }, + hideLinkItemForm() { + this.isShownLinkItemForm = false; }, }, i18n: { @@ -131,12 +154,28 @@ export default { </div> </template> <template #header-right> - <gl-button size="small" class="gl-ml-3"> + <gl-button + v-if="canAdminWorkItemLink" + data-testid="link-item-add-button" + size="small" + class="gl-ml-3" + @click="showLinkItemForm" + > <slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot> </gl-button> </template> <template #body> <div class="gl-new-card-content"> + <work-item-add-relationship-form + v-if="isShownLinkItemForm" + :work-item-id="workItemId" + :work-item-iid="workItemIid" + :work-item-full-path="workItemFullPath" + :children-ids="childrenIds" + :work-item-type="workItemType" + @submitted="hideLinkItemForm" + @cancel="hideLinkItemForm" + /> <gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" /> <template v-else> <div v-if="isEmptyRelatedWorkItems" data-testid="links-empty"> @@ -154,7 +193,7 @@ export default { :linked-items="linksBlocks" :heading="$options.i18n.blockingTitle" :work-item-full-path="workItemFullPath" - :can-update="canUpdate" + :can-update="false" @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })" /> <work-item-relationship-list @@ -166,7 +205,7 @@ export default { :linked-items="linksIsBlockedBy" :heading="$options.i18n.blockedByTitle" :work-item-full-path="workItemFullPath" - :can-update="canUpdate" + :can-update="false" @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })" /> <work-item-relationship-list @@ -174,7 +213,7 @@ export default { :linked-items="linksRelatesTo" :heading="$options.i18n.relatedToTitle" :work-item-full-path="workItemFullPath" - :can-update="canUpdate" + :can-update="false" @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })" /> </template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 2b118247426..04ef2a65aab 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -113,7 +113,7 @@ export const I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL = s__( ); export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => { - const workItemType = workItemTypeArg || s__('WorkItem|Work item'); + const workItemType = workItemTypeArg || s__('WorkItem|item'); return capitalizeFirstCharacter( sprintf(msg, { workItemType: workItemType.toLocaleLowerCase(), @@ -186,8 +186,11 @@ export const WORK_ITEM_NAME_TO_ICON_MAP = { Issue: 'issue-type-issue', Task: 'issue-type-task', Objective: 'issue-type-objective', + Incident: 'issue-type-incident', // eslint-disable-next-line @gitlab/require-i18n-strings 'Key Result': 'issue-type-keyresult', + // eslint-disable-next-line @gitlab/require-i18n-strings + 'Test Case': 'issue-type-test-case', }; export const FORM_TYPES = { @@ -262,3 +265,15 @@ export const LINKED_CATEGORIES_MAP = { IS_BLOCKED_BY: 'is_blocked_by', BLOCKS: 'blocks', }; + +export const LINKED_ITEM_TYPE_VALUE = { + RELATED: 'RELATED', + BLOCKED_BY: 'BLOCKED_BY', + BLOCKS: 'BLOCKS', +}; + +export const LINK_ITEM_FORM_HEADER_LABEL = { + [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: s__('WorkItem|The current objective'), + [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: s__('WorkItem|The current key result'), + [WORK_ITEM_TYPE_VALUE_TASK]: s__('WorkItem|The current task'), +}; diff --git a/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql b/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql new file mode 100644 index 00000000000..ba12c7f9b51 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql @@ -0,0 +1,10 @@ +#import "./work_item.fragment.graphql" + +mutation addLinkedItems($input: WorkItemAddLinkedItemsInput!) { + workItemAddLinkedItems(input: $input) { + workItem { + ...WorkItem + } + errors + } +} diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql index 7d63af448d4..2be436aa8c2 100644 --- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql +++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql @@ -9,6 +9,7 @@ query projectWorkItems( workItems(search: $searchTerm, types: $types, in: $in) { nodes { id + iid title state confidential diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql index 1ae5617f04d..fac99310890 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql @@ -33,6 +33,7 @@ fragment WorkItem on WorkItem { adminParentLink setWorkItemMetadata createNote + adminWorkItemLink } widgets { ...WorkItemWidgets |