diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 08b69b1d7df4f063e1992b3c27f7764df785fe86..907e305b11b107621cdd157f81030ce6f467539a 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -828,6 +828,7 @@ four standard [pagination arguments](#pagination-arguments): | <a id="queryissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. | | <a id="queryissuescrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. | | <a id="queryissuescrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. | +| <a id="queryissuescustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. | | <a id="queryissuesdueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. | | <a id="queryissuesduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. | | <a id="queryissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | @@ -27195,6 +27196,7 @@ four standard [pagination arguments](#pagination-arguments): | <a id="groupissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. | | <a id="groupissuescrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. | | <a id="groupissuescrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. | +| <a id="groupissuescustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. | | <a id="groupissuesdueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. | | <a id="groupissuesduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. | | <a id="groupissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | @@ -34485,6 +34487,7 @@ Returns [`Issue`](#issue). | <a id="projectissuecreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. | | <a id="projectissuecrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. | | <a id="projectissuecrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. | +| <a id="projectissuecustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. | | <a id="projectissuedueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. | | <a id="projectissueduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. | | <a id="projectissueepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | @@ -34539,6 +34542,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). | <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. | | <a id="projectissuestatuscountscrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. | | <a id="projectissuestatuscountscrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. | +| <a id="projectissuestatuscountscustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. | | <a id="projectissuestatuscountsdueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. | | <a id="projectissuestatuscountsduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. | | <a id="projectissuestatuscountsepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | @@ -34594,6 +34598,7 @@ four standard [pagination arguments](#pagination-arguments): | <a id="projectissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before the date. | | <a id="projectissuescrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. | | <a id="projectissuescrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. | +| <a id="projectissuescustomfield"></a>`customField` {{< icon name="warning-solid" >}} | [`[WorkItemWidgetCustomFieldFilterInputType!]`](#workitemwidgetcustomfieldfilterinputtype) | **Introduced** in GitLab 17.10. **Status**: Experiment. Filter by custom fields. | | <a id="projectissuesdueafter"></a>`dueAfter` | [`Time`](#time) | Return issues due on or after the given time. | | <a id="projectissuesduebefore"></a>`dueBefore` | [`Time`](#time) | Return issues due on or before the given time. | | <a id="projectissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | @@ -47741,6 +47746,15 @@ Attributes for value stream stage. | <a id="workitemwidgetcurrentusertodosinputaction"></a>`action` | [`WorkItemTodoUpdateAction!`](#workitemtodoupdateaction) | Action for the update. | | <a id="workitemwidgetcurrentusertodosinputtodoid"></a>`todoId` | [`TodoID`](#todoid) | Global ID of the to-do. If not present, all to-dos of the work item will be updated. | +### `WorkItemWidgetCustomFieldFilterInputType` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="workitemwidgetcustomfieldfilterinputtypecustomfieldid"></a>`customFieldId` | [`IssuablesCustomFieldID!`](#issuablescustomfieldid) | Global ID of the custom field. | +| <a id="workitemwidgetcustomfieldfilterinputtypeselectedoptionids"></a>`selectedOptionIds` | [`[IssuablesCustomFieldSelectOptionID!]`](#issuablescustomfieldselectoptionid) | Global IDs of the selected options for custom fields with select type. | + ### `WorkItemWidgetCustomFieldValueInputType` #### Arguments diff --git a/ee/app/graphql/ee/resolvers/issues/base_resolver.rb b/ee/app/graphql/ee/resolvers/issues/base_resolver.rb index 4bd5e8b4de041987032f343d40168bfbda5e8c57..95a2b5e8d4f9ca5b9601c50157118d709904d243 100644 --- a/ee/app/graphql/ee/resolvers/issues/base_resolver.rb +++ b/ee/app/graphql/ee/resolvers/issues/base_resolver.rb @@ -8,6 +8,11 @@ module BaseResolver extend ::Gitlab::Utils::Override prepended do + argument :custom_field, [::Types::WorkItems::Widgets::CustomFieldFilterInputType], + required: false, + experiment: { milestone: '17.10' }, + description: 'Filter by custom fields.', + prepare: ->(custom_fields, _ctx) { Array(custom_fields).inject({}, :merge) } argument :epic_id, GraphQL::Types::String, required: false, description: 'ID of an epic associated with the issues, "none" and "any" values are supported.' diff --git a/ee/app/graphql/types/work_items/widgets/custom_field_filter_input_type.rb b/ee/app/graphql/types/work_items/widgets/custom_field_filter_input_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..705bbf965d8d691ed4c93eb34b78b4ca4ca8c846 --- /dev/null +++ b/ee/app/graphql/types/work_items/widgets/custom_field_filter_input_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + class CustomFieldFilterInputType < BaseInputObject + graphql_name 'WorkItemWidgetCustomFieldFilterInputType' + + argument :custom_field_id, ::Types::GlobalIDType[::Issuables::CustomField], + required: true, + description: copy_field_description(Types::Issuables::CustomFieldType, :id), + prepare: ->(id, _ctx) { id&.model_id } + + argument :selected_option_ids, [::Types::GlobalIDType[::Issuables::CustomFieldSelectOption]], + required: false, + description: 'Global IDs of the selected options for custom fields with select type.', + prepare: ->(ids, _ctx) { ids.map(&:model_id) } + + def prepare + { custom_field_id => selected_option_ids } + end + end + end + end +end diff --git a/ee/spec/requests/api/graphql/project/issues_spec.rb b/ee/spec/requests/api/graphql/project/issues_spec.rb index 452fa5bd724b99886fcaf048fb12ddaf9a7e6d68..807209d0969ff422ec257db9399897e0804f5199 100644 --- a/ee/spec/requests/api/graphql/project/issues_spec.rb +++ b/ee/spec/requests/api/graphql/project/issues_spec.rb @@ -74,5 +74,48 @@ def query(params = issue_filter_params) expect(issues.first["id"]).to eq(issue_needs_attention.to_global_id.to_s) end end + + context "for custom field" do + include_context 'with group configured with custom fields' + + before_all do + create(:work_item_select_field_value, work_item_id: issue_a.id, custom_field: select_field, + custom_field_select_option: select_option_1) + create(:work_item_select_field_value, work_item_id: issue_b.id, custom_field: select_field, + custom_field_select_option: select_option_2) + create(:work_item_select_field_value, work_item_id: issue_c.id, custom_field: select_field, + custom_field_select_option: select_option_2) + end + + before do + stub_licensed_features(custom_fields: true) + end + + let(:query) do + graphql_query_for(:project, { full_path: project.full_path }, + query_nodes(:issues, :id, args: params) + ) + end + + context "when filtering on a select field" do + let(:params) do + { + custom_field: [{ + custom_field_id: select_field.to_global_id.to_s, + selected_option_ids: [select_option_2.to_global_id.to_s] + }] + } + end + + it 'returns issues that match the custom field filter' do + post_graphql(query, current_user: current_user) + + issues = graphql_data.dig('project', 'issues', 'nodes') + + expect(issues.size).to eq(2) + expect(issues.pluck("id")).to contain_exactly(issue_b.to_global_id.to_s, issue_c.to_global_id.to_s) + end + end + end end end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index deeed58fe0b5821645a3e86d0c3faaca66f520ab..f506c01e1fbfa8b0cd5eb7532781941c3ac645ee 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -245,6 +245,7 @@ iterationWildcardId weight weightWildcardId + customField ] end