diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index d4e2cdcfb1d00ed5b4eabbfdf85deef66a0a38ea..4541191730ad54973f06745bedce6380797c1ac5 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -132,6 +132,8 @@ export const TOKEN_TYPE_CONFIDENTIAL = 'confidential'; export const TOKEN_TYPE_ITERATION = 'iteration'; export const TOKEN_TYPE_EPIC = 'epic_id'; export const TOKEN_TYPE_WEIGHT = 'weight'; +export const TOKEN_TYPE_CONTACT = 'crm_contact'; +export const TOKEN_TYPE_ORGANIZATION = 'crm_organization'; export const filters = { [TOKEN_TYPE_AUTHOR]: { @@ -294,4 +296,24 @@ export const filters = { }, }, }, + [TOKEN_TYPE_CONTACT]: { + [API_PARAM]: { + [NORMAL_FILTER]: 'crmContactId', + }, + [URL_PARAM]: { + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'crm_contact_id', + }, + }, + }, + [TOKEN_TYPE_ORGANIZATION]: { + [API_PARAM]: { + [NORMAL_FILTER]: 'crmOrganizationId', + }, + [URL_PARAM]: { + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'crm_organization_id', + }, + }, + }, }; diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql index ec24ea7c56ae08a174d2f1a80bfe245c2b83d05e..dcc0db786b7597dbcf241a62c32deb96172c620c 100644 --- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql +++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql @@ -20,6 +20,8 @@ query getIssues( $releaseTag: [String!] $releaseTagWildcardId: ReleaseTagWildcardId $types: [IssueType!] + $crmContactId: String + $crmOrganizationId: String $not: NegatedIssueFilterInput $beforeCursor: String $afterCursor: String @@ -43,6 +45,8 @@ query getIssues( milestoneWildcardId: $milestoneWildcardId myReactionEmoji: $myReactionEmoji types: $types + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not before: $beforeCursor after: $afterCursor @@ -76,6 +80,8 @@ query getIssues( releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId types: $types + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not before: $beforeCursor after: $afterCursor diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql index 58e7ce32e7c9c54e3b29f12acc8171d84a8fcec6..c1aee772167ea3bc007fb5cfcc60a1f874d851eb 100644 --- a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql +++ b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql @@ -14,6 +14,8 @@ query getIssuesCount( $releaseTag: [String!] $releaseTagWildcardId: ReleaseTagWildcardId $types: [IssueType!] + $crmContactId: String + $crmOrganizationId: String $not: NegatedIssueFilterInput ) { group(fullPath: $fullPath) @skip(if: $isProject) { @@ -32,6 +34,8 @@ query getIssuesCount( milestoneWildcardId: $milestoneWildcardId myReactionEmoji: $myReactionEmoji types: $types + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -50,6 +54,8 @@ query getIssuesCount( milestoneWildcardId: $milestoneWildcardId myReactionEmoji: $myReactionEmoji types: $types + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -68,6 +74,8 @@ query getIssuesCount( milestoneWildcardId: $milestoneWildcardId myReactionEmoji: $myReactionEmoji types: $types + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -90,6 +98,8 @@ query getIssuesCount( releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId types: $types + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -109,6 +119,8 @@ query getIssuesCount( releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId types: $types + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -128,6 +140,8 @@ query getIssuesCount( releaseTag: $releaseTag releaseTagWildcardId: $releaseTagWildcardId types: $types + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index 432d6f48607f4df75a814cfecd0817316ee979a9..de44dbb26d7f92a8cf1e693d47c7237ae52d60b8 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -68,6 +68,12 @@ module IssueResolverArguments description: 'Negated arguments.', prepare: ->(negated_args, ctx) { negated_args.to_h }, required: false + argument :crm_contact_id, GraphQL::Types::String, + required: false, + description: 'ID of a contact assigned to the issues.' + argument :crm_organization_id, GraphQL::Types::String, + required: false, + description: 'ID of an organization assigned to the issues.' end def resolve_with_lookahead(**args) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7467144d5f45c01fc251cba9bcb60463c5306349..6b95f5e83d1bf27f79e4abd8ce755c5ed5423280 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11578,6 +11578,8 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="groupissuesconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. | | <a id="groupissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. | | <a id="groupissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this 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="groupissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | | <a id="groupissuesiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". | | <a id="groupissuesiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. | @@ -14813,6 +14815,8 @@ Returns [`Issue`](#issue). | <a id="projectissueconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. | | <a id="projectissuecreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. | | <a id="projectissuecreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this 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="projectissueepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | | <a id="projectissueiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". | | <a id="projectissueiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. | @@ -14853,6 +14857,8 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). | <a id="projectissuestatuscountsconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. | | <a id="projectissuestatuscountscreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. | | <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this 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="projectissuestatuscountsiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". | | <a id="projectissuestatuscountsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. | | <a id="projectissuestatuscountslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. | @@ -14890,6 +14896,8 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="projectissuesconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. | | <a id="projectissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. | | <a id="projectissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this 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="projectissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | | <a id="projectissuesiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". | | <a id="projectissuesiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. | diff --git a/ee/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/ee/app/assets/javascripts/issues/list/queries/get_issues.query.graphql index 5e88113b0f4f89c77d2c27617e3c7c678ccc0ae8..e89f8eb305d4a2cea10f60ab50c43f49a0a40637 100644 --- a/ee/app/assets/javascripts/issues/list/queries/get_issues.query.graphql +++ b/ee/app/assets/javascripts/issues/list/queries/get_issues.query.graphql @@ -24,6 +24,8 @@ query getIssuesEE( $iterationId: [ID] $iterationWildcardId: IterationWildcardId $weight: String + $crmContactId: String + $crmOrganizationId: String $not: NegatedIssueFilterInput $beforeCursor: String $afterCursor: String @@ -52,6 +54,8 @@ query getIssuesEE( iterationId: $iterationId iterationWildcardId: $iterationWildcardId weight: $weight + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not before: $beforeCursor after: $afterCursor @@ -93,6 +97,8 @@ query getIssuesEE( iterationId: $iterationId iterationWildcardId: $iterationWildcardId weight: $weight + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not before: $beforeCursor after: $afterCursor diff --git a/ee/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql b/ee/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql index cb687b7f6a009e701449a2ad0a489e850feef61e..7147764714c7406c5cbc77f30ad9a59d41dd1555 100644 --- a/ee/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql +++ b/ee/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql @@ -18,6 +18,8 @@ query getIssuesCountEE( $iterationId: [ID] $iterationWildcardId: IterationWildcardId $weight: String + $crmContactId: String + $crmOrganizationId: String $not: NegatedIssueFilterInput ) { group(fullPath: $fullPath) @skip(if: $isProject) { @@ -41,6 +43,8 @@ query getIssuesCountEE( iterationId: $iterationId iterationWildcardId: $iterationWildcardId weight: $weight + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -64,6 +68,8 @@ query getIssuesCountEE( iterationId: $iterationId iterationWildcardId: $iterationWildcardId weight: $weight + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -87,6 +93,8 @@ query getIssuesCountEE( iterationId: $iterationId iterationWildcardId: $iterationWildcardId weight: $weight + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -114,6 +122,8 @@ query getIssuesCountEE( iterationId: $iterationId iterationWildcardId: $iterationWildcardId weight: $weight + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -138,6 +148,8 @@ query getIssuesCountEE( iterationId: $iterationId iterationWildcardId: $iterationWildcardId weight: $weight + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count @@ -162,6 +174,8 @@ query getIssuesCountEE( iterationId: $iterationId iterationWildcardId: $iterationWildcardId weight: $weight + crmContactId: $crmContactId + crmOrganizationId: $crmOrganizationId not: $not ) { count diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js index b1a135ceb1899e50313022e465b1e080a09112ca..46f342cc673663bf3d27ecbf694ebe4c2c86e276 100644 --- a/spec/frontend/issues/list/mock_data.js +++ b/spec/frontend/issues/list/mock_data.js @@ -146,6 +146,8 @@ export const locationSearch = [ 'not[epic_id]=34', 'weight=1', 'not[weight]=3', + 'crm_contact_id=123', + 'crm_organization_id=456', ].join('&'); export const locationSearchWithSpecialValues = [ @@ -194,6 +196,8 @@ export const filteredTokens = [ { type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } }, { type: 'weight', value: { data: '1', operator: OPERATOR_IS } }, { type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } }, + { type: 'crm_contact', value: { data: '123', operator: OPERATOR_IS } }, + { type: 'crm_organization', value: { data: '456', operator: OPERATOR_IS } }, { type: 'filtered-search-term', value: { data: 'find' } }, { type: 'filtered-search-term', value: { data: 'issues' } }, ]; @@ -222,6 +226,8 @@ export const apiParams = { iterationId: ['4', '12'], epicId: '12', weight: '1', + crmContactId: '123', + crmOrganizationId: '456', not: { authorUsername: 'marge', assigneeUsernames: ['patty', 'selma'], @@ -270,6 +276,8 @@ export const urlParams = { 'not[epic_id]': '34', weight: '1', 'not[weight]': '3', + crm_contact_id: '123', + crm_organization_id: '456', }; export const urlParamsWithSpecialValues = { diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 81aeee0a3d24023c4bf8980b6d28399cf1b2457c..3569244e5148ff7a074d41dd6156a5ee7b298dd5 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -389,6 +389,34 @@ end end + describe 'filtering by crm' do + let_it_be(:organization) { create(:organization, group: group) } + let_it_be(:contact1) { create(:contact, group: group, organization: organization) } + let_it_be(:contact2) { create(:contact, group: group, organization: organization) } + let_it_be(:contact3) { create(:contact, group: group) } + let_it_be(:crm_issue1) { create(:issue, project: project) } + let_it_be(:crm_issue2) { create(:issue, project: project) } + let_it_be(:crm_issue3) { create(:issue, project: project) } + + before_all do + create(:issue_customer_relations_contact, issue: crm_issue1, contact: contact1) + create(:issue_customer_relations_contact, issue: crm_issue2, contact: contact2) + create(:issue_customer_relations_contact, issue: crm_issue3, contact: contact3) + end + + context 'contact' do + it 'returns only the issues for the contact' do + expect(resolve_issues({ crm_contact_id: contact1.id })).to contain_exactly(crm_issue1) + end + end + + context 'organization' do + it 'returns only the issues for the contact' do + expect(resolve_issues({ crm_organization_id: organization.id })).to contain_exactly(crm_issue1, crm_issue2) + end + end + end + describe 'sorting' do context 'when sorting by created' do it 'sorts issues ascending' do