From edfa1c6e3e93302eadd1784d2e02bb6b7efe8f5f Mon Sep 17 00:00:00 2001 From: Rajan Mistry Date: Thu, 7 Aug 2025 22:53:24 +0530 Subject: [PATCH 1/2] Add parent filter to the work items list --- .../javascripts/issues/list/constants.js | 16 +++ app/assets/javascripts/issues/list/utils.js | 8 ++ .../filtered_search_bar/constants.js | 2 + .../search_work_item_parent.query.graphql | 27 +++++ .../tokens/work_item_parent_token.vue | 108 ++++++++++++++++++ .../get_work_item_state_counts.query.graphql | 3 + .../list/get_work_items_full.query.graphql | 2 + .../list/get_work_items_slim.query.graphql | 2 + .../work_items/pages/work_items_list_app.vue | 22 ++++ .../groups/work_items_controller.rb | 2 + app/controllers/projects/issues_controller.rb | 2 + .../projects/work_items_controller.rb | 3 + app/models/group.rb | 4 + app/models/project.rb | 4 + .../beta/work_items_list_parent_filter.yml | 10 ++ .../get_work_item_state_counts.query.graphql | 3 + .../list/get_work_items_full.query.graphql | 2 + .../list/get_work_items_slim.query.graphql | 2 + ee/app/controllers/groups/epics_controller.rb | 2 + locale/gitlab.pot | 6 + 20 files changed, 230 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_work_item_parent.query.graphql create mode 100644 app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/work_item_parent_token.vue create mode 100644 config/feature_flags/beta/work_items_list_parent_filter.yml diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index 8e41d2ec1998b4..c3325347cdb4ea 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -44,6 +44,7 @@ import { TOKEN_TYPE_STATE, TOKEN_TYPE_SUBSCRIBED, TOKEN_TYPE_STATUS, + TOKEN_TYPE_PARENT, } from '~/vue_shared/components/filtered_search_bar/constants'; export const ISSUE_REFERENCE = /^#\d+$/; @@ -443,6 +444,21 @@ export const filtersMap = { }, }, }, + [TOKEN_TYPE_PARENT]: { + [API_PARAM]: { + [NORMAL_FILTER]: 'parentIds', + [WILDCARD_FILTER]: 'parentWildcardId', + }, + [URL_PARAM]: { + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'parent_id', + [WILDCARD_FILTER]: 'parent_id', + }, + [OPERATOR_NOT]: { + [NORMAL_FILTER]: 'not[parent_id]', + }, + }, + }, [TOKEN_TYPE_WEIGHT]: { [API_PARAM]: { [NORMAL_FILTER]: 'weight', diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js index fc0431e87e2a06..eb2a8c280fde34 100644 --- a/app/assets/javascripts/issues/list/utils.js +++ b/app/assets/javascripts/issues/list/utils.js @@ -24,6 +24,7 @@ import { TOKEN_TYPE_EPIC, TOKEN_TYPE_WEIGHT, TOKEN_TYPE_STATE, + TOKEN_TYPE_PARENT, } from '~/vue_shared/components/filtered_search_bar/constants'; import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants'; import { @@ -356,6 +357,10 @@ export const isIterationCadenceIdParam = (type, data) => { return type === TOKEN_TYPE_ITERATION && data?.includes('&'); }; +export const isParentIdParam = (type) => { + return type === TOKEN_TYPE_PARENT; +}; + const getFilterType = ({ type, value: { data, operator } }) => { const isUnionedAuthor = type === TOKEN_TYPE_AUTHOR && operator === OPERATOR_OR; const isUnionedLabel = type === TOKEN_TYPE_LABEL && operator === OPERATOR_OR; @@ -386,6 +391,7 @@ const wildcardTokens = [ TOKEN_TYPE_RELEASE, TOKEN_TYPE_REVIEWER, TOKEN_TYPE_WEIGHT, + TOKEN_TYPE_PARENT, ]; const isWildcardValue = (tokenType, value) => @@ -442,6 +448,8 @@ export const convertToApiParams = (filterTokens) => { ? [obj.get(secondApiField), iterationWildCardId].flat() : iterationWildCardId, ); + } else if (isParentIdParam(token.type)) { + obj.set('hierarchyFilters', { parentIds: data }); } else { obj.set(apiField, obj.has(apiField) ? [obj.get(apiField), data].flat() : data); } diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 1320889769b0c6..88f6de6f9d5380 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -97,6 +97,7 @@ export const TOKEN_TITLE_ENVIRONMENT = __('Environment'); export const TOKEN_TITLE_STATE = __('State'); export const TOKEN_TITLE_SUBSCRIBED = __('Subscribed'); export const TOKEN_TITLE_JOB_KIND = s__('Job|Kind'); +export const TOKEN_TITLE_PARENT = s__('WorkItem|Parent'); export const TOKEN_TYPE_APPROVER = 'approver'; export const TOKEN_TYPE_APPROVED_BY = 'approved-by'; @@ -143,6 +144,7 @@ export const TOKEN_TYPE_ENVIRONMENT = 'environment'; export const TOKEN_TYPE_STATE = 'state'; export const TOKEN_TYPE_SUBSCRIBED = 'subscribed'; export const TOKEN_TYPE_JOB_KIND = 'kind'; +export const TOKEN_TYPE_PARENT = 'parent'; // Due to the i18n eslint rule we can't have a capitalized string even if it is a case-aware URL param /* eslint-disable @gitlab/require-i18n-strings */ diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_work_item_parent.query.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_work_item_parent.query.graphql new file mode 100644 index 00000000000000..7e0ebfb5bc4aa7 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_work_item_parent.query.graphql @@ -0,0 +1,27 @@ +query parentWorkItems( + $search: String + $fullPath: ID! + $types: [IssueType!] + $in: [IssuableSearchableField!] +) { + workspace: namespace(fullPath: $fullPath) { + id + workItems(search: $search, types: $types, state: opened, in: $in) { + nodes { + id + iid + title + confidential + namespace { + id + fullPath + } + workItemType { + id + name + iconName + } + } + } + } +} 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 new file mode 100644 index 00000000000000..700a9d648c8d45 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/work_item_parent_token.vue @@ -0,0 +1,108 @@ + + + diff --git a/app/assets/javascripts/work_items/graphql/list/get_work_item_state_counts.query.graphql b/app/assets/javascripts/work_items/graphql/list/get_work_item_state_counts.query.graphql index d50b842fe345b0..b26243950f7a31 100644 --- a/app/assets/javascripts/work_items/graphql/list/get_work_item_state_counts.query.graphql +++ b/app/assets/javascripts/work_items/graphql/list/get_work_item_state_counts.query.graphql @@ -31,6 +31,7 @@ query getWorkItemStateCounts( $or: UnionedWorkItemFilterInput $releaseTag: [String!] $releaseTagWildcardId: ReleaseTagWildcardId + $hierarchyFilters: HierarchyFilterInput ) { group(fullPath: $fullPath) @include(if: $isGroup) { id @@ -60,6 +61,7 @@ query getWorkItemStateCounts( updatedBefore: $updatedBefore crmOrganizationId: $crmOrganizationId crmContactId: $crmContactId + hierarchyFilters: $hierarchyFilters in: $in not: $not or: $or @@ -97,6 +99,7 @@ query getWorkItemStateCounts( crmContactId: $crmContactId releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId + hierarchyFilters: $hierarchyFilters in: $in not: $not or: $or diff --git a/app/assets/javascripts/work_items/graphql/list/get_work_items_full.query.graphql b/app/assets/javascripts/work_items/graphql/list/get_work_items_full.query.graphql index c3d39e5470cd03..89773cec58a08c 100644 --- a/app/assets/javascripts/work_items/graphql/list/get_work_items_full.query.graphql +++ b/app/assets/javascripts/work_items/graphql/list/get_work_items_full.query.graphql @@ -37,6 +37,7 @@ query getWorkItemsFull( $lastPageSize: Int $releaseTag: [String!] $releaseTagWildcardId: ReleaseTagWildcardId + $hierarchyFilters: HierarchyFilterInput ) { namespace(fullPath: $fullPath) { id @@ -69,6 +70,7 @@ query getWorkItemsFull( crmContactId: $crmContactId releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId + hierarchyFilters: $hierarchyFilters in: $in not: $not or: $or diff --git a/app/assets/javascripts/work_items/graphql/list/get_work_items_slim.query.graphql b/app/assets/javascripts/work_items/graphql/list/get_work_items_slim.query.graphql index b7b2097e9b2e1e..976b34aea1e7b3 100644 --- a/app/assets/javascripts/work_items/graphql/list/get_work_items_slim.query.graphql +++ b/app/assets/javascripts/work_items/graphql/list/get_work_items_slim.query.graphql @@ -38,6 +38,7 @@ query getWorkItemsSlim( $lastPageSize: Int $releaseTag: [String!] $releaseTagWildcardId: ReleaseTagWildcardId + $hierarchyFilters: HierarchyFilterInput ) { namespace(fullPath: $fullPath) { id @@ -70,6 +71,7 @@ query getWorkItemsSlim( crmOrganizationId: $crmOrganizationId releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId + hierarchyFilters: $hierarchyFilters in: $in not: $not or: $or diff --git a/app/assets/javascripts/work_items/pages/work_items_list_app.vue b/app/assets/javascripts/work_items/pages/work_items_list_app.vue index d91e6919b0d8cd..6bd7424a83e295 100644 --- a/app/assets/javascripts/work_items/pages/work_items_list_app.vue +++ b/app/assets/javascripts/work_items/pages/work_items_list_app.vue @@ -87,6 +87,8 @@ import { TOKEN_TYPE_CONTACT, TOKEN_TYPE_RELEASE, TOKEN_TITLE_RELEASE, + TOKEN_TYPE_PARENT, + TOKEN_TITLE_PARENT, } from '~/vue_shared/components/filtered_search_bar/constants'; import DateToken from '~/vue_shared/components/filtered_search_bar/tokens/date_token.vue'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; @@ -131,6 +133,8 @@ const CrmOrganizationToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue'); const CrmContactToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue'); +const WorkItemParentToken = () => + import('~/vue_shared/components/filtered_search_bar/tokens/work_item_parent_token.vue'); const statusMap = { [STATUS_OPEN]: STATE_OPEN, @@ -667,6 +671,21 @@ export default { }); } + if (this.showParentFilter) { + tokens.push({ + type: TOKEN_TYPE_PARENT, + title: TOKEN_TITLE_PARENT, + icon: 'work-item-parent', + token: WorkItemParentToken, + fullPath: this.rootPageFullPath, + isProject: !this.isGroup, + recentSuggestionsStorageKey: `${this.rootPageFullPath}-issues-recent-tokens-parent`, + operators: OPERATORS_IS_NOT, + unique: true, + idProperty: 'id', + }); + } + if (this.eeSearchTokens.length) { tokens.push(...this.eeSearchTokens); } @@ -753,6 +772,9 @@ export default { hiddenMetadataKeys() { return this.displaySettings?.namespacePreferences?.hiddenMetadataKeys || []; }, + showParentFilter() { + return this.glFeatures.workItemsListParentFilter; + }, }, watch: { eeWorkItemUpdateCount() { diff --git a/app/controllers/groups/work_items_controller.rb b/app/controllers/groups/work_items_controller.rb index 366dfd4ad5e422..7b9355a367d2aa 100644 --- a/app/controllers/groups/work_items_controller.rb +++ b/app/controllers/groups/work_items_controller.rb @@ -15,6 +15,8 @@ class WorkItemsController < Groups::ApplicationController push_frontend_feature_flag(:issues_list_drawer, group) push_frontend_feature_flag(:work_item_status_feature_flag, group&.root_ancestor) push_frontend_feature_flag(:work_item_planning_view, group) + push_force_frontend_feature_flag(:work_items_list_parent_filter, + group&.work_items_list_parent_filter_feature_flag_enabled?) end before_action :handle_new_work_item_path, only: [:show] diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 273db979be6901..46b4fe1bc61f35 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -60,6 +60,8 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:work_item_view_for_issues, project&.group) push_frontend_feature_flag(:work_item_status_feature_flag, project&.root_ancestor) push_frontend_feature_flag(:hide_incident_management_features, project) + push_force_frontend_feature_flag(:work_items_list_parent_filter, + project&.work_items_list_parent_filter_feature_flag_enabled?) end before_action only: [:index, :show] do diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb index b08d2c35d26703..97cea4f6d78c2e 100644 --- a/app/controllers/projects/work_items_controller.rb +++ b/app/controllers/projects/work_items_controller.rb @@ -15,6 +15,9 @@ class Projects::WorkItemsController < Projects::ApplicationController push_force_frontend_feature_flag(:glql_load_on_click, !!project&.glql_load_on_click_feature_flag_enabled?) push_frontend_feature_flag(:work_item_status_feature_flag, project&.root_ancestor) push_frontend_feature_flag(:work_item_planning_view, project&.group) + push_frontend_feature_flag(:work_item_planning_view, project&.group) + push_force_frontend_feature_flag(:work_items_list_parent_filter, + project&.work_items_list_parent_filter_feature_flag_enabled?) end feature_category :team_planning diff --git a/app/models/group.rb b/app/models/group.rb index cb428237ee7f38..ff8daefccb1a75 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1115,6 +1115,10 @@ def work_item_status_feature_available? licensed_feature_available?(:work_item_status) end + def work_items_list_parent_filter_feature_flag_enabled? + feature_flag_enabled_for_self_or_ancestor?(:work_items_list_parent_filter, type: :beta) + end + def markdown_placeholders_feature_flag_enabled? feature_flag_enabled_for_self_or_ancestor?(:markdown_placeholders, type: :gitlab_com_derisk) end diff --git a/app/models/project.rb b/app/models/project.rb index 27afb4e9b0e90f..67abca39481ab7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3477,6 +3477,10 @@ def work_item_status_feature_available? licensed_feature_available?(:work_item_status) end + def work_items_list_parent_filter_feature_flag_enabled? + group&.work_items_list_parent_filter_feature_flag_enabled? || Feature.enabled?(:work_items_list_parent_filter, type: :beta) + end + def glql_load_on_click_feature_flag_enabled? group&.glql_load_on_click_feature_flag_enabled? || Feature.enabled?(:glql_load_on_click, self, type: :ops) end diff --git a/config/feature_flags/beta/work_items_list_parent_filter.yml b/config/feature_flags/beta/work_items_list_parent_filter.yml new file mode 100644 index 00000000000000..fa02c248b78610 --- /dev/null +++ b/config/feature_flags/beta/work_items_list_parent_filter.yml @@ -0,0 +1,10 @@ +--- +name: work_items_list_parent_filter +description: +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/536876 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/200693 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/560366 +milestone: '18.3' +group: group::product planning +type: beta +default_enabled: false diff --git a/ee/app/assets/javascripts/work_items/graphql/list/get_work_item_state_counts.query.graphql b/ee/app/assets/javascripts/work_items/graphql/list/get_work_item_state_counts.query.graphql index ec5475ecce0ba8..2e9395d7e95e7d 100644 --- a/ee/app/assets/javascripts/work_items/graphql/list/get_work_item_state_counts.query.graphql +++ b/ee/app/assets/javascripts/work_items/graphql/list/get_work_item_state_counts.query.graphql @@ -36,6 +36,7 @@ query EEgetWorkItemStateCounts( $status: WorkItemWidgetStatusFilterInput $releaseTag: [String!] $releaseTagWildcardId: ReleaseTagWildcardId + $hierarchyFilters: HierarchyFilterInput ) { group(fullPath: $fullPath) @include(if: $isGroup) { id @@ -66,6 +67,7 @@ query EEgetWorkItemStateCounts( crmOrganizationId: $crmOrganizationId crmContactId: $crmContactId customField: $customField + hierarchyFilters: $hierarchyFilters in: $in not: $not or: $or @@ -103,6 +105,7 @@ query EEgetWorkItemStateCounts( updatedBefore: $updatedBefore crmOrganizationId: $crmOrganizationId crmContactId: $crmContactId + hierarchyFilters: $hierarchyFilters in: $in not: $not or: $or diff --git a/ee/app/assets/javascripts/work_items/graphql/list/get_work_items_full.query.graphql b/ee/app/assets/javascripts/work_items/graphql/list/get_work_items_full.query.graphql index f31ec3ea4c1f88..fcaacaf8350e9a 100644 --- a/ee/app/assets/javascripts/work_items/graphql/list/get_work_items_full.query.graphql +++ b/ee/app/assets/javascripts/work_items/graphql/list/get_work_items_full.query.graphql @@ -42,6 +42,7 @@ query getWorkItemsFullEE( $status: WorkItemWidgetStatusFilterInput $releaseTag: [String!] $releaseTagWildcardId: ReleaseTagWildcardId + $hierarchyFilters: HierarchyFilterInput ) { namespace(fullPath: $fullPath) { id @@ -86,6 +87,7 @@ query getWorkItemsFullEE( status: $status releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId + hierarchyFilters: $hierarchyFilters ) { pageInfo { ...PageInfo diff --git a/ee/app/assets/javascripts/work_items/graphql/list/get_work_items_slim.query.graphql b/ee/app/assets/javascripts/work_items/graphql/list/get_work_items_slim.query.graphql index 7488bd2f949865..6857928d9adf35 100644 --- a/ee/app/assets/javascripts/work_items/graphql/list/get_work_items_slim.query.graphql +++ b/ee/app/assets/javascripts/work_items/graphql/list/get_work_items_slim.query.graphql @@ -43,6 +43,7 @@ query getWorkItemsSlimEE( $status: WorkItemWidgetStatusFilterInput $releaseTag: [String!] $releaseTagWildcardId: ReleaseTagWildcardId + $hierarchyFilters: HierarchyFilterInput ) { namespace(fullPath: $fullPath) { id @@ -87,6 +88,7 @@ query getWorkItemsSlimEE( status: $status releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId + hierarchyFilters: $hierarchyFilters ) { pageInfo { ...PageInfo diff --git a/ee/app/controllers/groups/epics_controller.rb b/ee/app/controllers/groups/epics_controller.rb index 16679b6c62beec..9fb3bb5a2b48ad 100644 --- a/ee/app/controllers/groups/epics_controller.rb +++ b/ee/app/controllers/groups/epics_controller.rb @@ -25,6 +25,8 @@ class Groups::EpicsController < Groups::ApplicationController push_force_frontend_feature_flag(:work_items_alpha, !!group.work_items_alpha_feature_flag_enabled?) push_frontend_feature_flag(:epics_list_drawer, @group) push_frontend_feature_flag(:work_item_status_feature_flag, @group&.root_ancestor) + push_force_frontend_feature_flag(:work_items_list_parent_filter, + @group&.work_items_list_parent_filter_feature_flag_enabled?) end before_action only: :show do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d4771e755b9965..bdbbbee809adb3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -64496,6 +64496,9 @@ msgstr "" msgid "There was a problem fetching the CI/CD job token permissions." msgstr "" +msgid "There was a problem fetching the items." +msgstr "" + msgid "There was a problem fetching the job token scope value" msgstr "" @@ -72434,6 +72437,9 @@ msgstr "" msgid "WorkItem|Options:" msgstr "" +msgid "WorkItem|Parent" +msgstr "" + msgid "WorkItem|Parent item type %{parentWorkItemType} is not supported on %{workItemType}. Remove the parent item to change type." msgstr "" -- GitLab From fe0901679e04144819415f64971d9a0da573f5fa Mon Sep 17 00:00:00 2001 From: Matt D'Angelo Date: Mon, 11 Aug 2025 21:12:45 +0930 Subject: [PATCH 2/2] Rewrite epic param in controller --- app/assets/javascripts/issues/list/utils.js | 4 +-- .../tokens/work_item_parent_token.vue | 4 +-- app/controllers/projects/issues_controller.rb | 28 +++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js index eb2a8c280fde34..efaabde4fddc32 100644 --- a/app/assets/javascripts/issues/list/utils.js +++ b/app/assets/javascripts/issues/list/utils.js @@ -1,5 +1,5 @@ import produce from 'immer'; -import { TYPENAME_ITERATIONS_CADENCE } from '~/graphql_shared/constants'; +import { TYPENAME_ITERATIONS_CADENCE, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import { isPositiveInteger } from '~/lib/utils/number_utils'; import { getParameterByName } from '~/lib/utils/url_utility'; @@ -449,7 +449,7 @@ export const convertToApiParams = (filterTokens) => { : iterationWildCardId, ); } else if (isParentIdParam(token.type)) { - obj.set('hierarchyFilters', { parentIds: data }); + obj.set('hierarchyFilters', { parentIds: convertToGraphQLId(TYPENAME_WORK_ITEM, data) }); } else { obj.set(apiField, obj.has(apiField) ? [obj.get(apiField), data].flat() : data); } 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 700a9d648c8d45..8a18fd46f4011b 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 @@ -1,7 +1,7 @@