diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/work_item_parent_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/work_item_parent_token.vue index b169d1306944579c1e17cec96e79c225a0ac7c16..71f43392858e5b820964e8df58d1099f5b31cb7a 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/work_item_parent_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/work_item_parent_token.vue @@ -62,6 +62,9 @@ export default { this.loading = true; try { + // The logic to fetch the Parent seems to be different than other pages + // Below issue targets to have a common logic across work items app + // https://gitlab.com/gitlab-org/gitlab/-/issues/571302 const { data } = await this.$apollo.query({ query: searchWorkItemParentQuery, variables: { diff --git a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_parent.vue b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_parent.vue index e4ef4b11a713a806b337ec5e45b8d90721d5c963..74591823a45f398c5cf1a63580762e10af1adcff 100644 --- a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_parent.vue +++ b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_parent.vue @@ -5,11 +5,17 @@ import { createAlert } from '~/alert'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { __, s__ } from '~/locale'; import { isValidURL } from '~/lib/utils/url_utility'; -import { BULK_EDIT_NO_VALUE } from '../../constants'; +import { + BULK_EDIT_NO_VALUE, + NAME_TO_ENUM_MAP, + WORK_ITEM_TYPE_ENUM_EPIC, + WORK_ITEMS_NO_PARENT_LIST, +} from '../../constants'; import groupWorkItemsQuery from '../../graphql/group_work_items.query.graphql'; import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql'; import workItemsByReferencesQuery from '../../graphql/work_items_by_references.query.graphql'; -import { isReference } from '../../utils'; +import namespaceWorkItemTypesQuery from '../../graphql/namespace_work_item_types.query.graphql'; +import { isReference, findHierarchyWidgetDefinition } from '../../utils'; export default { components: { @@ -36,6 +42,11 @@ export default { required: false, default: false, }, + selectedWorkItemTypesIds: { + type: Array, + required: false, + default: () => [], + }, }, data() { return { @@ -45,23 +56,31 @@ export default { workspaceWorkItems: [], workItemsCache: [], workItemsByReference: [], + allowedParentTypesMap: {}, }; }, apollo: { workspaceWorkItems: { query() { - return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery; + // The logic to fetch the Parent seems to be different than other pages + // Below issue targets to have a common logic across work items app + // https://gitlab.com/gitlab-org/gitlab/-/issues/571302 + return this.shouldSearchAcrossGroups ? groupWorkItemsQuery : projectWorkItemsQuery; }, variables() { return { - fullPath: this.fullPath, + fullPath: !this.isGroup && this.shouldSearchAcrossGroups ? this.groupPath : this.fullPath, searchTerm: this.searchTerm, in: this.searchTerm ? 'TITLE' : undefined, includeAncestors: true, + includeDescendants: this.shouldSearchAcrossGroups, + types: this.selectedItemParentTypes.filter( + (type) => !WORK_ITEMS_NO_PARENT_LIST.includes(type), + ), }; }, skip() { - return !this.searchStarted; + return !this.searchStarted || !this.shouldLoadParents; }, update(data) { return data.workspace?.workItems?.nodes || []; @@ -96,6 +115,38 @@ export default { }); }, }, + allowedParentTypesMap: { + query: namespaceWorkItemTypesQuery, + variables() { + return { + fullPath: this.fullPath, + }; + }, + update(data) { + const typesParentsMap = {}; + const types = data.workspace.workItemTypes.nodes || []; + + // Used `for` loop for better readability and performance + for (const type of types) { + // Get the hierarchy widgets + const hierarchyWidget = findHierarchyWidgetDefinition({ workItemType: type }); + + // If there are allowed parent types map the ids and names + if (hierarchyWidget?.allowedParentTypes?.nodes?.length > 0) { + const parentNames = hierarchyWidget.allowedParentTypes?.nodes.map((parent) => { + // Used enums because the workspaceWorkItems does not support gids in the types fields + return { id: parent.id, name: NAME_TO_ENUM_MAP[parent.name] }; + }); + typesParentsMap[type.id] = parentNames; + } + } + + return typesParentsMap; + }, + skip() { + return !this.fullPath; + }, + }, }, computed: { isSearchingByReference() { @@ -111,6 +162,10 @@ export default { return this.isSearchingByReference ? this.workItemsByReference : this.workspaceWorkItems; }, listboxItems() { + if (!this.shouldLoadParents) { + return []; + } + if (!this.searchTerm.trim().length) { return [ { @@ -141,6 +196,54 @@ export default { } return s__('WorkItem|Select parent'); }, + selectedItemsCanHaveParents() { + return this.selectedWorkItemTypesIds.some((id) => + Object.keys(this.allowedParentTypesMap).includes(id), + ); + }, + areTypesCompatible() { + return ( + this.selectedWorkItemTypesIds + .map((id) => new Set((this.allowedParentTypesMap[id] || []).map((type) => type.id))) + .reduce((intersection, parentIds) => { + // If there are no parents + if (parentIds.size === 0) return new Set(); + // If parents are unique + if (!intersection) return parentIds; + // Verify if the parents are incompatible + return new Set([...parentIds].filter((id) => intersection.has(id))); + }, null)?.size > 0 ?? false + ); + }, + shouldLoadParents() { + return this.selectedItemsCanHaveParents && this.areTypesCompatible; + }, + selectedItemParentTypes() { + return [ + ...new Set( + this.selectedWorkItemTypesIds?.flatMap( + (id) => this.allowedParentTypesMap?.[id]?.map((type) => type.name) || [], + ), + ), + ]; + }, + canHaveEpicParent() { + return this.selectedItemParentTypes?.includes(WORK_ITEM_TYPE_ENUM_EPIC); + }, + shouldSearchAcrossGroups() { + // Determines if we need to search across groups. + // Cross-group search applies only when the parent is + // a group-level work item, an epic. + return this.isGroup || this.canHaveEpicParent; + }, + groupPath() { + return this.fullPath.substring(0, this.fullPath.lastIndexOf('/')); + }, + noResultText() { + return !this.shouldLoadParents + ? s__('WorkItem|No available parent for all selected items.') + : s__('WorkItem|No matching results'); + }, }, watch: { workspaceWorkItems(workspaceWorkItems) { @@ -191,7 +294,7 @@ export default { :header-text="s__('WorkItem|Select parent')" is-check-centered :items="listboxItems" - :no-results-text="s__('WorkItem|No matching results')" + :no-results-text="noResultText" :reset-button-label="__('Reset')" searchable :searching="isLoading" diff --git a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue index 4bfd2691ab347aff9f838513fcf51bbf278963a9..1c6d2c64491535281d4f4a2a4749606827f5b095 100644 --- a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue +++ b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue @@ -322,11 +322,12 @@ export default { :disabled="!hasItemsSelected || !canEditMilestone" />