diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-13 18:07:56 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-13 18:07:56 +0300 |
commit | 0d55697d64b5f053bbd0f69da2962e7478097de3 (patch) | |
tree | 33dc75892313554223fb7dadd88e1c8875053d88 /app/assets/javascripts/work_items | |
parent | 9fdb3dbd6bacb125d40290aac8409da2f9fe19fc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/work_items')
10 files changed, 495 insertions, 53 deletions
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 376d71c7b31..e0ebc714dbb 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -1,5 +1,6 @@ <script> import { isEmpty } from 'lodash'; +import { produce } from 'immer'; import { GlAlert, GlSkeletonLoader, @@ -11,6 +12,7 @@ import { GlEmptyState, } from '@gitlab/ui'; import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg'; +import * as Sentry from '@sentry/browser'; import { s__ } from '~/locale'; import { parseBoolean } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/url_utility'; @@ -269,6 +271,12 @@ export default { id: this.workItemId, }; }, + children() { + const widgetHierarchy = this.workItem.widgets.find( + (widget) => widget.type === WIDGET_TYPE_HIERARCHY, + ); + return widgetHierarchy.children.nodes; + }, }, methods: { isWidgetPresent(type) { @@ -326,6 +334,74 @@ export default { this.error = this.$options.i18n.fetchError; document.title = s__('404|Not found'); }, + addChild(child) { + const { defaultClient: client } = this.$apollo.provider.clients; + this.toggleChildFromCache(child, child.id, client); + }, + toggleChildFromCache(workItem, childId, store) { + const sourceData = store.readQuery({ + query: getWorkItemQuery(this.fetchByIid), + variables: this.queryVariables, + }); + + const newData = produce(sourceData, (draftState) => { + const widgetHierarchy = draftState.workItem.widgets.find( + (widget) => widget.type === WIDGET_TYPE_HIERARCHY, + ); + + const index = widgetHierarchy.children.nodes.findIndex((child) => child.id === childId); + + if (index >= 0) { + widgetHierarchy.children.nodes.splice(index, 1); + } else { + widgetHierarchy.children.nodes.unshift(workItem); + } + }); + + store.writeQuery({ + query: getWorkItemQuery(this.fetchByIid), + variables: this.queryVariables, + data: newData, + }); + }, + async updateWorkItem(workItem, childId, parentId) { + return this.$apollo.mutate({ + mutation: updateWorkItemMutation, + variables: { input: { id: childId, hierarchyWidget: { parentId } } }, + update: (store) => this.toggleChildFromCache(workItem, childId, store), + }); + }, + async undoChildRemoval(workItem, childId) { + try { + const { data } = await this.updateWorkItem(workItem, childId, this.workItem.id); + + if (data.workItemUpdate.errors.length === 0) { + this.activeToast?.hide(); + } + } catch (error) { + this.updateError = s__('WorkItem|Something went wrong while undoing child removal.'); + Sentry.captureException(error); + } finally { + this.activeToast?.hide(); + } + }, + async removeChild(childId) { + try { + const { data } = await this.updateWorkItem(null, childId, null); + + if (data.workItemUpdate.errors.length === 0) { + this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), { + action: { + text: s__('WorkItem|Undo'), + onClick: this.undoChildRemoval.bind(this, data.workItemUpdate.workItem, childId), + }, + }); + } + } catch (error) { + this.updateError = s__('WorkItem|Something went wrong while removing child.'); + Sentry.captureException(error); + } + }, }, WORK_ITEM_TYPE_VALUE_OBJECTIVE, }; @@ -507,6 +583,11 @@ export default { v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE" :work-item-type="workItemType" :work-item-id="workItem.id" + :children="children" + :can-update="canUpdate" + :project-path="fullPath" + @addWorkItemChild="addChild" + @removeChild="removeChild" /> <gl-empty-state v-if="error" diff --git a/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue index a91133ce1ac..cbe67aa622c 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue @@ -1,5 +1,5 @@ <script> -import { GlDropdown, GlDropdownDivider, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui'; +import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; @@ -30,7 +30,6 @@ export default { objectiveActionItems, components: { GlDropdown, - GlDropdownDivider, GlDropdownSectionHeader, GlDropdownItem, }, @@ -53,6 +52,10 @@ export default { {{ item.title }} </gl-dropdown-item> + <!-- TODO: Uncomment once following two issues addressed --> + <!-- https://gitlab.com/gitlab-org/gitlab/-/issues/381833 --> + <!-- https://gitlab.com/gitlab-org/gitlab/-/issues/385084 --> + <!-- <gl-dropdown-divider /> <gl-dropdown-section-header>{{ __('Key result') }}</gl-dropdown-section-header> <gl-dropdown-item @@ -62,5 +65,6 @@ export default { > {{ item.title }} </gl-dropdown-item> + --> </gl-dropdown> </template> 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 7141d9c6f89..b04da53bf89 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,12 +1,21 @@ <script> import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; -import { STATE_OPEN } from '../../constants'; +import { + STATE_OPEN, + TASK_TYPE_NAME, + WORK_ITEM_TYPE_VALUE_OBJECTIVE, + WIDGET_TYPE_HIERARCHY, + WORK_ITEM_NAME_TO_ICON_MAP, +} from '../../constants'; +import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql'; import WorkItemLinksMenu from './work_item_links_menu.vue'; +import WorkItemTreeChildren from './work_item_tree_children.vue'; export default { components: { @@ -14,6 +23,7 @@ export default { GlIcon, RichTimestampTooltip, WorkItemLinksMenu, + WorkItemTreeChildren, }, directives: { GlTooltip: GlTooltipDirective, @@ -35,16 +45,45 @@ export default { type: Object, required: true, }, + hasIndirectChildren: { + type: Boolean, + required: false, + default: true, + }, + workItemType: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + isExpanded: false, + children: [], + isLoadingChildren: false, + }; }, computed: { + canHaveChildren() { + return this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE; + }, isItemOpen() { return this.childItem.state === STATE_OPEN; }, - iconClass() { - return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500'; + childItemType() { + return this.childItem.workItemType.name; }, iconName() { - return this.isItemOpen ? 'issue-open-m' : 'issue-close'; + if (this.childItemType === TASK_TYPE_NAME) { + return this.isItemOpen ? 'issue-open-m' : 'issue-close'; + } + return WORK_ITEM_NAME_TO_ICON_MAP[this.childItemType]; + }, + iconClass() { + if (this.childItemType === TASK_TYPE_NAME) { + return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500'; + } + return ''; }, stateTimestamp() { return this.isItemOpen ? this.childItem.createdAt : this.childItem.closedAt; @@ -55,55 +94,132 @@ export default { childPath() { return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(this.childItem.id)}`; }, + hasChildren() { + return this.getWidgetHierarchyForChild(this.childItem)?.hasChildren; + }, + chevronType() { + return this.isExpanded ? 'chevron-down' : 'chevron-right'; + }, + chevronTooltip() { + return this.isExpanded ? __('Collapse') : __('Expand'); + }, + }, + methods: { + toggleItem() { + this.isExpanded = !this.isExpanded; + if (this.children.length === 0 && this.hasChildren) { + this.fetchChildren(); + } + }, + getWidgetHierarchyForChild(workItem) { + const widgetHierarchy = workItem?.widgets?.find( + (widget) => widget.type === WIDGET_TYPE_HIERARCHY, + ); + return widgetHierarchy || {}; + }, + async fetchChildren() { + this.isLoadingChildren = true; + try { + const { data } = await this.$apollo.query({ + query: getWorkItemTreeQuery, + variables: { + id: this.childItem.id, + }, + }); + this.children = this.getWidgetHierarchyForChild(data?.workItem).children.nodes; + } catch (error) { + this.isExpanded = !this.isExpanded; + createAlert({ + message: s__('Hierarchy|Something went wrong while fetching children.'), + captureError: true, + error, + }); + } finally { + this.isLoadingChildren = false; + } + }, }, }; </script> <template> - <div - class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32" - data-testid="links-child" - > - <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1"> - <span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon"> - <gl-icon :name="iconName" :class="iconClass" :aria-label="stateTimestampTypeText" /> - </span> - <rich-timestamp-tooltip - :target="`stateIcon-${childItem.id}`" - :raw-timestamp="stateTimestamp" - :timestamp-type-text="stateTimestampTypeText" - /> - <gl-icon - v-if="childItem.confidential" - v-gl-tooltip.top - name="eye-slash" - class="gl-mr-2 gl-text-orange-500" - data-testid="confidential-icon" - :aria-label="__('Confidential')" - :title="__('Confidential')" - /> - <gl-button - :href="childPath" - category="tertiary" - variant="link" - class="gl-text-truncate gl-max-w-80 gl-text-black-normal!" - @click="$emit('click', $event)" - @mouseover="$emit('mouseover')" - @mouseout="$emit('mouseout')" - > - {{ childItem.title }} - </gl-button> - </div> + <div> <div - v-if="canUpdate" - class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center" + class="gl-display-flex gl-align-items-center gl-mb-3" + :class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }" > - <work-item-links-menu - :work-item-id="childItem.id" - :parent-work-item-id="issuableGid" - data-testid="links-menu" - @removeChild="$emit('remove', childItem.id)" + <gl-button + v-if="hasChildren" + v-gl-tooltip.viewport + :title="chevronTooltip" + :aria-label="chevronTooltip" + :icon="chevronType" + category="tertiary" + :loading="isLoadingChildren" + class="gl-px-0! gl-py-4! gl-mr-3" + data-testid="expand-child" + @click="toggleItem" /> + <div + class="gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-bg-white gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32" + data-testid="links-child" + > + <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1"> + <span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon"> + <gl-icon + class="gl-text-secondary" + :class="iconClass" + :name="iconName" + :aria-label="stateTimestampTypeText" + /> + </span> + <rich-timestamp-tooltip + :target="`stateIcon-${childItem.id}`" + :raw-timestamp="stateTimestamp" + :timestamp-type-text="stateTimestampTypeText" + /> + <gl-icon + v-if="childItem.confidential" + v-gl-tooltip.top + name="eye-slash" + class="gl-mr-2 gl-text-orange-500" + data-testid="confidential-icon" + :aria-label="__('Confidential')" + :title="__('Confidential')" + /> + <gl-button + :href="childPath" + category="tertiary" + variant="link" + class="gl-text-truncate gl-max-w-80 gl-text-black-normal!" + @click="$emit('click', $event)" + @mouseover="$emit('mouseover')" + @mouseout="$emit('mouseout')" + > + {{ childItem.title }} + </gl-button> + </div> + <div + v-if="canUpdate" + class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center" + > + <work-item-links-menu + :work-item-id="childItem.id" + :parent-work-item-id="issuableGid" + data-testid="links-menu" + @removeChild="$emit('removeChild', childItem.id)" + /> + </div> + </div> </div> + <work-item-tree-children + v-if="isExpanded" + :project-path="projectPath" + :can-update="canUpdate" + :work-item-id="issuableGid" + :work-item-type="workItemType" + :children="children" + @removeChild="fetchChildren" + /> </div> </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 e96b56f13a9..faadb5fa6fa 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 @@ -412,7 +412,7 @@ export default { @click="openChild(child, $event)" @mouseover="prefetchWorkItem(child)" @mouseout="clearPrefetching" - @remove="removeChild" + @removeChild="removeChild" /> <work-item-detail-modal ref="modal" 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 9c09ee3a66a..b4bb8a99452 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,15 +1,26 @@ <script> import { GlButton } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { isEmpty } from 'lodash'; +import { __ } from '~/locale'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import { getParameterByName } from '~/lib/utils/url_utility'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { FORM_TYPES, + WIDGET_TYPE_HIERARCHY, WORK_ITEMS_TREE_TEXT_MAP, WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_KEY_RESULT, } from '../../constants'; +import workItemQuery from '../../graphql/work_item.query.graphql'; +import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; import OkrActionsSplitButton from './okr_actions_split_button.vue'; import WorkItemLinksForm from './work_item_links_form.vue'; +import WorkItemLinkChild from './work_item_link_child.vue'; export default { FORM_TYPES, @@ -20,7 +31,9 @@ export default { GlButton, OkrActionsSplitButton, WorkItemLinksForm, + WorkItemLinkChild, }, + mixins: [glFeatureFlagMixin()], props: { workItemType: { type: String, @@ -30,6 +43,20 @@ export default { type: String, required: true, }, + children: { + type: Array, + required: false, + default: () => [], + }, + canUpdate: { + type: Boolean, + required: false, + default: false, + }, + projectPath: { + type: String, + required: true, + }, }, data() { return { @@ -38,6 +65,7 @@ export default { error: null, formType: null, childType: null, + prefetchedWorkItem: null, }; }, computed: { @@ -45,8 +73,41 @@ export default { return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down'; }, toggleLabel() { - return this.isOpen ? s__('WorkItem|Collapse tasks') : s__('WorkItem|Expand tasks'); + return this.isOpen ? __('Collapse') : __('Expand'); + }, + fetchByIid() { + return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path')); + }, + childrenIds() { + return this.children.map((c) => c.id); + }, + hasIndirectChildren() { + return this.children + .map( + (child) => child.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY) || {}, + ) + .some((hierarchy) => hierarchy.hasChildren); }, + childUrlParams() { + const params = {}; + if (this.fetchByIid) { + const iid = getParameterByName('work_item_iid'); + if (iid) { + params.iid = iid; + } + } else { + const workItemId = getParameterByName('work_item_id'); + if (workItemId) { + params.id = convertToGraphQLId(TYPE_WORK_ITEM, workItemId); + } + } + return params; + }, + }, + mounted() { + if (!isEmpty(this.childUrlParams)) { + this.addWorkItemQuery(this.childUrlParams); + } }, methods: { toggle() { @@ -64,6 +125,37 @@ export default { hideAddForm() { this.isShownAddForm = false; }, + addWorkItemQuery({ id, iid }) { + const variables = this.fetchByIid + ? { + fullPath: this.projectPath, + iid, + } + : { + id, + }; + this.$apollo.addSmartQuery('prefetchedWorkItem', { + query() { + return this.fetchByIid ? workItemByIidQuery : workItemQuery; + }, + variables, + update(data) { + return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem; + }, + context: { + isSingleRequest: true, + }, + }); + }, + prefetchWorkItem({ id, iid }) { + this.prefetch = setTimeout( + () => this.addWorkItemQuery({ id, iid }), + DEFAULT_DEBOUNCE_AND_THROTTLE_MS, + ); + }, + clearPrefetching() { + clearTimeout(this.prefetch); + }, }, }; </script> @@ -113,7 +205,7 @@ export default { :class="{ 'gl-p-5 gl-pb-3': !error }" data-testid="tree-body" > - <div v-if="!isShownAddForm && !error" data-testid="tree-empty"> + <div v-if="!isShownAddForm && !error && children.length === 0" data-testid="tree-empty"> <p class="gl-mb-3"> {{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].empty }} </p> @@ -125,8 +217,23 @@ export default { :issuable-gid="workItemId" :form-type="formType" :children-type="childType" + :children-ids="childrenIds" + @addWorkItemChild="$emit('addWorkItemChild', $event)" @cancel="hideAddForm" /> + <work-item-link-child + v-for="child in children" + :key="child.id" + :project-path="projectPath" + :can-update="canUpdate" + :issuable-gid="workItemId" + :child-item="child" + :work-item-type="workItemType" + :has-indirect-children="hasIndirectChildren" + @mouseover="prefetchWorkItem(child)" + @mouseout="clearPrefetching" + @removeChild="$emit('removeChild', $event)" + /> </div> </div> </template> 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 new file mode 100644 index 00000000000..911cac4de88 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue @@ -0,0 +1,68 @@ +<script> +import { createAlert } from '~/flash'; +import { s__ } from '~/locale'; + +import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; + +export default { + components: { + WorkItemLinkChild: () => import('./work_item_link_child.vue'), + }, + props: { + workItemType: { + type: String, + required: true, + }, + workItemId: { + type: String, + required: true, + }, + children: { + type: Array, + required: false, + default: () => [], + }, + canUpdate: { + type: Boolean, + required: false, + default: false, + }, + projectPath: { + type: String, + required: true, + }, + }, + methods: { + async updateWorkItem(childId) { + try { + await this.$apollo.mutate({ + mutation: updateWorkItemMutation, + variables: { input: { id: childId, hierarchyWidget: { parentId: null } } }, + }); + this.$emit('removeChild'); + } catch (error) { + createAlert({ + message: s__('Hierarchy|Something went wrong while removing a child item.'), + captureError: true, + error, + }); + } + }, + }, +}; +</script> + +<template> + <div class="gl-ml-6"> + <work-item-link-child + v-for="child in children" + :key="child.id" + :project-path="projectPath" + :can-update="canUpdate" + :issuable-gid="workItemId" + :child-item="child" + :work-item-type="workItemType" + @removeChild="updateWorkItem" + /> + </div> +</template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 939cc416b9e..368bb6a85a4 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -116,7 +116,7 @@ export const WORK_ITEMS_TYPE_MAP = { }, [WORK_ITEM_TYPE_ENUM_KEY_RESULT]: { icon: `issue-type-issue`, - name: s__('WorkItem|Key result'), + name: s__('WorkItem|Key Result'), }, }; @@ -127,6 +127,14 @@ export const WORK_ITEMS_TREE_TEXT_MAP = { }, }; +export const WORK_ITEM_NAME_TO_ICON_MAP = { + Issue: 'issue-type-issue', + Task: 'issue-type-task', + Objective: 'issue-type-objective', + // eslint-disable-next-line @gitlab/require-i18n-strings + 'Key Result': 'issue-type-key-result', +}; + export const FORM_TYPES = { create: 'create', add: 'add', diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql index a37e5b869f6..7fcf622cdb2 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql @@ -24,6 +24,8 @@ query workItemLinksQuery($id: WorkItemID!) { confidential workItemType { id + name + iconName } title state diff --git a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql new file mode 100644 index 00000000000..a850d002de8 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql @@ -0,0 +1,47 @@ +query workItemTreeQuery($id: WorkItemID!) { + workItem(id: $id) { + id + workItemType { + id + name + iconName + } + title + userPermissions { + deleteWorkItem + updateWorkItem + } + confidential + widgets { + type + ... on WorkItemWidgetHierarchy { + type + parent { + id + } + children { + nodes { + id + iid + confidential + workItemType { + id + name + iconName + } + title + state + createdAt + closedAt + widgets { + ... on WorkItemWidgetHierarchy { + type + hasChildren + } + } + } + } + } + } + } +} 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 025a9d3673b..9b802a8e8fc 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 @@ -38,6 +38,7 @@ fragment WorkItemWidgets on WorkItemWidget { } ... on WorkItemWidgetHierarchy { type + hasChildren parent { id iid @@ -56,11 +57,19 @@ fragment WorkItemWidgets on WorkItemWidget { confidential workItemType { id + name + iconName } title state createdAt closedAt + widgets { + ... on WorkItemWidgetHierarchy { + type + hasChildren + } + } } } } |