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
db0b9254
Commit
db0b9254
authored
Nov 10, 2021
by
Lee Tickett
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Display issue crm contacts in UI
Changelog: added
parent
c74db77d
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
358 additions
and
1 deletion
+358
-1
app/assets/javascripts/issue_show/issue.js
app/assets/javascripts/issue_show/issue.js
+1
-0
app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
...ascripts/sidebar/components/crm_contacts/crm_contacts.vue
+131
-0
app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql
...crm_contacts/queries/get_issue_crm_contacts.query.graphql
+7
-0
app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql
.../crm_contacts/queries/issue_crm_contacts.fragment.graphql
+17
-0
app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql
..._contacts/queries/issue_crm_contacts.subscription.graphql
+9
-0
app/assets/javascripts/sidebar/mount_sidebar.js
app/assets/javascripts/sidebar/mount_sidebar.js
+24
-0
app/controllers/concerns/issuable_actions.rb
app/controllers/concerns/issuable_actions.rb
+1
-0
app/graphql/graphql_triggers.rb
app/graphql/graphql_triggers.rb
+4
-0
app/graphql/types/subscription_type.rb
app/graphql/types/subscription_type.rb
+3
-0
app/services/issues/set_crm_contacts_service.rb
app/services/issues/set_crm_contacts_service.rb
+1
-0
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+4
-0
locale/gitlab.pot
locale/gitlab.pot
+11
-0
spec/features/action_cable_logging_spec.rb
spec/features/action_cable_logging_spec.rb
+1
-1
spec/frontend/sidebar/components/crm_contacts_spec.js
spec/frontend/sidebar/components/crm_contacts_spec.js
+87
-0
spec/frontend/sidebar/components/mock_data.js
spec/frontend/sidebar/components/mock_data.js
+56
-0
spec/graphql/types/subscription_type_spec.rb
spec/graphql/types/subscription_type_spec.rb
+1
-0
No files found.
app/assets/javascripts/issue_show/issue.js
View file @
db0b9254
...
...
@@ -44,6 +44,7 @@ export function initIssuableApp(issuableData, store) {
isConfidential
:
this
.
getNoteableData
?.
confidential
,
isLocked
:
this
.
getNoteableData
?.
discussion_locked
,
issuableStatus
:
this
.
getNoteableData
?.
state
,
id
:
this
.
getNoteableData
?.
id
,
},
});
},
...
...
app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
0 → 100644
View file @
db0b9254
<
script
>
import
{
GlIcon
,
GlPopover
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
__
,
n__
,
sprintf
}
from
'
~/locale
'
;
import
createFlash
from
'
~/flash
'
;
import
{
convertToGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
TYPE_ISSUE
}
from
'
~/graphql_shared/constants
'
;
import
getIssueCrmContactsQuery
from
'
./queries/get_issue_crm_contacts.query.graphql
'
;
import
issueCrmContactsSubscription
from
'
./queries/issue_crm_contacts.subscription.graphql
'
;
export
default
{
components
:
{
GlIcon
,
GlPopover
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
issueId
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
contacts
:
[],
};
},
apollo
:
{
contacts
:
{
query
:
getIssueCrmContactsQuery
,
variables
()
{
return
this
.
queryVariables
;
},
update
(
data
)
{
return
data
?.
issue
?.
customerRelationsContacts
?.
nodes
;
},
error
(
error
)
{
createFlash
({
message
:
__
(
'
Something went wrong trying to load issue contacts.
'
),
error
,
captureError
:
true
,
});
},
subscribeToMore
:
{
document
:
issueCrmContactsSubscription
,
variables
()
{
return
this
.
queryVariables
;
},
updateQuery
(
prev
,
{
subscriptionData
})
{
const
draftData
=
subscriptionData
?.
data
?.
issueCrmContactsUpdated
;
if
(
prev
&&
draftData
)
return
{
issue
:
draftData
};
return
prev
;
},
},
},
},
computed
:
{
shouldShowContacts
()
{
return
this
.
contacts
?.
length
;
},
queryVariables
()
{
return
{
id
:
convertToGraphQLId
(
TYPE_ISSUE
,
this
.
issueId
)
};
},
contactsLabel
()
{
return
sprintf
(
n__
(
'
%{count} contact
'
,
'
%{count} contacts
'
,
this
.
contactCount
),
{
count
:
this
.
contactCount
,
});
},
contactCount
()
{
return
this
.
contacts
?.
length
||
0
;
},
},
methods
:
{
shouldShowPopover
(
contact
)
{
return
this
.
popOverData
(
contact
).
length
>
0
;
},
divider
(
index
)
{
if
(
index
<
this
.
contactCount
-
1
)
return
'
,
'
;
return
''
;
},
popOverData
(
contact
)
{
return
[
contact
.
organization
?.
name
,
contact
.
email
,
contact
.
phone
,
contact
.
description
].
filter
(
Boolean
,
);
},
},
i18n
:
{
help
:
__
(
'
Work in progress- click here to find out more
'
),
},
};
</
script
>
<
template
>
<div>
<div
v-gl-tooltip
.
left
.
viewport
:title=
"contactsLabel"
class=
"sidebar-collapsed-icon"
>
<gl-icon
name=
"users"
/>
<span>
{{
contactCount
}}
</span>
</div>
<div
v-gl-tooltip.left.viewport=
"$options.i18n.help"
class=
"hide-collapsed help-button float-right"
>
<a
href=
"https://gitlab.com/gitlab-org/gitlab/-/issues/2256"
><gl-icon
name=
"question-o"
/></a>
</div>
<div
class=
"title hide-collapsed gl-mb-2 gl-line-height-20"
>
{{
contactsLabel
}}
</div>
<div
class=
"hide-collapsed gl-display-flex gl-flex-wrap"
>
<div
v-for=
"(contact, index) in contacts"
:id=
"`contact_container_$
{index}`"
:key="index"
class="gl-pr-2"
>
<span
:id=
"`contact_$
{index}`" class="gl-font-weight-bold"
>
{{
contact
.
firstName
}}
{{
contact
.
lastName
}}{{
divider
(
index
)
}}
</span
>
<gl-popover
v-if=
"shouldShowPopover(contact)"
:target=
"`contact_$
{index}`"
:container="`contact_container_${index}`"
triggers="hover focus"
placement="top"
>
<div
v-for=
"row in popOverData(contact)"
:key=
"row"
>
{{
row
}}
</div>
</gl-popover>
</div>
</div>
</div>
</
template
>
app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql
0 → 100644
View file @
db0b9254
#import "./issue_crm_contacts.fragment.graphql"
query
issueCrmContacts
(
$id
:
IssueID
!)
{
issue
(
id
:
$id
)
{
...
CrmContacts
}
}
app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql
0 → 100644
View file @
db0b9254
fragment
CrmContacts
on
Issue
{
id
customerRelationsContacts
{
nodes
{
id
firstName
lastName
email
phone
description
organization
{
id
name
}
}
}
}
app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql
0 → 100644
View file @
db0b9254
#import "./issue_crm_contacts.fragment.graphql"
subscription
issueCrmContactsUpdated
(
$id
:
IssuableID
!)
{
issueCrmContactsUpdated
(
issuableId
:
$id
)
{
...
on
Issue
{
...
CrmContacts
}
}
}
app/assets/javascripts/sidebar/mount_sidebar.js
View file @
db0b9254
...
...
@@ -34,6 +34,7 @@ import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subsc
import
SidebarTimeTracking
from
'
./components/time_tracking/sidebar_time_tracking.vue
'
;
import
{
IssuableAttributeType
}
from
'
./constants
'
;
import
SidebarMoveIssue
from
'
./lib/sidebar_move_issue
'
;
import
CrmContacts
from
'
./components/crm_contacts/crm_contacts.vue
'
;
Vue
.
use
(
Translate
);
Vue
.
use
(
VueApollo
);
...
...
@@ -205,6 +206,28 @@ function mountReviewersComponent(mediator) {
}
}
function
mountCrmContactsComponent
()
{
const
el
=
document
.
getElementById
(
'
js-issue-crm-contacts
'
);
if
(
!
el
)
return
;
const
{
issueId
}
=
el
.
dataset
;
// eslint-disable-next-line no-new
new
Vue
({
el
,
apolloProvider
,
components
:
{
CrmContacts
,
},
render
:
(
createElement
)
=>
createElement
(
'
crm-contacts
'
,
{
props
:
{
issueId
,
},
}),
});
}
function
mountMilestoneSelect
()
{
const
el
=
document
.
querySelector
(
'
.js-milestone-select
'
);
...
...
@@ -535,6 +558,7 @@ export function mountSidebar(mediator, store) {
mountAssigneesComponentDeprecated
(
mediator
);
}
mountReviewersComponent
(
mediator
);
mountCrmContactsComponent
();
mountSidebarLabels
();
mountMilestoneSelect
();
mountConfidentialComponent
(
mediator
);
...
...
app/controllers/concerns/issuable_actions.rb
View file @
db0b9254
...
...
@@ -17,6 +17,7 @@ module IssuableActions
def
show
respond_to
do
|
format
|
format
.
html
do
@show_crm_contacts
=
issuable
.
is_a?
(
Issue
)
&&
can?
(
current_user
,
:read_crm_contact
,
issuable
.
project
.
group
)
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@issuable_sidebar
=
serializer
.
represent
(
issuable
,
serializer:
'sidebar'
)
# rubocop:disable Gitlab/ModuleWithInstanceVariables
render
'show'
end
...
...
app/graphql/graphql_triggers.rb
View file @
db0b9254
...
...
@@ -4,4 +4,8 @@ module GraphqlTriggers
def
self
.
issuable_assignees_updated
(
issuable
)
GitlabSchema
.
subscriptions
.
trigger
(
'issuableAssigneesUpdated'
,
{
issuable_id:
issuable
.
to_gid
},
issuable
)
end
def
self
.
issue_crm_contacts_updated
(
issue
)
GitlabSchema
.
subscriptions
.
trigger
(
'issueCrmContactsUpdated'
,
{
issuable_id:
issue
.
to_gid
},
issue
)
end
end
app/graphql/types/subscription_type.rb
View file @
db0b9254
...
...
@@ -6,5 +6,8 @@ module Types
field
:issuable_assignees_updated
,
subscription:
Subscriptions
::
IssuableUpdated
,
null:
true
,
description:
'Triggered when the assignees of an issuable are updated.'
field
:issue_crm_contacts_updated
,
subscription:
Subscriptions
::
IssuableUpdated
,
null:
true
,
description:
'Triggered when the crm contacts of an issuable are updated.'
end
end
app/services/issues/set_crm_contacts_service.rb
View file @
db0b9254
...
...
@@ -23,6 +23,7 @@ module Issues
remove_by_email
if
params
[
:remove_emails
].
present?
if
issue
.
valid?
GraphqlTriggers
.
issue_crm_contacts_updated
(
issue
)
ServiceResponse
.
success
(
payload:
issue
)
else
# The default error isn't very helpful: "Issue customer relations contacts is invalid"
...
...
app/views/shared/issuable/_sidebar.html.haml
View file @
db0b9254
...
...
@@ -36,6 +36,10 @@
.block
{
class:
'gl-pt-0! gl-collapse-empty'
,
data:
{
qa_selector:
'iteration_container'
,
testid:
'iteration_container'
}
}
<
=
render_if_exists
'shared/issuable/iteration_select'
,
can_edit:
can_edit_issuable
.
to_s
,
group_path:
@project
.
group
.
full_path
,
project_path:
issuable_sidebar
[
:project_full_path
],
issue_iid:
issuable_sidebar
[
:iid
],
issuable_type:
issuable_type
-
if
@show_crm_contacts
.block.contact
#js-issue-crm-contacts
{
data:
{
issue_id:
issuable_sidebar
[
:id
]
}
}
-
if
issuable_sidebar
[
:supports_time_tracking
]
#issuable-time-tracker
.block
// Fallback while content is loading
...
...
locale/gitlab.pot
View file @
db0b9254
...
...
@@ -495,6 +495,11 @@ msgstr[1] ""
msgid "%{count} approvals from %{name}"
msgstr ""
msgid "%{count} contact"
msgid_plural "%{count} contacts"
msgstr[0] ""
msgstr[1] ""
msgid "%{count} files touched"
msgstr ""
...
...
@@ -32379,6 +32384,9 @@ msgstr ""
msgid "Something went wrong trying to change the locked state of this %{issuableDisplayName}"
msgstr ""
msgid "Something went wrong trying to load issue contacts."
msgstr ""
msgid "Something went wrong when creating a work item. Please try again"
msgstr ""
...
...
@@ -39335,6 +39343,9 @@ msgstr ""
msgid "Work in progress Limit"
msgstr ""
msgid "Work in progress- click here to find out more"
msgstr ""
msgid "WorkItem|Work Items"
msgstr ""
...
...
spec/features/action_cable_logging_spec.rb
View file @
db0b9254
...
...
@@ -25,7 +25,7 @@ RSpec.describe 'ActionCable logging', :js do
username:
user
.
username
)
expect
(
ActiveSupport
::
Notifications
).
to
receive
(
:instrument
).
with
(
'subscribe.action_cable'
,
subscription_data
)
expect
(
ActiveSupport
::
Notifications
).
to
receive
(
:instrument
).
with
(
'subscribe.action_cable'
,
subscription_data
)
.
at_least
(
:once
)
gitlab_sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
...
...
spec/frontend/sidebar/components/crm_contacts_spec.js
0 → 100644
View file @
db0b9254
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
CrmContacts
from
'
~/sidebar/components/crm_contacts/crm_contacts.vue
'
;
import
getIssueCrmContactsQuery
from
'
~/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql
'
;
import
issueCrmContactsSubscription
from
'
~/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql
'
;
import
{
getIssueCrmContactsQueryResponse
,
issueCrmContactsUpdateResponse
,
issueCrmContactsUpdateNullResponse
,
}
from
'
./mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
describe
(
'
Issue crm contacts component
'
,
()
=>
{
Vue
.
use
(
VueApollo
);
let
wrapper
;
let
fakeApollo
;
const
successQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
getIssueCrmContactsQueryResponse
);
const
successSubscriptionHandler
=
jest
.
fn
().
mockResolvedValue
(
issueCrmContactsUpdateResponse
);
const
nullSubscriptionHandler
=
jest
.
fn
().
mockResolvedValue
(
issueCrmContactsUpdateNullResponse
);
const
mountComponent
=
({
queryHandler
=
successQueryHandler
,
subscriptionHandler
=
successSubscriptionHandler
,
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([
[
getIssueCrmContactsQuery
,
queryHandler
],
[
issueCrmContactsSubscription
,
subscriptionHandler
],
]);
wrapper
=
shallowMountExtended
(
CrmContacts
,
{
propsData
:
{
issueId
:
'
123
'
},
apolloProvider
:
fakeApollo
,
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
fakeApollo
=
null
;
});
it
(
'
should render error message on reject
'
,
async
()
=>
{
mountComponent
({
queryHandler
:
jest
.
fn
().
mockRejectedValue
(
'
ERROR
'
)
});
await
waitForPromises
();
expect
(
createFlash
).
toHaveBeenCalled
();
});
it
(
'
calls the query with correct variables
'
,
()
=>
{
mountComponent
();
expect
(
successQueryHandler
).
toHaveBeenCalledWith
({
id
:
'
gid://gitlab/Issue/123
'
,
});
});
it
(
'
calls the subscription with correct variable for issue
'
,
()
=>
{
mountComponent
();
expect
(
successSubscriptionHandler
).
toHaveBeenCalledWith
({
id
:
'
gid://gitlab/Issue/123
'
,
});
});
it
(
'
renders correct initial results
'
,
async
()
=>
{
mountComponent
({
subscriptionHandler
:
nullSubscriptionHandler
});
await
waitForPromises
();
expect
(
wrapper
.
find
(
'
#contact_0
'
).
text
()).
toContain
(
'
Someone Important
'
);
expect
(
wrapper
.
find
(
'
#contact_container_0
'
).
text
()).
toContain
(
'
si@gitlab.com
'
);
expect
(
wrapper
.
find
(
'
#contact_1
'
).
text
()).
toContain
(
'
Marty McFly
'
);
});
it
(
'
renders correct results after subscription update
'
,
async
()
=>
{
mountComponent
();
await
waitForPromises
();
const
contact
=
[
'
Dave Davies
'
,
'
dd@gitlab.com
'
,
'
+44 20 1111 2222
'
,
'
Vice President
'
];
contact
.
forEach
((
property
)
=>
{
expect
(
wrapper
.
find
(
'
#contact_container_0
'
).
text
()).
toContain
(
property
);
});
});
});
spec/frontend/sidebar/components/mock_data.js
0 → 100644
View file @
db0b9254
export
const
getIssueCrmContactsQueryResponse
=
{
data
:
{
issue
:
{
id
:
'
gid://gitlab/Issue/123
'
,
customerRelationsContacts
:
{
nodes
:
[
{
id
:
'
gid://gitlab/CustomerRelations::Contact/1
'
,
firstName
:
'
Someone
'
,
lastName
:
'
Important
'
,
email
:
'
si@gitlab.com
'
,
phone
:
null
,
description
:
null
,
organization
:
null
,
},
{
id
:
'
gid://gitlab/CustomerRelations::Contact/5
'
,
firstName
:
'
Marty
'
,
lastName
:
'
McFly
'
,
email
:
null
,
phone
:
null
,
description
:
null
,
organization
:
null
,
},
],
},
},
},
};
export
const
issueCrmContactsUpdateNullResponse
=
{
data
:
{
issueCrmContactsUpdated
:
null
,
},
};
export
const
issueCrmContactsUpdateResponse
=
{
data
:
{
issueCrmContactsUpdated
:
{
id
:
'
gid://gitlab/Issue/123
'
,
customerRelationsContacts
:
{
nodes
:
[
{
id
:
'
gid://gitlab/CustomerRelations::Contact/13
'
,
firstName
:
'
Dave
'
,
lastName
:
'
Davies
'
,
email
:
'
dd@gitlab.com
'
,
phone
:
'
+44 20 1111 2222
'
,
description
:
'
Vice President
'
,
organization
:
null
,
},
],
},
},
},
};
spec/graphql/types/subscription_type_spec.rb
View file @
db0b9254
...
...
@@ -6,6 +6,7 @@ RSpec.describe GitlabSchema.types['Subscription'] do
it
'has the expected fields'
do
expected_fields
=
%i[
issuable_assignees_updated
issue_crm_contacts_updated
]
expect
(
described_class
).
to
have_graphql_fields
(
*
expected_fields
).
only
...
...
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