Commit 551a78e7 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '2256-search-issues-by-crm-contact' into 'master'

Add ability to search issues by crm contact

See merge request gitlab-org/gitlab!75451
parents d8506e9b 93d54410
<script>
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
export default {
components: {
GlButton,
GlLoadingIcon,
GlTable,
},
inject: ['groupFullPath'],
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['groupFullPath', 'groupIssuesPath'],
data() {
return { contacts: [] };
},
......@@ -59,9 +64,17 @@ export default {
},
sortable: true,
},
{
key: 'id',
label: __('Issues'),
formatter: (id) => {
return getIdFromGraphQLId(id);
},
},
],
i18n: {
emptyText: s__('Crm|No contacts found'),
issuesButtonLabel: __('View issues'),
},
};
</script>
......@@ -75,6 +88,16 @@ export default {
:fields="$options.fields"
:empty-text="$options.i18n.emptyText"
show-empty
>
<template #cell(id)="data">
<gl-button
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="`${groupIssuesPath}?scope=all&state=opened&crm_contact_id=${data.value}`"
/>
</template>
</gl-table>
</div>
</template>
......@@ -16,10 +16,12 @@ export default () => {
return false;
}
const { groupFullPath, groupIssuesPath } = el.dataset;
return new Vue({
el,
apolloProvider,
provide: { groupFullPath: el.dataset.groupFullPath },
provide: { groupFullPath, groupIssuesPath },
render(createElement) {
return createElement(CrmContactsRoot);
},
......
......@@ -35,6 +35,7 @@
# updated_before: datetime
# attempt_group_search_optimizations: boolean
# attempt_project_search_optimizations: boolean
# crm_contact_id: integer
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
......@@ -59,6 +60,7 @@ class IssuableFinder
assignee_username
author_id
author_username
crm_contact_id
label_name
milestone_title
release_tag
......@@ -138,7 +140,8 @@ class IssuableFinder
items = by_milestone(items)
items = by_release(items)
items = by_label(items)
by_my_reaction_emoji(items)
items = by_my_reaction_emoji(items)
by_crm_contact(items)
end
def should_filter_negated_args?
......@@ -463,6 +466,10 @@ class IssuableFinder
params[:non_archived].present? ? items.non_archived : items
end
def by_crm_contact(items)
Issuables::CrmContactFilter.new(params: original_params).filter(items)
end
def or_filters_enabled?
strong_memoize(:or_filters_enabled) do
Feature.enabled?(:or_issuable_queries, feature_flag_scope, default_enabled: :yaml)
......
# frozen_string_literal: true
module Issuables
class CrmContactFilter < BaseFilter
def filter(issuables)
by_crm_contact(issuables)
end
# rubocop: disable CodeReuse/ActiveRecord
def by_crm_contact(issuables)
return issuables if params[:crm_contact_id].blank?
condition = CustomerRelations::IssueContact
.where(contact_id: params[:crm_contact_id])
.where(Arel.sql("issue_id = issues.id"))
issuables.where(condition.arel.exists)
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
- breadcrumb_title _('Customer Relations Contacts')
- page_title _('Customer Relations Contacts')
#js-crm-contacts-app{ data: { group_full_path: @group.full_path } }
#js-crm-contacts-app{ data: { group_full_path: @group.full_path, group_issues_path: issues_group_path(@group) } }
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Issuables::CrmContactFilter do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:contact1) { create(:contact, group: group) }
let_it_be(:contact2) { create(:contact, group: group) }
let_it_be(:contact1_issue1) { create(:issue, project: project) }
let_it_be(:contact1_issue2) { create(:issue, project: project) }
let_it_be(:contact2_issue1) { create(:issue, project: project) }
let_it_be(:issues) { Issue.where(id: [contact1_issue1.id, contact1_issue2.id, contact2_issue1.id]) }
before_all do
create(:issue_customer_relations_contact, issue: contact1_issue1, contact: contact1)
create(:issue_customer_relations_contact, issue: contact1_issue2, contact: contact1)
create(:issue_customer_relations_contact, issue: contact2_issue1, contact: contact2)
end
describe 'when a contact has issues' do
it 'returns all contact1 issues' do
params = { crm_contact_id: contact1.id }
expect(described_class.new(params: params).filter(issues)).to contain_exactly(contact1_issue1, contact1_issue2)
end
it 'returns all contact2 issues' do
params = { crm_contact_id: contact2.id }
expect(described_class.new(params: params).filter(issues)).to contain_exactly(contact2_issue1)
end
end
describe 'when a contact has no issues' do
it 'returns no issues' do
contact3 = create(:contact, group: group)
params = { crm_contact_id: contact3.id }
expect(described_class.new(params: params).filter(issues)).to be_empty
end
end
end
......@@ -910,6 +910,25 @@ RSpec.describe IssuesFinder do
end
end
context 'filtering by crm contact' do
let_it_be(:contact1) { create(:contact, group: group) }
let_it_be(:contact2) { create(:contact, group: group) }
let_it_be(:contact1_issue1) { create(:issue, project: project1) }
let_it_be(:contact1_issue2) { create(:issue, project: project1) }
let_it_be(:contact2_issue1) { create(:issue, project: project1) }
let(:params) { { crm_contact_id: contact1.id } }
it 'returns issues with that label' do
create(:issue_customer_relations_contact, issue: contact1_issue1, contact: contact1)
create(:issue_customer_relations_contact, issue: contact1_issue2, contact: contact1)
create(:issue_customer_relations_contact, issue: contact2_issue1, contact: contact2)
expect(issues).to contain_exactly(contact1_issue1, contact1_issue2)
end
end
context 'when the user is unauthorized' do
let(:search_user) { nil }
......
......@@ -18,6 +18,7 @@ describe('Customer relations contacts root app', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
const mountComponent = ({
......@@ -26,7 +27,7 @@ describe('Customer relations contacts root app', () => {
} = {}) => {
fakeApollo = createMockApollo([[getGroupContactsQuery, queryHandler]]);
wrapper = mountFunction(ContactsRoot, {
provide: { groupFullPath: 'flightjs' },
provide: { groupFullPath: 'flightjs', groupIssuesPath: '/issues' },
apolloProvider: fakeApollo,
});
};
......@@ -56,5 +57,9 @@ describe('Customer relations contacts root app', () => {
expect(findRowByName(/Marty/i)).toHaveLength(1);
expect(findRowByName(/George/i)).toHaveLength(1);
expect(findRowByName(/jd@gitlab.com/i)).toHaveLength(1);
const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true);
expect(issueLink.attributes('href')).toBe('/issues?scope=all&state=opened&crm_contact_id=16');
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment