diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-14 06:11:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-14 06:11:17 +0300 |
commit | 886077c08875d595fc88a689f1ac841252813513 (patch) | |
tree | f47e7078289041816fb8a540bb12218a2cfccc27 /app/assets/javascripts/work_items | |
parent | 71df3555b295779dec870c8ad59c30b6a47c837e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/work_items')
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 + } + } + } + } } } |