Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
93d54410
Commit
93d54410
authored
Dec 03, 2021
by
Lee Tickett
Committed by
Heinrich Lee Yu
Dec 03, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add ability to search issues by crm contact
parent
bd86b61a
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
128 additions
and
7 deletions
+128
-7
app/assets/javascripts/crm/components/contacts_root.vue
app/assets/javascripts/crm/components/contacts_root.vue
+26
-3
app/assets/javascripts/crm/contacts_bundle.js
app/assets/javascripts/crm/contacts_bundle.js
+3
-1
app/finders/issuable_finder.rb
app/finders/issuable_finder.rb
+8
-1
app/finders/issuables/crm_contact_filter.rb
app/finders/issuables/crm_contact_filter.rb
+20
-0
app/views/groups/crm/contacts.html.haml
app/views/groups/crm/contacts.html.haml
+1
-1
spec/finders/issuables/crm_contact_filter_spec.rb
spec/finders/issuables/crm_contact_filter_spec.rb
+45
-0
spec/finders/issues_finder_spec.rb
spec/finders/issues_finder_spec.rb
+19
-0
spec/frontend/crm/contacts_root_spec.js
spec/frontend/crm/contacts_root_spec.js
+6
-1
No files found.
app/assets/javascripts/crm/components/contacts_root.vue
View file @
93d54410
<
script
>
<
script
>
import
{
Gl
LoadingIcon
,
GlTabl
e
}
from
'
@gitlab/ui
'
;
import
{
Gl
Button
,
GlLoadingIcon
,
GlTable
,
GlTooltipDirectiv
e
}
from
'
@gitlab/ui
'
;
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
getGroupContactsQuery
from
'
./queries/get_group_contacts.query.graphql
'
;
import
getGroupContactsQuery
from
'
./queries/get_group_contacts.query.graphql
'
;
export
default
{
export
default
{
components
:
{
components
:
{
GlButton
,
GlLoadingIcon
,
GlLoadingIcon
,
GlTable
,
GlTable
,
},
},
inject
:
[
'
groupFullPath
'
],
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
inject
:
[
'
groupFullPath
'
,
'
groupIssuesPath
'
],
data
()
{
data
()
{
return
{
contacts
:
[]
};
return
{
contacts
:
[]
};
},
},
...
@@ -59,9 +64,17 @@ export default {
...
@@ -59,9 +64,17 @@ export default {
},
},
sortable
:
true
,
sortable
:
true
,
},
},
{
key
:
'
id
'
,
label
:
__
(
'
Issues
'
),
formatter
:
(
id
)
=>
{
return
getIdFromGraphQLId
(
id
);
},
},
],
],
i18n
:
{
i18n
:
{
emptyText
:
s__
(
'
Crm|No contacts found
'
),
emptyText
:
s__
(
'
Crm|No contacts found
'
),
issuesButtonLabel
:
__
(
'
View issues
'
),
},
},
};
};
</
script
>
</
script
>
...
@@ -75,6 +88,16 @@ export default {
...
@@ -75,6 +88,16 @@ export default {
:fields=
"$options.fields"
:fields=
"$options.fields"
:empty-text=
"$options.i18n.emptyText"
:empty-text=
"$options.i18n.emptyText"
show-empty
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>
</div>
</template>
</template>
app/assets/javascripts/crm/contacts_bundle.js
View file @
93d54410
...
@@ -16,10 +16,12 @@ export default () => {
...
@@ -16,10 +16,12 @@ export default () => {
return
false
;
return
false
;
}
}
const
{
groupFullPath
,
groupIssuesPath
}
=
el
.
dataset
;
return
new
Vue
({
return
new
Vue
({
el
,
el
,
apolloProvider
,
apolloProvider
,
provide
:
{
groupFullPath
:
el
.
dataset
.
groupFull
Path
},
provide
:
{
groupFullPath
,
groupIssues
Path
},
render
(
createElement
)
{
render
(
createElement
)
{
return
createElement
(
CrmContactsRoot
);
return
createElement
(
CrmContactsRoot
);
},
},
...
...
app/finders/issuable_finder.rb
View file @
93d54410
...
@@ -35,6 +35,7 @@
...
@@ -35,6 +35,7 @@
# updated_before: datetime
# updated_before: datetime
# attempt_group_search_optimizations: boolean
# attempt_group_search_optimizations: boolean
# attempt_project_search_optimizations: boolean
# attempt_project_search_optimizations: boolean
# crm_contact_id: integer
#
#
class
IssuableFinder
class
IssuableFinder
prepend
FinderWithCrossProjectAccess
prepend
FinderWithCrossProjectAccess
...
@@ -59,6 +60,7 @@ class IssuableFinder
...
@@ -59,6 +60,7 @@ class IssuableFinder
assignee_username
assignee_username
author_id
author_id
author_username
author_username
crm_contact_id
label_name
label_name
milestone_title
milestone_title
release_tag
release_tag
...
@@ -138,7 +140,8 @@ class IssuableFinder
...
@@ -138,7 +140,8 @@ class IssuableFinder
items
=
by_milestone
(
items
)
items
=
by_milestone
(
items
)
items
=
by_release
(
items
)
items
=
by_release
(
items
)
items
=
by_label
(
items
)
items
=
by_label
(
items
)
by_my_reaction_emoji
(
items
)
items
=
by_my_reaction_emoji
(
items
)
by_crm_contact
(
items
)
end
end
def
should_filter_negated_args?
def
should_filter_negated_args?
...
@@ -463,6 +466,10 @@ class IssuableFinder
...
@@ -463,6 +466,10 @@ class IssuableFinder
params
[
:non_archived
].
present?
?
items
.
non_archived
:
items
params
[
:non_archived
].
present?
?
items
.
non_archived
:
items
end
end
def
by_crm_contact
(
items
)
Issuables
::
CrmContactFilter
.
new
(
params:
original_params
).
filter
(
items
)
end
def
or_filters_enabled?
def
or_filters_enabled?
strong_memoize
(
:or_filters_enabled
)
do
strong_memoize
(
:or_filters_enabled
)
do
Feature
.
enabled?
(
:or_issuable_queries
,
feature_flag_scope
,
default_enabled: :yaml
)
Feature
.
enabled?
(
:or_issuable_queries
,
feature_flag_scope
,
default_enabled: :yaml
)
...
...
app/finders/issuables/crm_contact_filter.rb
0 → 100644
View file @
93d54410
# 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
app/views/groups/crm/contacts.html.haml
View file @
93d54410
-
breadcrumb_title
_
(
'Customer Relations Contacts'
)
-
breadcrumb_title
_
(
'Customer Relations Contacts'
)
-
page_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
)
}
}
spec/finders/issuables/crm_contact_filter_spec.rb
0 → 100644
View file @
93d54410
# 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
spec/finders/issues_finder_spec.rb
View file @
93d54410
...
@@ -910,6 +910,25 @@ RSpec.describe IssuesFinder do
...
@@ -910,6 +910,25 @@ RSpec.describe IssuesFinder do
end
end
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
context
'when the user is unauthorized'
do
let
(
:search_user
)
{
nil
}
let
(
:search_user
)
{
nil
}
...
...
spec/frontend/crm/contacts_root_spec.js
View file @
93d54410
...
@@ -18,6 +18,7 @@ describe('Customer relations contacts root app', () => {
...
@@ -18,6 +18,7 @@ describe('Customer relations contacts root app', () => {
const
findLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
const
findLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
const
findRowByName
=
(
rowName
)
=>
wrapper
.
findAllByRole
(
'
row
'
,
{
name
:
rowName
});
const
findRowByName
=
(
rowName
)
=>
wrapper
.
findAllByRole
(
'
row
'
,
{
name
:
rowName
});
const
findIssuesLinks
=
()
=>
wrapper
.
findAllByTestId
(
'
issues-link
'
);
const
successQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
getGroupContactsQueryResponse
);
const
successQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
getGroupContactsQueryResponse
);
const
mountComponent
=
({
const
mountComponent
=
({
...
@@ -26,7 +27,7 @@ describe('Customer relations contacts root app', () => {
...
@@ -26,7 +27,7 @@ describe('Customer relations contacts root app', () => {
}
=
{})
=>
{
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([[
getGroupContactsQuery
,
queryHandler
]]);
fakeApollo
=
createMockApollo
([[
getGroupContactsQuery
,
queryHandler
]]);
wrapper
=
mountFunction
(
ContactsRoot
,
{
wrapper
=
mountFunction
(
ContactsRoot
,
{
provide
:
{
groupFullPath
:
'
flightjs
'
},
provide
:
{
groupFullPath
:
'
flightjs
'
,
groupIssuesPath
:
'
/issues
'
},
apolloProvider
:
fakeApollo
,
apolloProvider
:
fakeApollo
,
});
});
};
};
...
@@ -56,5 +57,9 @@ describe('Customer relations contacts root app', () => {
...
@@ -56,5 +57,9 @@ describe('Customer relations contacts root app', () => {
expect
(
findRowByName
(
/Marty/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/Marty/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/George/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/George/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/jd@gitlab.com/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
'
);
});
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment