Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-09-27 06:10:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-09-27 06:10:19 +0300
commit0a0dcc392ca69b7f0567bff6bc1040ded035a11b (patch)
tree1a892633e20f593140612e700a31c4460b2a08c0 /app/assets/javascripts/work_items
parent272c39ac05e0d68444114aed58ef2b44e1af30d6 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/work_items')
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue26
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue245
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue55
-rw-r--r--app/assets/javascripts/work_items/constants.js17
-rw-r--r--app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql10
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
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