From 9635a60a492740414de7c4b7583c645cabfaa109 Mon Sep 17 00:00:00 2001
From: Nick Leonard
Date: Fri, 12 Dec 2025 16:14:04 -0600
Subject: [PATCH 1/2] Adds type scope for boards
Allows one or more types to be
set as a board scope, using
existing scope patterns.
Changelog: added
---
.../boards/components/board_form.vue | 7 +-
.../boards/board_issue_input_base_type.rb | 4 +
...212041921_add_work_item_types_to_boards.rb | 9 ++
db/structure.sql | 1 +
doc/api/graphql/reference/_index.md | 7 +
.../assets/javascripts/boards/boards_util.js | 13 ++
.../components/board_filtered_search.vue | 1 +
.../boards/components/board_form.vue | 1 +
.../boards/components/board_scope.vue | 8 +
.../components/work_item_type_select.vue | 153 ++++++++++++++++++
.../graphql/board_scope.fragment.graphql | 1 +
.../ee/resolvers/board_item_filterable.rb | 7 +
ee/app/graphql/ee/types/board_type.rb | 3 +
.../mutations/boards/scoped_board_mutation.rb | 4 +
ee/app/models/ee/board.rb | 3 +-
ee/app/services/ee/boards/update_service.rb | 3 +-
locale/gitlab.pot | 9 ++
17 files changed, 231 insertions(+), 3 deletions(-)
create mode 100644 db/migrate/20251212041921_add_work_item_types_to_boards.rb
create mode 100644 ee/app/assets/javascripts/boards/components/work_item_type_select.vue
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index c8abf8ee4aae44..d264cb3907d5e1 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -21,6 +21,7 @@ const boardDefaults = {
iteration: {},
assignee: {},
weight: null,
+ workItemTypes: [],
hideBacklogList: false,
hideClosedList: false,
};
@@ -273,12 +274,12 @@ export default {
} else {
this.$emit('addBoard', board);
}
+ this.isDisabled = true;
this.close();
} catch (error) {
setError({ error, message: this.$options.i18n.saveErrorMessage });
} finally {
this.isLoading = false;
- this.isDisabled = true;
}
}
},
@@ -325,6 +326,9 @@ export default {
weight,
};
},
+ setWorkItemTypes(types) {
+ this.board.workItemTypes = types.map((type) => type.id);
+ },
},
formType,
};
@@ -396,6 +400,7 @@ export default {
@set-assignee="setAssignee"
@set-milestone="setMilestone"
@set-weight="setWeight"
+ @set-work-item-types="setWorkItemTypes"
/>
diff --git a/app/graphql/types/boards/board_issue_input_base_type.rb b/app/graphql/types/boards/board_issue_input_base_type.rb
index 3f0e38ade41586..6c42d61f6398a9 100644
--- a/app/graphql/types/boards/board_issue_input_base_type.rb
+++ b/app/graphql/types/boards/board_issue_input_base_type.rb
@@ -25,6 +25,10 @@ class BoardIssueInputBaseType < BoardIssuableInputBaseType
description: 'Filter by the given issue types.',
required: false
+ argument :work_item_types, [GraphQL::Types::String],
+ required: false,
+ description: 'Filter by work item types.'
+
argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum,
required: false,
description: 'Filter by milestone ID wildcard.'
diff --git a/db/migrate/20251212041921_add_work_item_types_to_boards.rb b/db/migrate/20251212041921_add_work_item_types_to_boards.rb
new file mode 100644
index 00000000000000..cf183bd77a98c8
--- /dev/null
+++ b/db/migrate/20251212041921_add_work_item_types_to_boards.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddWorkItemTypesToBoards < Gitlab::Database::Migration[2.3]
+ milestone '18.8'
+
+ def change
+ add_column :boards, :work_item_types, :text, array: true, default: [], null: false
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 2188d6e075cd59..ec4c123e0437c0 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -13734,6 +13734,7 @@ CREATE TABLE boards (
hide_closed_list boolean DEFAULT false NOT NULL,
iteration_id bigint,
iteration_cadence_id bigint,
+ work_item_types text[] DEFAULT '{}'::text[] NOT NULL,
CONSTRAINT check_a60857cc50 CHECK ((num_nonnulls(group_id, project_id) = 1))
);
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 26ed284ec1e8a1..2d3e4a0e20f62e 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -5030,6 +5030,7 @@ Input type: `CreateBoardInput`
| `name` | [`String`](#string) | Board name. |
| `projectPath` | [`ID`](#id) | Full path of the project with which the resource is associated. |
| `weight` | [`Int`](#int) | Weight value to be assigned to the board. |
+| `workItemTypes` | [`[String!]`](#string) | Work item types to filter by on the board. |
#### Fields
@@ -6968,6 +6969,7 @@ Input type: `EpicBoardCreateInput`
| `labelIds` | [`[LabelID!]`](#labelid) | IDs of labels to be added to the board. |
| `labels` | [`[String!]`](#string) | Labels of the issue. |
| `name` | [`String`](#string) | Board name. |
+| `workItemTypes` | [`[String!]`](#string) | Work item types to filter by on the board. |
#### Fields
@@ -7050,6 +7052,7 @@ Input type: `EpicBoardUpdateInput`
| `labelIds` | [`[LabelID!]`](#labelid) | IDs of labels to be added to the board. |
| `labels` | [`[String!]`](#string) | Labels of the issue. |
| `name` | [`String`](#string) | Board name. |
+| `workItemTypes` | [`[String!]`](#string) | Work item types to filter by on the board. |
#### Fields
@@ -13321,6 +13324,7 @@ Input type: `UpdateBoardInput`
| `milestoneId` | [`MilestoneID`](#milestoneid) | ID of milestone to be assigned to the board. |
| `name` | [`String`](#string) | Board name. |
| `weight` | [`Int`](#int) | Weight value to be assigned to the board. |
+| `workItemTypes` | [`[String!]`](#string) | Work item types to filter by on the board. |
#### Fields
@@ -27024,6 +27028,7 @@ Represents a project or group issue board.
| `webPath` | [`String!`](#string) | Web path of the board. |
| `webUrl` | [`String!`](#string) | Web URL of the board. |
| `weight` | [`Int`](#int) | Weight of the board. |
+| `workItemTypes` | [`[String!]`](#string) | Work item types to filter by on the board. |
#### Fields with arguments
@@ -58523,6 +58528,7 @@ Input type for filtering projects by security attributes.
| `types` | [`[IssueType!]`](#issuetype) | Filter by the given issue types. |
| `weight` | [`String`](#string) | Filter by weight. |
| `weightWildcardId` | [`WeightWildcardId`](#weightwildcardid) | Filter by weight ID wildcard. Incompatible with weight. |
+| `workItemTypes` | [`[String!]`](#string) | Filter by work item types. |
### `BranchProtectionInput`
@@ -58975,6 +58981,7 @@ Defines which user roles, users, or groups can merge into a protected branch.
| `releaseTag` | [`String`](#string) | Filter by release tag. |
| `types` | [`[IssueType!]`](#issuetype) | Filter by the given issue types. |
| `weight` | [`String`](#string) | Filter by weight. |
+| `workItemTypes` | [`[String!]`](#string) | Filter by work item types. |
### `NegatedComplianceFrameworkFilters`
diff --git a/ee/app/assets/javascripts/boards/boards_util.js b/ee/app/assets/javascripts/boards/boards_util.js
index ea4de8f4038a90..d749c0e14a3a46 100644
--- a/ee/app/assets/javascripts/boards/boards_util.js
+++ b/ee/app/assets/javascripts/boards/boards_util.js
@@ -213,6 +213,15 @@ export function transformBoardConfig(boardConfig) {
let updatedFilterPath = objectToQuery(updatedBoardConfig);
const filterPath = updatedFilterPath ? updatedFilterPath.split('&') : [];
+ boardConfig.workItemTypes?.forEach((type) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ const param = `types=${type}`;
+
+ if (!passedFilterParams.types?.includes(type)) {
+ filterPath.push(param);
+ }
+ });
+
boardConfig.labels?.forEach((label) => {
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
@@ -276,6 +285,10 @@ export const FiltersInfo = {
transform: (v) =>
v === HealthStatusFilterType.any || v === HealthStatusFilterType.none ? v.toUpperCase() : v,
},
+ types: {
+ negatedSupport: false,
+ transform: (val) => val.toUpperCase(),
+ },
};
export function getBoardQuery(boardType, isEpicBoard) {
diff --git a/ee/app/assets/javascripts/boards/components/board_filtered_search.vue b/ee/app/assets/javascripts/boards/components/board_filtered_search.vue
index 1946e3535060ae..555d95cc3afc6c 100644
--- a/ee/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/ee/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -38,6 +38,7 @@ export default {
labels: board.labels || [],
labelIds: board.labels?.map((label) => label.id) || [],
weight: board.weight,
+ workItemTypes: board.workItemTypes || [],
};
},
},
diff --git a/ee/app/assets/javascripts/boards/components/board_form.vue b/ee/app/assets/javascripts/boards/components/board_form.vue
index 1a5fcbdd872d7f..e02aff0930a7c7 100644
--- a/ee/app/assets/javascripts/boards/components/board_form.vue
+++ b/ee/app/assets/javascripts/boards/components/board_form.vue
@@ -32,6 +32,7 @@ export default {
boardScopeMutationVariables() {
return {
labelIds: this.board.labels.map(fullLabelId),
+ workItemTypes: this.board.workItemTypes || [],
...(this.isIssueBoard && this.issueBoardScopeMutationVariables),
};
},
diff --git a/ee/app/assets/javascripts/boards/components/board_scope.vue b/ee/app/assets/javascripts/boards/components/board_scope.vue
index bc240e1bab4c8a..77d4f2c72a2db1 100644
--- a/ee/app/assets/javascripts/boards/components/board_scope.vue
+++ b/ee/app/assets/javascripts/boards/components/board_scope.vue
@@ -6,6 +6,7 @@ import BoardLabelsSelect from './labels_select.vue';
import BoardIterationSelect from './iteration_select.vue';
import BoardMilestoneSelect from './milestone_select.vue';
import BoardWeightSelect from './weight_select.vue';
+import WorkItemTypeSelect from './work_item_type_select.vue';
export default {
components: {
@@ -15,6 +16,7 @@ export default {
BoardIterationSelect,
BoardMilestoneSelect,
BoardWeightSelect,
+ WorkItemTypeSelect,
},
inject: ['isIssueBoard'],
props: {
@@ -114,6 +116,12 @@ export default {
:can-edit="canAdminBoard"
@set-weight="$emit('set-weight', $event)"
/>
+
+
diff --git a/ee/app/assets/javascripts/boards/components/work_item_type_select.vue b/ee/app/assets/javascripts/boards/components/work_item_type_select.vue
new file mode 100644
index 00000000000000..ab90d77335a9cb
--- /dev/null
+++ b/ee/app/assets/javascripts/boards/components/work_item_type_select.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+ {{ $options.i18n.label }}
+
+ {{ $options.i18n.edit }}
+
+
+
+
{{ $options.i18n.anyType }}
+
{{ selectedTypeNames }}
+
+
+
+
+ {{ item.name }}
+
+
+
+
diff --git a/ee/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql b/ee/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql
index 1bbe56426fb016..f55cbe170095ad 100644
--- a/ee/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql
+++ b/ee/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql
@@ -28,4 +28,5 @@ fragment BoardScopeFragment on Board {
durationInWeeks
}
weight
+ workItemTypes
}
diff --git a/ee/app/graphql/ee/resolvers/board_item_filterable.rb b/ee/app/graphql/ee/resolvers/board_item_filterable.rb
index af1701e5b2e431..f64d8c7fb8720a 100644
--- a/ee/app/graphql/ee/resolvers/board_item_filterable.rb
+++ b/ee/app/graphql/ee/resolvers/board_item_filterable.rb
@@ -12,6 +12,7 @@ def set_filter_values(filters)
filter_by_iteration(filters)
filter_by_iteration_cadence(filters)
filter_by_weight(filters)
+ filter_by_work_item_types(filters)
super
end
@@ -71,6 +72,12 @@ def filter_by_weight(filters)
filters[:weight] = weight_wildcard if weight_wildcard
end
+
+ def filter_by_work_item_types(filters)
+ return if filters[:work_item_types].blank?
+
+ filters[:work_item_types] = filters[:work_item_types].map(&:upcase)
+ end
end
end
end
diff --git a/ee/app/graphql/ee/types/board_type.rb b/ee/app/graphql/ee/types/board_type.rb
index 795865143b8dc5..dbd6130c7e43d8 100644
--- a/ee/app/graphql/ee/types/board_type.rb
+++ b/ee/app/graphql/ee/types/board_type.rb
@@ -27,6 +27,9 @@ module BoardType
description: 'Board iteration cadence.'
field :weight, type: GraphQL::Types::Int, null: true, description: 'Weight of the board.'
+
+ field :work_item_types, [GraphQL::Types::String], null: true,
+ description: 'Work item types to filter by on the board.'
end
end
end
diff --git a/ee/app/graphql/mutations/boards/scoped_board_mutation.rb b/ee/app/graphql/mutations/boards/scoped_board_mutation.rb
index 11c00ec87e59f9..3ac260014612c0 100644
--- a/ee/app/graphql/mutations/boards/scoped_board_mutation.rb
+++ b/ee/app/graphql/mutations/boards/scoped_board_mutation.rb
@@ -14,6 +14,10 @@ module ScopedBoardMutation
required: false,
description: 'IDs of labels to be added to the board.'
+ argument :work_item_types, [GraphQL::Types::String],
+ required: false,
+ description: 'Work item types to filter by on the board.'
+
validates mutually_exclusive: [:labels, :label_ids]
end
diff --git a/ee/app/models/ee/board.rb b/ee/app/models/ee/board.rb
index 38ff71ec5f1cad..3f6a5fa618a2c4 100644
--- a/ee/app/models/ee/board.rb
+++ b/ee/app/models/ee/board.rb
@@ -37,7 +37,8 @@ def scoped?
EMPTY_SCOPE_STATE.exclude?(milestone_id) ||
EMPTY_SCOPE_STATE.exclude?(weight) ||
labels.any? ||
- assignee.present?
+ assignee.present? ||
+ work_item_types.any?
end
def milestone
diff --git a/ee/app/services/ee/boards/update_service.rb b/ee/app/services/ee/boards/update_service.rb
index 8b5aaaac4b4058..c92a0d94c0111c 100644
--- a/ee/app/services/ee/boards/update_service.rb
+++ b/ee/app/services/ee/boards/update_service.rb
@@ -20,7 +20,8 @@ def permitted_params
permitted = super
if parent.feature_available?(:scoped_issue_board)
- permitted += %i[milestone_id iteration_id iteration_cadence_id assignee_id weight labels label_ids]
+ permitted += %i[milestone_id iteration_id iteration_cadence_id assignee_id weight labels label_ids
+ work_item_types]
end
permitted
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index de441c898f70f4..580a6bae6d8dc2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11701,12 +11701,18 @@ msgstr ""
msgid "BoardScope|Any label"
msgstr ""
+msgid "BoardScope|Any type"
+msgstr ""
+
msgid "BoardScope|Assignee"
msgstr ""
msgid "BoardScope|Choose labels"
msgstr ""
+msgid "BoardScope|Choose types"
+msgstr ""
+
msgid "BoardScope|Current iteration"
msgstr ""
@@ -11752,6 +11758,9 @@ msgstr ""
msgid "BoardScope|Started"
msgstr ""
+msgid "BoardScope|Type"
+msgstr ""
+
msgid "BoardScope|Upcoming"
msgstr ""
--
GitLab
From 194149dce283c70d601836a420d368d507220db7 Mon Sep 17 00:00:00 2001
From: Nick Leonard
Date: Fri, 12 Dec 2025 16:41:05 -0600
Subject: [PATCH 2/2] Remove from epic boards
---
.../javascripts/boards/components/board_scope.vue | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/ee/app/assets/javascripts/boards/components/board_scope.vue b/ee/app/assets/javascripts/boards/components/board_scope.vue
index 77d4f2c72a2db1..d8966c12a732e4 100644
--- a/ee/app/assets/javascripts/boards/components/board_scope.vue
+++ b/ee/app/assets/javascripts/boards/components/board_scope.vue
@@ -81,6 +81,13 @@ export default {
{{ scopeText }}
+
+
-
-
--
GitLab