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
ff17e0f3
Commit
ff17e0f3
authored
Dec 06, 2021
by
Lee Tickett
Committed by
Patrick Bair
Dec 06, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add ability to search issues by crm organization
parent
646e81c7
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
160 additions
and
22 deletions
+160
-22
app/assets/javascripts/crm/components/contacts_root.vue
app/assets/javascripts/crm/components/contacts_root.vue
+4
-1
app/assets/javascripts/crm/components/organizations_root.vue
app/assets/javascripts/crm/components/organizations_root.vue
+43
-11
app/assets/javascripts/crm/organizations_bundle.js
app/assets/javascripts/crm/organizations_bundle.js
+3
-1
app/finders/issuable_finder.rb
app/finders/issuable_finder.rb
+8
-1
app/finders/issuables/crm_organization_filter.rb
app/finders/issuables/crm_organization_filter.rb
+21
-0
app/views/groups/crm/organizations/index.html.haml
app/views/groups/crm/organizations/index.html.haml
+1
-1
spec/finders/issuables/crm_organization_filter_spec.rb
spec/finders/issuables/crm_organization_filter_spec.rb
+48
-0
spec/finders/issues_finder_spec.rb
spec/finders/issues_finder_spec.rb
+21
-1
spec/frontend/crm/organizations_root_spec.js
spec/frontend/crm/organizations_root_spec.js
+11
-6
No files found.
app/assets/javascripts/crm/components/contacts_root.vue
View file @
ff17e0f3
...
@@ -71,6 +71,9 @@ export default {
...
@@ -71,6 +71,9 @@ export default {
this
.
error
=
false
;
this
.
error
=
false
;
this
.
errorMessages
=
[];
this
.
errorMessages
=
[];
},
},
getIssuesPath
(
path
,
value
)
{
return
`
${
path
}
?scope=all&state=opened&crm_contact_id=
${
value
}
`
;
},
},
},
fields
:
[
fields
:
[
{
key
:
'
firstName
'
,
sortable
:
true
},
{
key
:
'
firstName
'
,
sortable
:
true
},
...
@@ -142,7 +145,7 @@ export default {
...
@@ -142,7 +145,7 @@ export default {
data-testid=
"issues-link"
data-testid=
"issues-link"
icon=
"issues"
icon=
"issues"
:aria-label=
"$options.i18n.issuesButtonLabel"
:aria-label=
"$options.i18n.issuesButtonLabel"
:href=
"
`$
{groupIssuesPath}?scope=all
&
state=opened
&
crm_contact_id=${data.value}`
"
:href=
"
getIssuesPath(groupIssuesPath, data.value)
"
/>
/>
</
template
>
</
template
>
</gl-table>
</gl-table>
...
...
app/assets/javascripts/crm/components/organizations_root.vue
View file @
ff17e0f3
<
script
>
<
script
>
import
{
GlLoadingIcon
,
GlTable
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
GlButton
,
GlLoadingIcon
,
GlTable
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
createFlash
from
'
~/flash
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
getGroupOrganizationsQuery
from
'
./queries/get_group_organizations.query.graphql
'
;
import
getGroupOrganizationsQuery
from
'
./queries/get_group_organizations.query.graphql
'
;
export
default
{
export
default
{
components
:
{
components
:
{
GlAlert
,
GlButton
,
GlLoadingIcon
,
GlLoadingIcon
,
GlTable
,
GlTable
,
},
},
inject
:
[
'
groupFullPath
'
],
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
inject
:
[
'
groupFullPath
'
,
'
groupIssuesPath
'
],
data
()
{
data
()
{
return
{
organizations
:
[]
};
return
{
error
:
false
,
organizations
:
[],
};
},
},
apollo
:
{
apollo
:
{
organizations
:
{
organizations
:
{
...
@@ -26,12 +34,8 @@ export default {
...
@@ -26,12 +34,8 @@ export default {
update
(
data
)
{
update
(
data
)
{
return
this
.
extractOrganizations
(
data
);
return
this
.
extractOrganizations
(
data
);
},
},
error
(
error
)
{
error
()
{
createFlash
({
this
.
error
=
true
;
message
:
__
(
'
Something went wrong. Please try again.
'
),
error
,
captureError
:
true
,
});
},
},
},
},
},
},
...
@@ -45,20 +49,38 @@ export default {
...
@@ -45,20 +49,38 @@ export default {
const
organizations
=
data
?.
group
?.
organizations
?.
nodes
||
[];
const
organizations
=
data
?.
group
?.
organizations
?.
nodes
||
[];
return
organizations
.
slice
().
sort
((
a
,
b
)
=>
a
.
name
.
localeCompare
(
b
.
name
));
return
organizations
.
slice
().
sort
((
a
,
b
)
=>
a
.
name
.
localeCompare
(
b
.
name
));
},
},
dismissError
()
{
this
.
error
=
false
;
},
getIssuesPath
(
path
,
value
)
{
return
`
${
path
}
?scope=all&state=opened&crm_organization_id=
${
value
}
`
;
},
},
},
fields
:
[
fields
:
[
{
key
:
'
name
'
,
sortable
:
true
},
{
key
:
'
name
'
,
sortable
:
true
},
{
key
:
'
defaultRate
'
,
sortable
:
true
},
{
key
:
'
defaultRate
'
,
sortable
:
true
},
{
key
:
'
description
'
,
sortable
:
true
},
{
key
:
'
description
'
,
sortable
:
true
},
{
key
:
'
id
'
,
label
:
__
(
'
Issues
'
),
formatter
:
(
id
)
=>
{
return
getIdFromGraphQLId
(
id
);
},
},
],
],
i18n
:
{
i18n
:
{
emptyText
:
s__
(
'
Crm|No organizations found
'
),
emptyText
:
s__
(
'
Crm|No organizations found
'
),
issuesButtonLabel
:
__
(
'
View issues
'
),
errorText
:
__
(
'
Something went wrong. Please try again.
'
),
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div>
<div>
<gl-alert
v-if=
"error"
variant=
"danger"
class=
"gl-my-6"
@
dismiss=
"dismissError"
>
<div>
{{
$options
.
i18n
.
errorText
}}
</div>
</gl-alert>
<gl-loading-icon
v-if=
"isLoading"
class=
"gl-mt-5"
size=
"lg"
/>
<gl-loading-icon
v-if=
"isLoading"
class=
"gl-mt-5"
size=
"lg"
/>
<gl-table
<gl-table
v-else
v-else
...
@@ -66,6 +88,16 @@ export default {
...
@@ -66,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=
"getIssuesPath(groupIssuesPath, data.value)"
/>
</
template
>
</gl-table>
</div>
</div>
</template>
</template>
app/assets/javascripts/crm/organizations_bundle.js
View file @
ff17e0f3
...
@@ -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
(
CrmOrganizationsRoot
);
return
createElement
(
CrmOrganizationsRoot
);
},
},
...
...
app/finders/issuable_finder.rb
View file @
ff17e0f3
...
@@ -36,6 +36,7 @@
...
@@ -36,6 +36,7 @@
# attempt_group_search_optimizations: boolean
# attempt_group_search_optimizations: boolean
# attempt_project_search_optimizations: boolean
# attempt_project_search_optimizations: boolean
# crm_contact_id: integer
# crm_contact_id: integer
# crm_organization_id: integer
#
#
class
IssuableFinder
class
IssuableFinder
prepend
FinderWithCrossProjectAccess
prepend
FinderWithCrossProjectAccess
...
@@ -61,6 +62,7 @@ class IssuableFinder
...
@@ -61,6 +62,7 @@ class IssuableFinder
author_id
author_id
author_username
author_username
crm_contact_id
crm_contact_id
crm_organization_id
label_name
label_name
milestone_title
milestone_title
release_tag
release_tag
...
@@ -141,7 +143,8 @@ class IssuableFinder
...
@@ -141,7 +143,8 @@ class IssuableFinder
items
=
by_release
(
items
)
items
=
by_release
(
items
)
items
=
by_label
(
items
)
items
=
by_label
(
items
)
items
=
by_my_reaction_emoji
(
items
)
items
=
by_my_reaction_emoji
(
items
)
by_crm_contact
(
items
)
items
=
by_crm_contact
(
items
)
by_crm_organization
(
items
)
end
end
def
should_filter_negated_args?
def
should_filter_negated_args?
...
@@ -470,6 +473,10 @@ class IssuableFinder
...
@@ -470,6 +473,10 @@ class IssuableFinder
Issuables
::
CrmContactFilter
.
new
(
params:
original_params
).
filter
(
items
)
Issuables
::
CrmContactFilter
.
new
(
params:
original_params
).
filter
(
items
)
end
end
def
by_crm_organization
(
items
)
Issuables
::
CrmOrganizationFilter
.
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_organization_filter.rb
0 → 100644
View file @
ff17e0f3
# frozen_string_literal: true
module
Issuables
class
CrmOrganizationFilter
<
BaseFilter
def
filter
(
issuables
)
by_crm_organization
(
issuables
)
end
# rubocop: disable CodeReuse/ActiveRecord
def
by_crm_organization
(
issuables
)
return
issuables
if
params
[
:crm_organization_id
].
blank?
condition
=
CustomerRelations
::
IssueContact
.
joins
(
:contact
)
.
where
(
contact:
{
organization_id:
params
[
:crm_organization_id
]
})
.
where
(
Arel
.
sql
(
"issue_id = issues.id"
))
issuables
.
where
(
condition
.
arel
.
exists
)
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
app/views/groups/crm/organizations/index.html.haml
View file @
ff17e0f3
-
breadcrumb_title
_
(
'Customer Relations Organizations'
)
-
breadcrumb_title
_
(
'Customer Relations Organizations'
)
-
page_title
_
(
'Customer Relations Organizations'
)
-
page_title
_
(
'Customer Relations Organizations'
)
#js-crm-organizations-app
{
data:
{
group_full_path:
@group
.
full_path
}
}
#js-crm-organizations-app
{
data:
{
group_full_path:
@group
.
full_path
,
group_issues_path:
issues_group_path
(
@group
)
}
}
spec/finders/issuables/crm_organization_filter_spec.rb
0 → 100644
View file @
ff17e0f3
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
Issuables
::
CrmOrganizationFilter
do
let_it_be
(
:group
)
{
create
(
:group
)
}
let_it_be
(
:project
)
{
create
(
:project
,
group:
group
)
}
let_it_be
(
:organization1
)
{
create
(
:organization
,
group:
group
)
}
let_it_be
(
:organization2
)
{
create
(
:organization
,
group:
group
)
}
let_it_be
(
:contact1
)
{
create
(
:contact
,
group:
group
,
organization:
organization1
)
}
let_it_be
(
:contact2
)
{
create
(
:contact
,
group:
group
,
organization:
organization1
)
}
let_it_be
(
:contact3
)
{
create
(
:contact
,
group:
group
,
organization:
organization2
)
}
let_it_be
(
:contact1_issue
)
{
create
(
:issue
,
project:
project
)
}
let_it_be
(
:contact2_issue
)
{
create
(
:issue
,
project:
project
)
}
let_it_be
(
:contact3_issue
)
{
create
(
:issue
,
project:
project
)
}
let_it_be
(
:issues
)
{
Issue
.
where
(
id:
[
contact1_issue
.
id
,
contact2_issue
.
id
,
contact3_issue
.
id
])
}
before_all
do
create
(
:issue_customer_relations_contact
,
issue:
contact1_issue
,
contact:
contact1
)
create
(
:issue_customer_relations_contact
,
issue:
contact2_issue
,
contact:
contact2
)
create
(
:issue_customer_relations_contact
,
issue:
contact3_issue
,
contact:
contact3
)
end
describe
'when an organization has issues'
do
it
'returns all organization1 issues'
do
params
=
{
crm_organization_id:
organization1
.
id
}
expect
(
described_class
.
new
(
params:
params
).
filter
(
issues
)).
to
contain_exactly
(
contact1_issue
,
contact2_issue
)
end
it
'returns all organization2 issues'
do
params
=
{
crm_organization_id:
organization2
.
id
}
expect
(
described_class
.
new
(
params:
params
).
filter
(
issues
)).
to
contain_exactly
(
contact3_issue
)
end
end
describe
'when an organization has no issues'
do
it
'returns no issues'
do
organization3
=
create
(
:organization
,
group:
group
)
params
=
{
crm_organization_id:
organization3
.
id
}
expect
(
described_class
.
new
(
params:
params
).
filter
(
issues
)).
to
be_empty
end
end
end
spec/finders/issues_finder_spec.rb
View file @
ff17e0f3
...
@@ -920,7 +920,7 @@ RSpec.describe IssuesFinder do
...
@@ -920,7 +920,7 @@ RSpec.describe IssuesFinder do
let
(
:params
)
{
{
crm_contact_id:
contact1
.
id
}
}
let
(
:params
)
{
{
crm_contact_id:
contact1
.
id
}
}
it
'returns
issues with that label
'
do
it
'returns
for that contact
'
do
create
(
:issue_customer_relations_contact
,
issue:
contact1_issue1
,
contact:
contact1
)
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:
contact1_issue2
,
contact:
contact1
)
create
(
:issue_customer_relations_contact
,
issue:
contact2_issue1
,
contact:
contact2
)
create
(
:issue_customer_relations_contact
,
issue:
contact2_issue1
,
contact:
contact2
)
...
@@ -929,6 +929,26 @@ RSpec.describe IssuesFinder do
...
@@ -929,6 +929,26 @@ RSpec.describe IssuesFinder do
end
end
end
end
context
'filtering by crm organization'
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
(
: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_organization_id:
organization
.
id
}
}
it
'returns for that contact'
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
,
contact2_issue1
)
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/organizations_root_spec.js
View file @
ff17e0f3
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
Gl
Alert
,
Gl
LoadingIcon
}
from
'
@gitlab/ui
'
;
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
mountExtended
,
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
{
mountExtended
,
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
OrganizationsRoot
from
'
~/crm/components/organizations_root.vue
'
;
import
OrganizationsRoot
from
'
~/crm/components/organizations_root.vue
'
;
import
getGroupOrganizationsQuery
from
'
~/crm/components/queries/get_group_organizations.query.graphql
'
;
import
getGroupOrganizationsQuery
from
'
~/crm/components/queries/get_group_organizations.query.graphql
'
;
import
{
getGroupOrganizationsQueryResponse
}
from
'
./mock_data
'
;
import
{
getGroupOrganizationsQueryResponse
}
from
'
./mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
describe
(
'
Customer relations organizations root app
'
,
()
=>
{
describe
(
'
Customer relations organizations root app
'
,
()
=>
{
Vue
.
use
(
VueApollo
);
Vue
.
use
(
VueApollo
);
let
wrapper
;
let
wrapper
;
...
@@ -18,6 +15,8 @@ describe('Customer relations organizations root app', () => {
...
@@ -18,6 +15,8 @@ describe('Customer relations organizations 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
findError
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
successQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
getGroupOrganizationsQueryResponse
);
const
successQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
getGroupOrganizationsQueryResponse
);
const
mountComponent
=
({
const
mountComponent
=
({
...
@@ -26,7 +25,7 @@ describe('Customer relations organizations root app', () => {
...
@@ -26,7 +25,7 @@ describe('Customer relations organizations root app', () => {
}
=
{})
=>
{
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([[
getGroupOrganizationsQuery
,
queryHandler
]]);
fakeApollo
=
createMockApollo
([[
getGroupOrganizationsQuery
,
queryHandler
]]);
wrapper
=
mountFunction
(
OrganizationsRoot
,
{
wrapper
=
mountFunction
(
OrganizationsRoot
,
{
provide
:
{
groupFullPath
:
'
flightjs
'
},
provide
:
{
groupFullPath
:
'
flightjs
'
,
groupIssuesPath
:
'
/issues
'
},
apolloProvider
:
fakeApollo
,
apolloProvider
:
fakeApollo
,
});
});
};
};
...
@@ -46,7 +45,7 @@ describe('Customer relations organizations root app', () => {
...
@@ -46,7 +45,7 @@ describe('Customer relations organizations root app', () => {
mountComponent
({
queryHandler
:
jest
.
fn
().
mockRejectedValue
(
'
ERROR
'
)
});
mountComponent
({
queryHandler
:
jest
.
fn
().
mockRejectedValue
(
'
ERROR
'
)
});
await
waitForPromises
();
await
waitForPromises
();
expect
(
createFlash
).
toHaveBeenCalled
(
);
expect
(
findError
().
exists
()).
toBe
(
true
);
});
});
it
(
'
renders correct results
'
,
async
()
=>
{
it
(
'
renders correct results
'
,
async
()
=>
{
...
@@ -56,5 +55,11 @@ describe('Customer relations organizations root app', () => {
...
@@ -56,5 +55,11 @@ describe('Customer relations organizations root app', () => {
expect
(
findRowByName
(
/Test Inc/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/Test Inc/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/VIP/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/VIP/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/120/i
)).
toHaveLength
(
1
);
expect
(
findRowByName
(
/120/i
)).
toHaveLength
(
1
);
const
issueLink
=
findIssuesLinks
().
at
(
0
);
expect
(
issueLink
.
exists
()).
toBe
(
true
);
expect
(
issueLink
.
attributes
(
'
href
'
)).
toBe
(
'
/issues?scope=all&state=opened&crm_organization_id=2
'
,
);
});
});
});
});
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