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-14 06:11:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-09-14 06:11:17 +0300
commit886077c08875d595fc88a689f1ac841252813513 (patch)
treef47e7078289041816fb8a540bb12218a2cfccc27 /app/assets/javascripts/work_items
parent71df3555b295779dec870c8ad59c30b6a47c837e (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_link_child_contents.vue2
-rw-r--r--app/assets/javascripts/work_items/components/widget_wrapper.vue19
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue3
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue3
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue61
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue155
-rw-r--r--app/assets/javascripts/work_items/constants.js6
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql23
11 files changed, 262 insertions, 21 deletions
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 b10a3727e9f..f50cfac90f7 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
@@ -149,7 +149,7 @@ export default {
</span>
<gl-link
:href="childPath"
- class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
+ class="gl-text-truncate gl-font-weight-semibold"
data-testid="item-title"
@click="$emit('click', $event)"
@mouseover="$emit('mouseover')"
diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue
index f343f787358..27de858fe4e 100644
--- a/app/assets/javascripts/work_items/components/widget_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue
@@ -14,6 +14,11 @@ export default {
required: false,
default: '',
},
+ widgetName: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -30,6 +35,12 @@ export default {
isOpenString() {
return this.isOpen ? 'true' : 'false';
},
+ anchorLink() {
+ return `#${this.widgetName}`;
+ },
+ anchorLinkId() {
+ return `user-content-${this.widgetName}-links`;
+ },
},
methods: {
hide() {
@@ -46,14 +57,14 @@ export default {
</script>
<template>
- <div id="tasks" class="gl-new-card" :aria-expanded="isOpenString">
+ <div :id="widgetName" class="gl-new-card" :aria-expanded="isOpenString">
<div class="gl-new-card-header">
<div class="gl-new-card-title-wrapper">
<h3 class="gl-new-card-title">
<gl-link
- id="user-content-tasks-links"
- class="anchor position-absolute gl-text-decoration-none"
- href="#tasks"
+ :id="anchorLinkId"
+ class="gl-text-decoration-none"
+ :href="anchorLink"
aria-hidden="true"
/>
<slot name="header"></slot>
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 6beca682f8f..edecd7addcc 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -606,7 +606,8 @@ export default {
<work-item-relationships
v-if="showWorkItemLinkedItems"
:work-item-iid="workItemIid"
- :work-item-fullpath="workItem.project.fullPath"
+ :work-item-full-path="workItem.project.fullPath"
+ @showModal="openInModal"
/>
<work-item-notes
v-if="workItemNotes"
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 bf427feaa35..9d9414b5399 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
@@ -56,14 +56,14 @@ export default {
return isLoggedIn() && this.canUpdate;
},
treeRootWrapper() {
- return this.canReorder ? Draggable : 'div';
+ return this.canReorder ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
...defaultSortableOptions,
fallbackOnBody: false,
group: 'sortable-container',
- tag: 'div',
+ tag: 'ul',
'ghost-class': 'tree-item-drag-active',
'data-parent-id': this.workItemId,
value: this.children,
@@ -248,6 +248,7 @@ export default {
<component
:is="treeRootWrapper"
v-bind="treeRootOptions"
+ class="content-list"
:class="{ 'gl-cursor-grab sortable-container': canReorder }"
@end="handleDragOnEnd"
>
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 4c85652178f..679287338c8 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
@@ -213,7 +213,7 @@ export default {
</script>
<template>
- <div class="tree-item">
+ <li class="tree-item">
<div
class="gl-display-flex gl-align-items-flex-start"
:class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }"
@@ -250,5 +250,5 @@ export default {
@removeChild="removeChild"
@click="$emit('click', $event)"
/>
- </div>
+ </li>
</template>
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 a0ff693e156..eb836007e75 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
@@ -103,6 +103,7 @@ export default {
isReportDrawerOpen: false,
reportedUserId: 0,
reportedUrl: '',
+ widgetName: 'tasks',
};
},
computed: {
@@ -166,7 +167,6 @@ export default {
this.updateWorkItemIdUrlQuery(child);
},
async closeModal() {
- this.activeChild = {};
this.updateWorkItemIdUrlQuery();
},
handleWorkItemDeleted(child) {
@@ -206,6 +206,7 @@ export default {
<widget-wrapper
ref="wrapper"
:error="error"
+ :widget-name="widgetName"
data-testid="work-item-links"
@dismissAlert="error = undefined"
>
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 246eac82c78..bc3f5201fb8 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
@@ -64,6 +64,7 @@ export default {
isShownAddForm: false,
formType: null,
childType: null,
+ widgetName: 'tasks',
};
},
computed: {
@@ -101,6 +102,7 @@ export default {
<template>
<widget-wrapper
ref="wrapper"
+ :widget-name="widgetName"
:error="error"
data-testid="work-item-tree"
@dismissAlert="error = undefined"
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
new file mode 100644
index 00000000000..cbe830f9565
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -0,0 +1,61 @@
+<script>
+import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
+import { workItemPath } from '../../utils';
+
+export default {
+ components: {
+ WorkItemLinkChildContents,
+ },
+ props: {
+ linkedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ heading: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ workItemFullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ linkedItemPath(fullPath, id) {
+ return workItemPath(fullPath, id);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h4
+ v-if="heading"
+ data-testid="work-items-list-heading"
+ class="gl-font-sm gl-font-weight-semibold gl-text-gray-700 gl-mx-2 gl-mt-3 gl-mb-2"
+ >
+ {{ heading }}
+ </h4>
+ <div class="work-items-list-body">
+ <ul ref="list" class="work-items-list content-list">
+ <li
+ v-for="linkedItem in linkedItems"
+ :key="linkedItem.workItem.id"
+ class="gl-pt-0! gl-pb-0! gl-border-b-0!"
+ >
+ <work-item-link-child-contents
+ :child-item="linkedItem.workItem"
+ :can-update="canUpdate"
+ :child-path="linkedItemPath(workItemFullPath, linkedItem.workItem.iid)"
+ @click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
+ />
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
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 7e58627b92f..4f6879e9605 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,37 +1,135 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants';
+
import WidgetWrapper from '../widget_wrapper.vue';
+import WorkItemRelationshipList from './work_item_relationship_list.vue';
export default {
components: {
- WidgetWrapper,
+ GlLoadingIcon,
+ GlIcon,
GlButton,
+ WidgetWrapper,
+ WorkItemRelationshipList,
},
props: {
workItemIid: {
type: String,
required: true,
},
- workItemFullpath: {
+ workItemFullPath: {
type: String,
required: true,
},
},
+ apollo: {
+ workItem: {
+ query: workItemByIidQuery,
+ variables() {
+ return {
+ fullPath: this.workItemFullPath,
+ iid: this.workItemIid,
+ };
+ },
+ update(data) {
+ return data.workspace.workItems.nodes[0] ?? {};
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ skip() {
+ return !this.workItemIid;
+ },
+ error(e) {
+ this.error = e.message || this.$options.i18n.fetchError;
+ },
+ async result() {
+ // When work items are switched in a modal, the data props are not getting reset.
+ // Thus, duplicating the work items in the list.
+ // Here, the existing list are cleared before the new items are pushed.
+ this.linksRelatesTo = [];
+ this.linksIsBlockedBy = [];
+ this.linksBlocks = [];
+
+ this.linkedWorkItems.forEach((item) => {
+ if (item.linkType === LINKED_CATEGORIES_MAP.RELATES_TO) {
+ this.linksRelatesTo.push(item);
+ } else if (item.linkType === LINKED_CATEGORIES_MAP.IS_BLOCKED_BY) {
+ this.linksIsBlockedBy.push(item);
+ } else if (item.linkType === LINKED_CATEGORIES_MAP.BLOCKS) {
+ this.linksBlocks.push(item);
+ }
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ error: '',
+ linksRelatesTo: [],
+ linksIsBlockedBy: [],
+ linksBlocks: [],
+ widgetName: 'linkeditems',
+ };
+ },
+ computed: {
+ canUpdate() {
+ // This will be false untill we implement remove item mutation
+ return false;
+ },
+ isLoading() {
+ return this.$apollo.queries.workItem.loading;
+ },
+ linkedWorkItemsWidget() {
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS);
+ },
+ linkedWorkItems() {
+ return this.linkedWorkItemsWidget?.linkedItems?.nodes || [];
+ },
+ linkedWorkItemsCount() {
+ return this.linkedWorkItems.length;
+ },
+ isEmptyRelatedWorkItems() {
+ return !this.error && this.linkedWorkItems.length === 0;
+ },
+ },
i18n: {
title: s__('WorkItem|Linked Items'),
+ fetchError: s__('WorkItem|Something went wrong when fetching tasks. Please refresh this page.'),
emptyStateMessage: s__(
"WorkItem|Link work items together to show that they're related or that one is blocking others.",
),
+ addChildButtonLabel: s__('WorkItem|Add'),
+ relatedToTitle: s__('WorkItem|Related to'),
+ blockingTitle: s__('WorkItem|Blocking'),
+ blockedByTitle: s__('WorkItem|Blocked by'),
addLinkedWorkItemButtonLabel: s__('WorkItem|Add'),
},
};
</script>
<template>
- <widget-wrapper class="work-item-relationships">
- <template #header>{{ $options.i18n.title }}</template>
+ <widget-wrapper
+ :error="error"
+ class="work-item-relationships"
+ :widget-name="widgetName"
+ @dismissAlert="error = undefined"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper">
+ <h3 class="gl-new-card-title">
+ {{ $options.i18n.title }}
+ </h3>
+ <div v-if="linkedWorkItemsCount" class="gl-new-card-count">
+ <gl-icon name="link" class="gl-mr-2" />
+ <span data-testid="linked-items-count">{{ linkedWorkItemsCount }}</span>
+ </div>
+ </div>
+ </template>
<template #header-right>
<gl-button size="small" class="gl-ml-3">
<slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
@@ -39,11 +137,48 @@ export default {
</template>
<template #body>
<div class="gl-new-card-content">
- <div data-testid="links-empty">
- <p class="gl-new-card-empty">
- {{ $options.i18n.emptyStateMessage }}
- </p>
- </div>
+ <gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" />
+ <template v-else>
+ <div v-if="isEmptyRelatedWorkItems" data-testid="links-empty">
+ <p class="gl-new-card-empty">
+ {{ $options.i18n.emptyStateMessage }}
+ </p>
+ </div>
+ <template v-else>
+ <work-item-relationship-list
+ v-if="linksBlocks.length"
+ :class="{
+ 'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
+ linksIsBlockedBy.length,
+ }"
+ :linked-items="linksBlocks"
+ :heading="$options.i18n.blockingTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ <work-item-relationship-list
+ v-if="linksIsBlockedBy.length"
+ :class="{
+ 'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
+ linksRelatesTo.length,
+ }"
+ :linked-items="linksIsBlockedBy"
+ :heading="$options.i18n.blockedByTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ <work-item-relationship-list
+ v-if="linksRelatesTo.length"
+ :linked-items="linksRelatesTo"
+ :heading="$options.i18n.relatedToTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ </template>
+ </template>
</div>
</template>
</widget-wrapper>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 2834799e4e8..2b118247426 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -256,3 +256,9 @@ export const WORK_ITEM_TO_ISSUE_MAP = {
[WIDGET_TYPE_HEALTH_STATUS]: 'healthStatus',
[WIDGET_TYPE_AWARD_EMOJI]: 'awardEmoji',
};
+
+export const LINKED_CATEGORIES_MAP = {
+ RELATES_TO: 'relates_to',
+ IS_BLOCKED_BY: 'is_blocked_by',
+ BLOCKS: 'blocks',
+};
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index 14cb6f8415c..ffc9fe2f7f7 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -103,5 +103,28 @@ fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetLinkedItems {
type
+ linkedItems {
+ nodes {
+ linkId
+ linkType
+ workItem {
+ id
+ iid
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ createdAt
+ closedAt
+ widgets {
+ ...WorkItemMetadataWidgets
+ }
+ }
+ }
+ }
}
}