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
acdd883d
Commit
acdd883d
authored
Apr 13, 2021
by
Natalia Tepluhina
Committed by
Vitaly Slobodin
Apr 13, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[RUN-AS-IF-FOSS] Resolve "Add assignees widget to issue sidebar"
parent
42954bd5
Changes
43
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
43 changed files
with
970 additions
and
429 deletions
+970
-429
app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
...aphql_shared/fragments/user_availability.fragment.graphql
+5
-0
app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
...scripts/graphql_shared/queries/users_search.query.graphql
+3
-1
app/assets/javascripts/invite_member/components/invite_member_modal.vue
...ascripts/invite_member/components/invite_member_modal.vue
+3
-1
app/assets/javascripts/invite_member/components/invite_member_trigger.vue
...cripts/invite_member/components/invite_member_trigger.vue
+7
-1
app/assets/javascripts/invite_member/init_invite_member_modal.js
...ets/javascripts/invite_member/init_invite_member_modal.js
+6
-3
app/assets/javascripts/invite_member/init_invite_member_trigger.js
...s/javascripts/invite_member/init_invite_member_trigger.js
+4
-2
app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
...ipts/issuable_sidebar/queries/issue_sidebar.query.graphql
+0
-16
app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
...pts/sidebar/components/assignees/assignee_avatar_link.vue
+2
-2
app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
...ripts/sidebar/components/assignees/assignees_realtime.vue
+17
-8
app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
...ripts/sidebar/components/assignees/issuable_assignees.vue
+25
-12
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
...cripts/sidebar/components/assignees/sidebar_assignees.vue
+1
-0
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
...sidebar/components/assignees/sidebar_assignees_widget.vue
+152
-127
app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
...s/sidebar/components/assignees/sidebar_invite_members.vue
+51
-0
app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
...ipts/sidebar/components/assignees/sidebar_participant.vue
+39
-0
app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
...idebar/components/assignees/uncollapsed_assignee_list.vue
+5
-1
app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
.../javascripts/sidebar/components/sidebar_editable_item.vue
+28
-10
app/assets/javascripts/sidebar/mount_sidebar.js
app/assets/javascripts/sidebar/mount_sidebar.js
+53
-11
app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
...ts/vue_shared/components/sidebar/multiselect_dropdown.vue
+3
-0
app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
...ents/sidebar/queries/get_issue_participants.query.graphql
+3
-0
app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
...ponents/sidebar/queries/get_mr_participants.query.graphql
+3
-0
app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
...s/sidebar/queries/update_issue_assignees.mutation.graphql
+3
-0
app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
...ents/sidebar/queries/update_mr_assignees.mutation.graphql
+3
-0
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+2
-1
app/views/shared/issuable/_sidebar_assignees.html.haml
app/views/shared/issuable/_sidebar_assignees.html.haml
+2
-2
changelogs/unreleased/322109-add-assignees-widget-to-issue-and-merge-request-sidebar.yml
...d-assignees-widget-to-issue-and-merge-request-sidebar.yml
+5
-0
ee/spec/features/boards/sidebar_spec.rb
ee/spec/features/boards/sidebar_spec.rb
+4
-4
ee/spec/features/issues/issue_sidebar_spec.rb
ee/spec/features/issues/issue_sidebar_spec.rb
+22
-0
ee/spec/frontend/multiple_assignees/sidebar_spec.js
ee/spec/frontend/multiple_assignees/sidebar_spec.js
+0
-9
ee/spec/frontend/sidebar/mock_data.js
ee/spec/frontend/sidebar/mock_data.js
+1
-132
spec/features/boards/sidebar_assignee_spec.rb
spec/features/boards/sidebar_assignee_spec.rb
+3
-3
spec/features/issues/issue_sidebar_spec.rb
spec/features/issues/issue_sidebar_spec.rb
+74
-29
spec/features/issues/user_edits_issue_spec.rb
spec/features/issues/user_edits_issue_spec.rb
+17
-18
spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
...ures/issues/user_sees_sidebar_updates_in_realtime_spec.rb
+4
-1
spec/features/profiles/user_edit_profile_spec.rb
spec/features/profiles/user_edit_profile_spec.rb
+5
-3
spec/frontend/invite_member/components/invite_member_modal_spec.js
...tend/invite_member/components/invite_member_modal_spec.js
+1
-1
spec/frontend/invite_member/components/invite_member_trigger_spec.js
...nd/invite_member/components/invite_member_trigger_spec.js
+1
-1
spec/frontend/sidebar/assignees_realtime_spec.js
spec/frontend/sidebar/assignees_realtime_spec.js
+8
-7
spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
...bar/components/assignees/sidebar_assignees_widget_spec.js
+116
-15
spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
...idebar/components/assignees/sidebar_editable_item_spec.js
+32
-1
spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
...debar/components/assignees/sidebar_invite_members_spec.js
+59
-0
spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
.../sidebar/components/assignees/sidebar_participant_spec.js
+43
-0
spec/frontend/sidebar/issuable_assignees_spec.js
spec/frontend/sidebar/issuable_assignees_spec.js
+12
-7
spec/frontend/sidebar/mock_data.js
spec/frontend/sidebar/mock_data.js
+143
-0
No files found.
app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
0 → 100644
View file @
acdd883d
fragment
UserAvailability
on
User
{
status
{
availability
}
}
app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
View file @
acdd883d
#import "../fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query
usersSearch
(
$search
:
String
!,
$fullPath
:
ID
!)
{
workspace
:
project
(
fullPath
:
$fullPath
)
{
users
:
projectMembers
(
search
:
$search
)
{
users
:
projectMembers
(
search
:
$search
,
relations
:
[
DIRECT
,
INHERITED
,
INVITED_GROUPS
]
)
{
nodes
{
user
{
...
User
...
UserAvailability
}
}
}
...
...
app/assets/javascripts/invite_member/components/invite_member_modal.vue
View file @
acdd883d
...
...
@@ -19,8 +19,10 @@ export default {
GlLink
,
GlModal
,
},
inject
:
{
props
:
{
membersPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
...
...
app/assets/javascripts/invite_member/components/invite_member_trigger.vue
View file @
acdd883d
...
...
@@ -7,14 +7,20 @@ export default {
components
:
{
GlLink
,
},
inject
:
{
props
:
{
displayText
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
event
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
label
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
...
...
app/assets/javascripts/invite_member/init_invite_member_modal.js
View file @
acdd883d
import
{
GlToast
}
from
'
@gitlab/ui
'
;
import
Vue
from
'
vue
'
;
import
{
isInIssuePage
,
isInDesignPage
}
from
'
~/lib/utils/common_utils
'
;
import
InviteMemberModal
from
'
./components/invite_member_modal.vue
'
;
Vue
.
use
(
GlToast
);
...
...
@@ -7,7 +8,7 @@ Vue.use(GlToast);
export
default
function
initInviteMembersModal
()
{
const
el
=
document
.
querySelector
(
'
.js-invite-member-modal
'
);
if
(
!
el
)
{
if
(
!
el
||
isInDesignPage
()
||
isInIssuePage
()
)
{
return
false
;
}
...
...
@@ -15,7 +16,9 @@ export default function initInviteMembersModal() {
return
new
Vue
({
el
,
provide
:
{
membersPath
},
render
:
(
createElement
)
=>
createElement
(
InviteMemberModal
),
render
:
(
createElement
)
=>
createElement
(
InviteMemberModal
,
{
props
:
{
membersPath
},
}),
});
}
app/assets/javascripts/invite_member/init_invite_member_trigger.js
View file @
acdd883d
...
...
@@ -10,7 +10,9 @@ export default function initInviteMembersTrigger() {
return
new
Vue
({
el
,
provide
:
{
...
el
.
dataset
},
render
:
(
createElement
)
=>
createElement
(
InviteMemberTrigger
),
render
:
(
createElement
)
=>
createElement
(
InviteMemberTrigger
,
{
props
:
{
...
el
.
dataset
},
}),
});
}
app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
deleted
100644 → 0
View file @
42954bd5
#import "~/graphql_shared/fragments/author.fragment.graphql"
query
getProjectIssue
(
$iid
:
String
!,
$fullPath
:
ID
!)
{
project
(
fullPath
:
$fullPath
)
{
issue
(
iid
:
$iid
)
{
id
assignees
{
nodes
{
...
Author
id
state
}
}
}
}
}
app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
View file @
acdd883d
...
...
@@ -103,10 +103,10 @@ export default {
v-gl-tooltip=
"tooltipOption"
:href=
"assigneeUrl"
:title=
"tooltipTitle"
class=
"
d
-inline-block"
class=
"
gl-display
-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span
class=
"
d
-flex"
>
<span
class=
"
gl-display
-flex"
>
<assignee-avatar
:user=
"user"
:img-size=
"32"
:issuable-type=
"issuableType"
/>
<slot></slot>
</span>
...
...
app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
View file @
acdd883d
<
script
>
import
actionCable
from
'
~/actioncable_consumer
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
query
from
'
~/issuable_sidebar/queries/issue_sidebar.query.graphql
'
;
import
{
assigneesQueries
}
from
'
~/sidebar/constants
'
;
export
default
{
subscription
:
null
,
...
...
@@ -9,7 +9,8 @@ export default {
props
:
{
mediator
:
{
type
:
Object
,
required
:
true
,
required
:
false
,
default
:
null
,
},
issuableIid
:
{
type
:
String
,
...
...
@@ -19,10 +20,16 @@ export default {
type
:
String
,
required
:
true
,
},
issuableType
:
{
type
:
String
,
required
:
true
,
},
},
apollo
:
{
project
:
{
query
,
workspace
:
{
query
()
{
return
assigneesQueries
[
this
.
issuableType
].
query
;
},
variables
()
{
return
{
iid
:
this
.
issuableIid
,
...
...
@@ -30,7 +37,9 @@ export default {
};
},
result
(
data
)
{
this
.
handleFetchResult
(
data
);
if
(
this
.
mediator
)
{
this
.
handleFetchResult
(
data
);
}
},
},
},
...
...
@@ -43,7 +52,7 @@ export default {
methods
:
{
received
(
data
)
{
if
(
data
.
event
===
'
updated
'
)
{
this
.
$apollo
.
queries
.
project
.
refetch
();
this
.
$apollo
.
queries
.
workspace
.
refetch
();
}
},
initActionCablePolling
()
{
...
...
@@ -57,7 +66,7 @@ export default {
);
},
handleFetchResult
({
data
})
{
const
{
nodes
}
=
data
.
project
.
issu
e
.
assignees
;
const
{
nodes
}
=
data
.
workspace
.
issuabl
e
.
assignees
;
const
assignees
=
nodes
.
map
((
n
)
=>
({
...
n
,
...
...
@@ -69,7 +78,7 @@ export default {
},
},
render
()
{
return
this
.
$slots
.
default
;
return
null
;
},
};
</
script
>
app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
View file @
acdd883d
...
...
@@ -18,6 +18,11 @@ export default {
required
:
false
,
default
:
'
issue
'
,
},
signedIn
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
computed
:
{
assigneesText
()
{
...
...
@@ -34,20 +39,28 @@ export default {
<div
class=
"gl-display-flex gl-flex-direction-column issuable-assignees"
>
<div
v-if=
"emptyUsers"
class=
"gl-display-flex gl-align-items-center gl-text-gray-500"
class=
"gl-display-flex gl-align-items-center gl-text-gray-500
gl-mt-2 hide-collapsed
"
data-testid=
"none"
>
<span>
{{
__
(
'
None
'
)
}}
-
</span>
<gl-button
data-testid=
"assign-yourself"
category=
"tertiary"
variant=
"link"
class=
"gl-ml-2"
@
click=
"$emit('assign-self')"
>
<span
class=
"gl-text-gray-500 gl-hover-text-blue-800"
>
{{
__
(
'
assign yourself
'
)
}}
</span>
</gl-button>
<span>
{{
__
(
'
None
'
)
}}
</span>
<template
v-if=
"signedIn"
>
<span
class=
"gl-ml-2"
>
-
</span>
<gl-button
data-testid=
"assign-yourself"
category=
"tertiary"
variant=
"link"
class=
"gl-ml-2"
@
click=
"$emit('assign-self')"
>
<span
class=
"gl-text-gray-500 gl-hover-text-blue-800"
>
{{
__
(
'
assign yourself
'
)
}}
</span>
</gl-button>
</
template
>
</div>
<uncollapsed-assignee-list
v-else
:users=
"users"
:issuable-type=
"issuableType"
/>
<uncollapsed-assignee-list
v-else
:users=
"users"
:issuable-type=
"issuableType"
class=
"gl-mt-2 hide-collapsed"
/>
</div>
</template>
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
View file @
acdd883d
...
...
@@ -123,6 +123,7 @@ export default {
v-if=
"shouldEnableRealtime"
:issuable-iid=
"issuableIid"
:project-path=
"projectPath"
:issuable-type=
"issuableType"
:mediator=
"mediator"
/>
<assignee-title
...
...
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
View file @
acdd883d
This diff is collapsed.
Click to expand it.
app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
0 → 100644
View file @
acdd883d
<
script
>
import
InviteMemberModal
from
'
~/invite_member/components/invite_member_modal.vue
'
;
import
InviteMemberTrigger
from
'
~/invite_member/components/invite_member_trigger.vue
'
;
import
InviteMembersTrigger
from
'
~/invite_members/components/invite_members_trigger.vue
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
displayText
:
__
(
'
Invite members
'
),
dataTrackLabel
:
'
edit_assignee
'
,
components
:
{
InviteMemberTrigger
,
InviteMemberModal
,
InviteMembersTrigger
,
},
inject
:
{
projectMembersPath
:
{
default
:
''
,
},
directlyInviteMembers
:
{
default
:
false
,
},
},
computed
:
{
trackEvent
()
{
return
this
.
directlyInviteMembers
?
'
click_invite_members
'
:
'
click_invite_members_version_b
'
;
},
},
};
</
script
>
<
template
>
<div>
<invite-members-trigger
v-if=
"directlyInviteMembers"
trigger-element=
"anchor"
:display-text=
"$options.displayText"
:event=
"trackEvent"
:label=
"$options.dataTrackLabel"
classes=
"gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
/>
<template
v-else
>
<invite-member-trigger
:display-text=
"$options.displayText"
:event=
"trackEvent"
:label=
"$options.dataTrackLabel"
class=
"gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
/>
<invite-member-modal
:members-path=
"projectMembersPath"
/>
</
template
>
</div>
</template>
app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
0 → 100644
View file @
acdd883d
<
script
>
import
{
GlAvatarLabeled
,
GlAvatarLink
}
from
'
@gitlab/ui
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
export
default
{
components
:
{
GlAvatarLabeled
,
GlAvatarLink
,
},
props
:
{
user
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
userLabel
()
{
if
(
!
this
.
user
.
status
)
{
return
this
.
user
.
name
;
}
return
sprintf
(
s__
(
'
UserAvailability|%{author} (Busy)
'
),
{
author
:
this
.
user
.
name
,
});
},
},
};
</
script
>
<
template
>
<gl-avatar-link>
<gl-avatar-labeled
:size=
"32"
:label=
"userLabel"
:sub-label=
"user.username"
:src=
"user.avatarUrl || user.avatar || user.avatar_url"
class=
"gl-align-items-center"
/>
</gl-avatar-link>
</
template
>
app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
View file @
acdd883d
<
script
>
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
AssigneeAvatarLink
from
'
./assignee_avatar_link.vue
'
;
import
UserNameWithStatus
from
'
./user_name_with_status.vue
'
;
...
...
@@ -58,7 +59,10 @@ export default {
this
.
showLess
=
!
this
.
showLess
;
},
userAvailability
(
u
)
{
return
u
?.
availability
||
''
;
if
(
this
.
issuableType
===
IssuableType
.
MergeRequest
)
{
return
u
?.
availability
||
''
;
}
return
u
?.
status
?.
availability
||
''
;
},
},
};
...
...
app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
View file @
acdd883d
<
script
>
import
{
GlButton
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
components
:
{
GlButton
,
GlLoadingIcon
},
...
...
@@ -20,6 +21,16 @@ export default {
required
:
false
,
default
:
false
,
},
initialLoading
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isDirty
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
tracking
:
{
type
:
Object
,
required
:
false
,
...
...
@@ -35,6 +46,11 @@ export default {
edit
:
false
,
};
},
computed
:
{
editButtonText
()
{
return
this
.
isDirty
?
__
(
'
Apply
'
)
:
__
(
'
Edit
'
);
},
},
destroyed
()
{
window
.
removeEventListener
(
'
click
'
,
this
.
collapseWhenOffClick
);
window
.
removeEventListener
(
'
keyup
'
,
this
.
collapseOnEscape
);
...
...
@@ -86,15 +102,15 @@ export default {
<
template
>
<div>
<div
class=
"gl-display-flex gl-align-items-center"
@
click.self=
"collapse"
>
<span
class=
"hide-collapsed"
data-testid=
"title"
>
{{
title
}}
</span>
<gl-loading-icon
v-if=
"loading"
inline
class=
"gl-ml-2 hide-collapsed"
/>
<span
class=
"hide-collapsed"
data-testid=
"title"
@
click=
"collapse"
>
{{
title
}}
</span>
<gl-loading-icon
v-if=
"loading
|| initialLoading
"
inline
class=
"gl-ml-2 hide-collapsed"
/>
<gl-loading-icon
v-if=
"loading && isClassicSidebar"
inline
class=
"gl-mx-auto gl-my-0 hide-expanded"
/>
<gl-button
v-if=
"canUpdate"
v-if=
"canUpdate
&& !initialLoading
"
variant=
"link"
class=
"gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
data-testid=
"edit-button"
...
...
@@ -105,14 +121,16 @@ export default {
@
keyup.esc=
"toggle"
@
click=
"toggle"
>
{{
__
(
'
Edit
'
)
}}
{{
editButtonText
}}
</gl-button>
</div>
<div
v-show=
"!edit"
data-testid=
"collapsed-content"
>
<slot
name=
"collapsed"
>
{{
__
(
'
None
'
)
}}
</slot>
</div>
<div
v-show=
"edit"
data-testid=
"expanded-content"
:class=
"
{ 'gl-mt-3': !isClassicSidebar }">
<slot
:edit=
"edit"
></slot>
</div>
<template
v-if=
"!initialLoading"
>
<div
v-show=
"!edit"
data-testid=
"collapsed-content"
>
<slot
name=
"collapsed"
>
{{
__
(
'
None
'
)
}}
</slot>
</div>
<div
v-show=
"edit"
data-testid=
"expanded-content"
:class=
"
{ 'gl-mt-3': !isClassicSidebar }">
<slot
:edit=
"edit"
></slot>
</div>
</
template
>
</div>
</template>
app/assets/javascripts/sidebar/mount_sidebar.js
View file @
acdd883d
...
...
@@ -10,6 +10,8 @@ import {
parseBoolean
,
}
from
'
~/lib/utils/common_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
CollapsedAssigneeList
from
'
~/sidebar/components/assignees/collapsed_assignee_list.vue
'
;
import
SidebarAssigneesWidget
from
'
~/sidebar/components/assignees/sidebar_assignees_widget.vue
'
;
import
SidebarConfidentialityWidget
from
'
~/sidebar/components/confidential/sidebar_confidentiality_widget.vue
'
;
import
SidebarReferenceWidget
from
'
~/sidebar/components/reference/sidebar_reference_widget.vue
'
;
import
{
apolloProvider
}
from
'
~/sidebar/graphql
'
;
...
...
@@ -32,15 +34,6 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
return
JSON
.
parse
(
sidebarOptEl
.
innerHTML
);
}
/**
* Extracts the list of assignees with availability information from a hidden input
* field and converts to a key:value pair for use in the sidebar assignees component.
* The assignee username is used as the key and their busy status is the value
*
* e.g { root: 'busy', admin: '' }
*
* @returns {Object}
*/
function
getSidebarAssigneeAvailabilityData
()
{
const
sidebarAssigneeEl
=
document
.
querySelectorAll
(
'
.js-sidebar-assignee-data input
'
);
return
Array
.
from
(
sidebarAssigneeEl
)
...
...
@@ -54,7 +47,7 @@ function getSidebarAssigneeAvailabilityData() {
);
}
function
mountAssigneesComponent
(
mediator
)
{
function
mountAssigneesComponent
Deprecated
(
mediator
)
{
const
el
=
document
.
getElementById
(
'
js-vue-sidebar-assignees
'
);
if
(
!
el
)
return
;
...
...
@@ -86,6 +79,51 @@ function mountAssigneesComponent(mediator) {
});
}
function
mountAssigneesComponent
()
{
const
el
=
document
.
getElementById
(
'
js-vue-sidebar-assignees
'
);
if
(
!
el
)
return
;
const
{
iid
,
fullPath
,
editable
,
projectMembersPath
}
=
getSidebarOptions
();
// eslint-disable-next-line no-new
new
Vue
({
el
,
apolloProvider
,
components
:
{
SidebarAssigneesWidget
,
},
provide
:
{
canUpdate
:
editable
,
projectMembersPath
,
directlyInviteMembers
:
el
.
hasAttribute
(
'
data-directly-invite-members
'
),
indirectlyInviteMembers
:
el
.
hasAttribute
(
'
data-indirectly-invite-members
'
),
},
render
:
(
createElement
)
=>
createElement
(
'
sidebar-assignees-widget
'
,
{
props
:
{
iid
:
String
(
iid
),
fullPath
,
issuableType
:
isInIssuePage
()
||
isInIncidentPage
()
||
isInDesignPage
()
?
IssuableType
.
Issue
:
IssuableType
.
MergeRequest
,
multipleAssignees
:
!
el
.
dataset
.
maxAssignees
,
},
scopedSlots
:
{
collapsed
:
({
users
,
onClick
})
=>
createElement
(
CollapsedAssigneeList
,
{
props
:
{
users
,
},
nativeOn
:
{
click
:
onClick
,
},
}),
},
}),
});
}
function
mountReviewersComponent
(
mediator
)
{
const
el
=
document
.
getElementById
(
'
js-vue-sidebar-reviewers
'
);
...
...
@@ -342,7 +380,11 @@ function mountCopyEmailComponent() {
}
export
function
mountSidebar
(
mediator
)
{
mountAssigneesComponent
(
mediator
);
if
(
isInIssuePage
()
||
isInDesignPage
())
{
mountAssigneesComponent
();
}
else
{
mountAssigneesComponentDeprecated
(
mediator
);
}
mountReviewersComponent
(
mediator
);
mountConfidentialComponent
(
mediator
);
mountReferenceComponent
(
mediator
);
...
...
app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
View file @
acdd883d
...
...
@@ -30,5 +30,8 @@ export default {
<gl-dropdown-form>
<slot
name=
"items"
></slot>
</gl-dropdown-form>
<
template
#footer
>
<slot
name=
"footer"
></slot>
</
template
>
</gl-dropdown>
</template>
app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
View file @
acdd883d
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query
issueParticipants
(
$fullPath
:
ID
!,
$iid
:
String
!)
{
workspace
:
project
(
fullPath
:
$fullPath
)
{
...
...
@@ -9,11 +10,13 @@ query issueParticipants($fullPath: ID!, $iid: String!) {
participants
{
nodes
{
...
User
...
UserAvailability
}
}
assignees
{
nodes
{
...
User
...
UserAvailability
}
}
}
...
...
app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
View file @
acdd883d
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query
getMrParticipants
(
$fullPath
:
ID
!,
$iid
:
String
!)
{
workspace
:
project
(
fullPath
:
$fullPath
)
{
...
...
@@ -7,11 +8,13 @@ query getMrParticipants($fullPath: ID!, $iid: String!) {
participants
{
nodes
{
...
User
...
UserAvailability
}
}
assignees
{
nodes
{
...
User
...
UserAvailability
}
}
}
...
...
app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
View file @
acdd883d
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
mutation
issueSetAssignees
(
$iid
:
String
!,
$assigneeUsernames
:
[
String
!]!,
$fullPath
:
ID
!)
{
issuableSetAssignees
:
issueSetAssignees
(
...
...
@@ -9,11 +10,13 @@ mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullP
assignees
{
nodes
{
...
User
...
UserAvailability
}
}
participants
{
nodes
{
...
User
...
UserAvailability
}
}
}
...
...
app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
View file @
acdd883d
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
mutation
mergeRequestSetAssignees
(
$iid
:
String
!,
$assigneeUsernames
:
[
String
!]!,
$fullPath
:
ID
!)
{
mergeRequestSetAssignees
(
...
...
@@ -9,11 +10,13 @@ mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!,
assignees
{
nodes
{
...
User
...
UserAvailability
}
}
participants
{
nodes
{
...
User
...
UserAvailability
}
}
}
...
...
app/helpers/issuables_helper.rb
View file @
acdd883d
...
...
@@ -389,7 +389,8 @@ module IssuablesHelper
severity:
issuable
[
:severity
],
timeTrackingLimitToHours:
Gitlab
::
CurrentSettings
.
time_tracking_limit_to_hours
,
createNoteEmail:
issuable
[
:create_note_email
],
issuableType:
issuable
[
:type
]
issuableType:
issuable
[
:type
],
projectMembersPath:
project_project_members_path
(
@project
,
sort: :access_level_desc
)
}
end
...
...
app/views/shared/issuable/_sidebar_assignees.html.haml
View file @
acdd883d
-
issuable_type
=
issuable_sidebar
[
:type
]
-
dropdown_options
=
assignees_dropdown_options
(
issuable_type
)
#js-vue-sidebar-assignees
{
data:
{
field:
issuable_type
,
signed_in:
signed_in
}
}
#js-vue-sidebar-assignees
{
data:
{
field:
issuable_type
,
signed_in:
signed_in
,
max_assignees:
dropdown_options
[
:data
][
:"max-select"
],
directly_invite_members:
directly_invite_members?
,
indirectly_invite_members:
indirectly_invite_members?
}
}
.title.hide-collapsed
=
_
(
'Assignee'
)
=
loading_icon
(
css_class:
'gl-vertical-align-text-bottom'
)
...
...
@@ -29,7 +30,6 @@
null_user:
true
,
display:
'static'
}
}
-
dropdown_options
=
assignees_dropdown_options
(
issuable_type
)
-
title
=
dropdown_options
[
:title
]
-
options
[
:toggle_class
]
+=
' js-multiselect js-save-user-data'
-
data
=
{
field_name:
"
#{
issuable_type
}
[assignee_ids][]"
}
...
...
changelogs/unreleased/322109-add-assignees-widget-to-issue-and-merge-request-sidebar.yml
0 → 100644
View file @
acdd883d
---
title
:
Assignee dropdown in issue page displays only participants by default
merge_request
:
56742
author
:
type
:
changed
ee/spec/features/boards/sidebar_spec.rb
View file @
acdd883d
...
...
@@ -48,7 +48,7 @@ RSpec.describe 'Issue Boards', :js do
first
(
'.gl-avatar-labeled'
).
click
end
click_button
(
'
Edit
'
)
click_button
(
'
Apply
'
)
wait_for_requests
expect
(
page
).
to
have_content
(
assignee
)
...
...
@@ -73,7 +73,7 @@ RSpec.describe 'Issue Boards', :js do
all
(
'.gl-avatar-labeled'
)[
1
].
click
end
click_button
(
'
Edit
'
)
click_button
(
'
Apply
'
)
wait_for_requests
expect
(
page
).
to
have_link
(
nil
,
title:
user
.
name
)
...
...
@@ -94,7 +94,7 @@ RSpec.describe 'Issue Boards', :js do
find
(
'[data-testid="unassign"]'
).
click
end
click_button
(
'
Edit
'
)
click_button
(
'
Apply
'
)
wait_for_requests
expect
(
page
).
to
have_content
(
'None'
)
...
...
@@ -134,7 +134,7 @@ RSpec.describe 'Issue Boards', :js do
first
(
'.gl-avatar-labeled'
).
click
end
click_button
(
'
Edit
'
)
click_button
(
'
Apply
'
)
wait_for_requests
expect
(
page
).
to
have_content
(
assignee
)
...
...
ee/spec/features/issues/issue_sidebar_spec.rb
View file @
acdd883d
...
...
@@ -17,6 +17,21 @@ RSpec.describe 'Issue Sidebar' do
sign_in
(
user
)
end
context
'Assignees'
,
:js
do
let
(
:user2
)
{
create
(
:user
)
}
let
(
:issue2
)
{
create
(
:issue
,
project:
project
,
author:
user2
)
}
it
'shows label text as "Apply" when assignees are changed'
do
project
.
add_developer
(
user
)
visit_issue
(
project
,
issue2
)
open_assignees_dropdown
click_on
'Unassigned'
expect
(
page
).
to
have_content
(
'Apply'
)
end
end
context
'updating weight'
,
:js
do
before
do
project
.
add_maintainer
(
user
)
...
...
@@ -210,4 +225,11 @@ RSpec.describe 'Issue Sidebar' do
find
(
'aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle'
).
click
find
(
'aside.right-sidebar.right-sidebar-expanded'
)
end
def
open_assignees_dropdown
page
.
within
(
'.assignee'
)
do
click_button
(
'Edit'
)
wait_for_requests
end
end
end
ee/spec/frontend/multiple_assignees/sidebar_spec.js
deleted
100644 → 0
View file @
42954bd5
describe
(
'
Sidebar
'
,
()
=>
{
beforeEach
(()
=>
loadFixtures
(
'
issues/open-issue.html
'
));
it
(
'
does not have a max select
'
,
()
=>
{
const
dropdown
=
document
.
querySelector
(
'
.js-author-search
'
);
expect
(
dropdown
.
dataset
.
maxSelect
).
toBeUndefined
();
});
});
ee/spec/frontend/sidebar/mock_data.js
View file @
acdd883d
...
...
@@ -63,7 +63,7 @@ export const mockMutationResponse = {
issuableSetIteration
:
{
errors
:
[],
issuable
:
{
id
:
mockIssueId
,
id
:
'
gid://gitlab/Issue/1
'
,
iteration
:
{
id
:
'
gid://gitlab/Iteration/2
'
,
title
:
'
Awesome Iteration
'
,
...
...
@@ -76,134 +76,3 @@ export const mockMutationResponse = {
},
},
};
export
const
issuableQueryResponse
=
{
data
:
{
workspace
:
{
__typename
:
'
Project
'
,
issuable
:
{
__typename
:
'
Issue
'
,
id
:
'
gid://gitlab/Issue/1
'
,
iid
:
'
1
'
,
participants
:
{
nodes
:
[
{
id
:
'
gid://gitlab/User/1
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
name
:
'
Administrator
'
,
username
:
'
root
'
,
webUrl
:
'
/root
'
,
},
{
id
:
'
gid://gitlab/User/2
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80
\
u0026d=identicon
'
,
name
:
'
Jacki Kub
'
,
username
:
'
francina.skiles
'
,
webUrl
:
'
/franc
'
,
},
{
id
:
'
gid://gitlab/User/3
'
,
avatarUrl
:
'
/avatar
'
,
name
:
'
John Doe
'
,
username
:
'
johndoe
'
,
webUrl
:
'
/john
'
,
},
],
},
assignees
:
{
nodes
:
[
{
id
:
'
gid://gitlab/User/2
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80
\
u0026d=identicon
'
,
name
:
'
Jacki Kub
'
,
username
:
'
francina.skiles
'
,
webUrl
:
'
/franc
'
,
},
],
},
},
},
},
};
export
const
searchQueryResponse
=
{
data
:
{
workspace
:
{
__typename
:
'
Project
'
,
users
:
{
nodes
:
[
{
user
:
{
id
:
'
1
'
,
avatarUrl
:
'
/avatar
'
,
name
:
'
root
'
,
username
:
'
root
'
,
webUrl
:
'
root
'
,
},
},
{
user
:
{
id
:
'
2
'
,
avatarUrl
:
'
/avatar2
'
,
name
:
'
rookie
'
,
username
:
'
rookie
'
,
webUrl
:
'
rookie
'
,
},
},
],
},
},
},
};
export
const
updateIssueAssigneesMutationResponse
=
{
data
:
{
issuableSetAssignees
:
{
issuable
:
{
id
:
'
gid://gitlab/Issue/1
'
,
iid
:
'
1
'
,
assignees
:
{
nodes
:
[
{
__typename
:
'
User
'
,
id
:
'
gid://gitlab/User/1
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
name
:
'
Administrator
'
,
username
:
'
root
'
,
webUrl
:
'
/root
'
,
},
],
__typename
:
'
UserConnection
'
,
},
participants
:
{
nodes
:
[
{
__typename
:
'
User
'
,
id
:
'
gid://gitlab/User/1
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
name
:
'
Administrator
'
,
username
:
'
root
'
,
webUrl
:
'
/root
'
,
},
{
__typename
:
'
User
'
,
id
:
'
gid://gitlab/User/2
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80
\
u0026d=identicon
'
,
name
:
'
Jacki Kub
'
,
username
:
'
francina.skiles
'
,
webUrl
:
'
/franc
'
,
},
],
__typename
:
'
UserConnection
'
,
},
__typename
:
'
Issue
'
,
},
},
},
};
spec/features/boards/sidebar_assignee_spec.rb
View file @
acdd883d
...
...
@@ -41,7 +41,7 @@ RSpec.describe 'Project issue boards sidebar assignee', :js do
first
(
'.gl-avatar-labeled'
).
click
end
click_button
(
'
Edit
'
)
click_button
(
'
Apply
'
)
wait_for_requests
expect
(
page
).
to
have_content
(
assignee
)
...
...
@@ -63,7 +63,7 @@ RSpec.describe 'Project issue boards sidebar assignee', :js do
find
(
'[data-testid="unassign"]'
).
click
end
click_button
(
'
Edit
'
)
click_button
(
'
Apply
'
)
wait_for_requests
expect
(
page
).
to
have_content
(
'None'
)
...
...
@@ -102,7 +102,7 @@ RSpec.describe 'Project issue boards sidebar assignee', :js do
first
(
'.gl-avatar-labeled'
).
click
end
click_button
(
'
Edit
'
)
click_button
(
'
Apply
'
)
wait_for_requests
expect
(
page
).
to
have_content
(
assignee
)
...
...
spec/features/issues/issue_sidebar_spec.rb
View file @
acdd883d
...
...
@@ -30,29 +30,80 @@ RSpec.describe 'Issue Sidebar' do
let
(
:user2
)
{
create
(
:user
)
}
let
(
:issue2
)
{
create
(
:issue
,
project:
project
,
author:
user2
)
}
include_examples
'issuable invite members experiments'
do
let
(
:issuable_path
)
{
project_issue_path
(
project
,
issue2
)
}
context
'when a privileged user can invite'
do
it
'shows a link for inviting members and launches invite modal'
do
project
.
add_maintainer
(
user
)
visit_issue
(
project
,
issue2
)
open_assignees_dropdown
page
.
within
'.dropdown-menu-user'
do
expect
(
page
).
to
have_link
(
'Invite members'
)
expect
(
page
).
to
have_selector
(
'[data-track-event="click_invite_members"]'
)
expect
(
page
).
to
have_selector
(
'[data-track-label="edit_assignee"]'
)
end
click_link
'Invite members'
expect
(
page
).
to
have_content
(
"You're inviting members to the"
)
end
end
context
'when
user is a developer
'
do
context
'when
invite_members_version_b experiment is enabled
'
do
before
do
stub_experiment_for_subject
(
invite_members_version_b:
true
)
end
it
'shows a link for inviting members and follows through to modal'
do
project
.
add_developer
(
user
)
visit_issue
(
project
,
issue2
)
find
(
'.block.assignee .edit-link'
).
click
open_assignees_dropdown
wait_for_requests
page
.
within
'.dropdown-menu-user'
do
expect
(
page
).
to
have_link
(
'Invite members'
,
href:
'#'
)
expect
(
page
).
to
have_selector
(
'[data-track-event="click_invite_members_version_b"]'
)
expect
(
page
).
to
have_selector
(
'[data-track-label="edit_assignee"]'
)
end
click_link
'Invite members'
expect
(
page
).
to
have_content
(
"Oops, this feature isn't ready yet"
)
end
end
context
'when invite_members_version_b experiment is disabled'
do
it
'shows author in assignee dropdown and no invite link'
do
project
.
add_developer
(
user
)
visit_issue
(
project
,
issue2
)
open_assignees_dropdown
page
.
within
'.dropdown-menu-user'
do
expect
(
page
).
not_to
have_link
(
'Invite members'
)
end
end
end
context
'when user is a developer'
do
before
do
project
.
add_developer
(
user
)
visit_issue
(
project
,
issue2
)
end
it
'shows author in assignee dropdown'
do
open_assignees_dropdown
page
.
within
'.dropdown-menu-user'
do
expect
(
page
).
to
have_content
(
user2
.
name
)
end
end
it
'shows author when filtering assignee dropdown'
do
open_assignees_dropdown
page
.
within
'.dropdown-menu-user'
do
find
(
'.
dropdown-input-field
'
).
set
(
user2
.
name
)
find
(
'.
js-dropdown-input-field'
).
find
(
'input
'
).
set
(
user2
.
name
)
wait_for_requests
...
...
@@ -61,23 +112,18 @@ RSpec.describe 'Issue Sidebar' do
end
it
'assigns yourself'
do
find
(
'.block.assignee .dropdown-menu-toggle'
).
click
click_button
'assign yourself'
wait_for_requests
find
(
'.block.assignee .edit-link'
).
click
page
.
within
'.dropdown-menu-user'
do
expect
(
page
.
find
(
'.dropdown-header'
)).
to
be_visible
expect
(
page
.
find
(
'.dropdown-menu-user-link.is-active'
)).
to
have_content
(
user
.
name
)
page
.
within
'.assignee'
do
expect
(
page
).
to
have_content
(
user
.
name
)
end
end
it
'keeps your filtered term after filtering and dismissing the dropdown'
do
find
(
'.dropdown-input-field'
).
set
(
user2
.
name
)
open_assignees_dropdown
find
(
'.js-dropdown-input-field'
).
find
(
'input'
).
set
(
user2
.
name
)
wait_for_requests
page
.
within
'.dropdown-menu-user'
do
...
...
@@ -86,23 +132,15 @@ RSpec.describe 'Issue Sidebar' do
end
find
(
'.js-right-sidebar'
).
click
find
(
'.block.assignee .edit-link'
).
click
expect
(
page
.
all
(
'.dropdown-menu-user li'
).
length
).
to
eq
(
1
)
expect
(
find
(
'.dropdown-input-field'
).
value
).
to
eq
(
user2
.
name
)
end
end
it
'shows label text as "Apply" when assignees are changed'
do
project
.
add_developer
(
user
)
visit_issue
(
project
,
issue2
)
find
(
'.block.assignee .edit-link'
).
click
wait_for_requests
open_assignees_dropdown
click_on
'Unassigned'
page
.
within
(
'.assignee'
)
do
expect
(
page
.
all
(
'[data-testid="selected-participant"]'
).
length
).
to
eq
(
1
)
end
expect
(
page
).
to
have_link
(
'Apply'
)
expect
(
find
(
'.js-dropdown-input-field'
).
find
(
'input'
).
value
).
to
eq
(
user2
.
name
)
end
end
end
...
...
@@ -334,4 +372,11 @@ RSpec.describe 'Issue Sidebar' do
find
(
'aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle'
).
click
find
(
'aside.right-sidebar.right-sidebar-expanded'
)
end
def
open_assignees_dropdown
page
.
within
(
'.assignee'
)
do
click_button
(
'Edit'
)
wait_for_requests
end
end
end
spec/features/issues/user_edits_issue_spec.rb
View file @
acdd883d
...
...
@@ -168,21 +168,19 @@ RSpec.describe "Issues > User edits issue", :js do
describe
'update assignee'
do
context
'by authorized user'
do
def
close_dropdown_menu_if_visible
find
(
'.dropdown-menu-toggle'
,
visible: :all
).
tap
do
|
toggle
|
toggle
.
click
if
toggle
.
visible?
end
end
it
'allows user to select unassigned'
do
visit
project_issue_path
(
project
,
issue
)
page
.
within
(
'.assignee'
)
do
expect
(
page
).
to
have_content
"
#{
user
.
name
}
"
click_link
'Edit'
click_link
'Unassigned'
first
(
'.title'
).
click
click_button
(
'Edit'
)
wait_for_requests
find
(
'[data-testid="unassign"]'
).
click
find
(
'[data-testid="title"]'
).
click
wait_for_requests
expect
(
page
).
to
have_content
'None - assign yourself'
end
end
...
...
@@ -193,10 +191,8 @@ RSpec.describe "Issues > User edits issue", :js do
page
.
within
(
'.assignee'
)
do
expect
(
page
).
to
have_content
"None"
end
page
.
within
'.assignee'
do
click_link
'Edit'
click_button
(
'Edit'
)
wait_for_requests
end
page
.
within
'.dropdown-menu-user'
do
...
...
@@ -204,6 +200,9 @@ RSpec.describe "Issues > User edits issue", :js do
end
page
.
within
(
'.assignee'
)
do
find
(
'[data-testid="title"]'
).
click
wait_for_requests
expect
(
page
).
to
have_content
user
.
name
end
end
...
...
@@ -216,14 +215,14 @@ RSpec.describe "Issues > User edits issue", :js do
page
.
within
'.assignee'
do
expect
(
page
).
to
have_content
user
.
name
click_link
'Edit'
click_button
(
'Edit'
)
wait_for_requests
click_link
user
.
name
close_dropdown_menu_if_visible
find
(
'[data-testid="title"]'
).
click
wait_for_requests
page
.
within
'.value .assign-yourself'
do
expect
(
page
).
to
have_content
"None"
end
expect
(
page
).
to
have_content
"None"
end
end
end
...
...
spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
View file @
acdd883d
...
...
@@ -19,11 +19,14 @@ RSpec.describe 'Issues > Real-time sidebar', :js do
expect
(
page
.
find
(
'.assignee'
)).
to
have_content
'None'
end
gitlab_sign_in
(
user
)
sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
expect
(
page
.
find
(
'.assignee'
)).
to
have_content
'None'
click_button
'assign yourself'
wait_for_requests
expect
(
page
.
find
(
'.assignee'
)).
to
have_content
user
.
name
using_session
:other_session
do
expect
(
page
.
find
(
'.assignee'
)).
to
have_content
user
.
name
...
...
spec/features/profiles/user_edit_profile_spec.rb
View file @
acdd883d
...
...
@@ -212,8 +212,10 @@ RSpec.describe 'User edit profile' do
end
it
'shows author as busy in the assignee dropdown'
do
find
(
'.block.assignee .edit-link'
).
click
wait_for_requests
page
.
within
(
'.assignee'
)
do
click_button
(
'Edit'
)
wait_for_requests
end
page
.
within
'.dropdown-menu-user'
do
expect
(
page
).
to
have_content
(
"
#{
user
.
name
}
(Busy)"
)
...
...
@@ -227,7 +229,7 @@ RSpec.describe 'User edit profile' do
visit
project_issue_path
(
project
,
issue
)
wait_for_requests
expect
(
page
.
find
(
'
[data-testid="expanded-assignee"]'
)).
to
have_tex
t
(
"
#{
user
.
name
}
(Busy)"
)
expect
(
page
.
find
(
'
.issuable-assignees'
)).
to
have_conten
t
(
"
#{
user
.
name
}
(Busy)"
)
end
end
...
...
spec/frontend/invite_member/components/invite_member_modal_spec.js
View file @
acdd883d
...
...
@@ -9,7 +9,7 @@ const memberPath = 'member_path';
const
GlEmoji
=
{
template
:
'
<img />
'
};
const
createComponent
=
()
=>
{
return
shallowMount
(
InviteMemberModal
,
{
pro
vide
:
{
pro
psData
:
{
membersPath
:
memberPath
,
},
stubs
:
{
...
...
spec/frontend/invite_member/components/invite_member_trigger_spec.js
View file @
acdd883d
...
...
@@ -5,7 +5,7 @@ import InviteMemberTrigger from '~/invite_member/components/invite_member_trigge
import
triggerProvides
from
'
./invite_member_trigger_mock_data
'
;
const
createComponent
=
()
=>
{
return
shallowMount
(
InviteMemberTrigger
,
{
pro
vide
:
triggerProvides
});
return
shallowMount
(
InviteMemberTrigger
,
{
pro
psData
:
triggerProvides
});
};
describe
(
'
InviteMemberTrigger
'
,
()
=>
{
...
...
spec/frontend/sidebar/assignees_realtime_spec.js
View file @
acdd883d
import
ActionCable
from
'
@rails/actioncable
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
query
from
'
~/issuable_sidebar/queries/issue_sidebar.query.graphql
'
;
import
AssigneesRealtime
from
'
~/sidebar/components/assignees/assignees_realtime.vue
'
;
import
{
assigneesQueries
}
from
'
~/sidebar/constants
'
;
import
SidebarMediator
from
'
~/sidebar/sidebar_mediator
'
;
import
Mock
from
'
./mock_data
'
;
...
...
@@ -18,18 +18,19 @@ describe('Assignees Realtime', () => {
let
wrapper
;
let
mediator
;
const
createComponent
=
()
=>
{
const
createComponent
=
(
issuableType
=
'
issue
'
)
=>
{
wrapper
=
shallowMount
(
AssigneesRealtime
,
{
propsData
:
{
issuableIid
:
'
1
'
,
mediator
,
projectPath
:
'
path/to/project
'
,
issuableType
,
},
mocks
:
{
$apollo
:
{
query
,
query
:
assigneesQueries
[
issuableType
].
query
,
queries
:
{
project
:
{
workspace
:
{
refetch
:
jest
.
fn
(),
},
},
...
...
@@ -51,8 +52,8 @@ describe('Assignees Realtime', () => {
describe
(
'
when handleFetchResult is called from smart query
'
,
()
=>
{
it
(
'
sets assignees to the store
'
,
()
=>
{
const
data
=
{
project
:
{
issue
:
{
workspace
:
{
issu
abl
e
:
{
assignees
:
{
nodes
:
[{
id
:
'
gid://gitlab/Environments/123
'
,
avatarUrl
:
'
url
'
}],
},
...
...
@@ -95,7 +96,7 @@ describe('Assignees Realtime', () => {
wrapper
.
vm
.
received
({
event
:
'
updated
'
});
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
$apollo
.
queries
.
project
.
refetch
).
toHaveBeenCalledTimes
(
1
);
expect
(
wrapper
.
vm
.
$apollo
.
queries
.
workspace
.
refetch
).
toHaveBeenCalledTimes
(
1
);
});
});
});
...
...
ee/
spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
→
spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
View file @
acdd883d
...
...
@@ -7,8 +7,11 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
searchUsersQuery
from
'
~/graphql_shared/queries/users_search.query.graphql
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
SidebarAssigneesRealtime
from
'
~/sidebar/components/assignees/assignees_realtime.vue
'
;
import
IssuableAssignees
from
'
~/sidebar/components/assignees/issuable_assignees.vue
'
;
import
SidebarAssigneesWidget
from
'
~/sidebar/components/assignees/sidebar_assignees_widget.vue
'
;
import
SidebarInviteMembers
from
'
~/sidebar/components/assignees/sidebar_invite_members.vue
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
{
ASSIGNEES_DEBOUNCE_DELAY
}
from
'
~/sidebar/constants
'
;
import
MultiSelectDropdown
from
'
~/vue_shared/components/sidebar/multiselect_dropdown.vue
'
;
...
...
@@ -40,21 +43,23 @@ const initialAssignees = [
},
];
describe
(
'
BoardCardAssigneeDropdown
'
,
()
=>
{
describe
(
'
Sidebar assignees widget
'
,
()
=>
{
let
wrapper
;
let
fakeApollo
;
const
findAssignees
=
()
=>
wrapper
.
findComponent
(
IssuableAssignees
);
const
findRealtimeAssignees
=
()
=>
wrapper
.
findComponent
(
SidebarAssigneesRealtime
);
const
findEditableItem
=
()
=>
wrapper
.
findComponent
(
SidebarEditableItem
);
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
MultiSelectDropdown
);
const
findAssigneesLoading
=
()
=>
wrapper
.
find
(
'
[data-testid="loading-assignees"]
'
);
const
findInviteMembersLink
=
()
=>
wrapper
.
findComponent
(
SidebarInviteMembers
);
const
findSearchField
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
const
findParticipantsLoading
=
()
=>
wrapper
.
find
(
'
[data-testid="loading-participants"]
'
);
const
findSelectedParticipants
=
()
=>
wrapper
.
findAll
(
'
[data-testid="selected-participant"]
'
);
const
findUnselectedParticipants
=
()
=>
wrapper
.
findAll
(
'
[data-testid="unselected-participant"]
'
);
const
findCurrentUser
=
()
=>
wrapper
.
findAll
(
'
[data-testid="current-user"]
'
);
const
findUnassignLink
=
()
=>
wrapper
.
find
(
'
[data-testid="unassign"]
'
);
const
findSearchField
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
const
findEmptySearchResults
=
()
=>
wrapper
.
find
(
'
[data-testid="empty-results"]
'
);
const
expandDropdown
=
()
=>
wrapper
.
vm
.
$refs
.
toggle
.
expand
();
...
...
@@ -65,6 +70,7 @@ describe('BoardCardAssigneeDropdown', () => {
searchQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
searchQueryResponse
),
updateIssueAssigneesMutationHandler
=
updateIssueAssigneesMutationSuccess
,
props
=
{},
provide
=
{},
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([
[
getIssueParticipantsQuery
,
issuableQueryHandler
],
...
...
@@ -88,6 +94,7 @@ describe('BoardCardAssigneeDropdown', () => {
provide
:
{
canUpdate
:
true
,
rootPath
:
'
/
'
,
...
provide
,
},
stubs
:
{
SidebarEditableItem
,
...
...
@@ -99,28 +106,27 @@ describe('BoardCardAssigneeDropdown', () => {
};
beforeEach
(()
=>
{
window
.
gon
=
window
.
gon
||
{};
window
.
gon
.
current_username
=
'
root
'
;
window
.
gon
.
current_user_fullname
=
'
Administrator
'
;
window
.
gon
.
current_user_avatar_url
=
'
/root
'
;
gon
.
current_username
=
'
root
'
;
gon
.
current_user_fullname
=
'
Administrator
'
;
gon
.
current_user_avatar_url
=
'
/root
'
;
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
fakeApollo
=
null
;
delete
window
.
gon
.
current_username
;
delete
gon
.
current_username
;
});
describe
(
'
with passed initial assignees
'
,
()
=>
{
it
(
'
does not show loading state when query is loading
'
,
()
=>
{
it
(
'
passes `initialLoading` as false to editable item
'
,
()
=>
{
createComponent
({
props
:
{
initialAssignees
,
},
});
expect
(
find
AssigneesLoading
().
exists
(
)).
toBe
(
false
);
expect
(
find
EditableItem
().
props
(
'
initialLoading
'
)).
toBe
(
false
);
});
it
(
'
renders an initial assignees list with initialAssignees prop
'
,
()
=>
{
...
...
@@ -158,10 +164,10 @@ describe('BoardCardAssigneeDropdown', () => {
});
describe
(
'
without passed initial assignees
'
,
()
=>
{
it
(
'
shows loading state when query is loading
'
,
()
=>
{
it
(
'
passes `initialLoading` as true to editable item
'
,
()
=>
{
createComponent
();
expect
(
find
AssigneesLoading
().
exists
(
)).
toBe
(
true
);
expect
(
find
EditableItem
().
props
(
'
initialLoading
'
)).
toBe
(
true
);
});
it
(
'
renders assignees list from API response when resolved
'
,
async
()
=>
{
...
...
@@ -232,6 +238,7 @@ describe('BoardCardAssigneeDropdown', () => {
name
:
'
Administrator
'
,
username
:
'
root
'
,
webUrl
:
'
/root
'
,
status
:
null
,
},
],
],
...
...
@@ -239,9 +246,9 @@ describe('BoardCardAssigneeDropdown', () => {
});
it
(
'
renders current user if they are not in participants or assignees
'
,
async
()
=>
{
window
.
gon
.
current_username
=
'
random
'
;
window
.
gon
.
current_user_fullname
=
'
Mr Random
'
;
window
.
gon
.
current_user_avatar_url
=
'
/random
'
;
gon
.
current_username
=
'
random
'
;
gon
.
current_user_fullname
=
'
Mr Random
'
;
gon
.
current_user_avatar_url
=
'
/random
'
;
createComponent
();
await
waitForPromises
();
...
...
@@ -393,6 +400,7 @@ describe('BoardCardAssigneeDropdown', () => {
name
:
'
Roodie
'
,
username
:
'
roodie
'
,
webUrl
:
'
/roodie
'
,
status
:
null
,
});
const
issuableQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
responseCopy
);
...
...
@@ -454,4 +462,97 @@ describe('BoardCardAssigneeDropdown', () => {
});
});
});
describe
(
'
when user is not signed in
'
,
()
=>
{
beforeEach
(()
=>
{
gon
.
current_username
=
undefined
;
createComponent
();
});
it
(
'
does not show current user in the dropdown
'
,
()
=>
{
expandDropdown
();
expect
(
findCurrentUser
().
exists
()).
toBe
(
false
);
});
it
(
'
passes signedIn prop as false to IssuableAssignees
'
,
()
=>
{
expect
(
findAssignees
().
props
(
'
signedIn
'
)).
toBe
(
false
);
});
});
it
(
'
when realtime feature flag is disabled
'
,
async
()
=>
{
createComponent
();
await
waitForPromises
();
expect
(
findRealtimeAssignees
().
exists
()).
toBe
(
false
);
});
it
(
'
when realtime feature flag is enabled
'
,
async
()
=>
{
createComponent
({
provide
:
{
glFeatures
:
{
realTimeIssueSidebar
:
true
,
},
},
});
await
waitForPromises
();
expect
(
findRealtimeAssignees
().
exists
()).
toBe
(
true
);
});
describe
(
'
when making changes to participants list
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
();
});
it
(
'
passes falsy `isDirty` prop to editable item if no changes to selected users were made
'
,
()
=>
{
expandDropdown
();
expect
(
findEditableItem
().
props
(
'
isDirty
'
)).
toBe
(
false
);
});
it
(
'
passes truthy `isDirty` prop if selected users list was changed
'
,
async
()
=>
{
expandDropdown
();
expect
(
findEditableItem
().
props
(
'
isDirty
'
)).
toBe
(
false
);
findUnselectedParticipants
().
at
(
0
).
vm
.
$emit
(
'
click
'
);
await
nextTick
();
expect
(
findEditableItem
().
props
(
'
isDirty
'
)).
toBe
(
true
);
});
it
(
'
passes falsy `isDirty` prop after dropdown is closed
'
,
async
()
=>
{
expandDropdown
();
findUnselectedParticipants
().
at
(
0
).
vm
.
$emit
(
'
click
'
);
findEditableItem
().
vm
.
$emit
(
'
close
'
);
await
waitForPromises
();
expect
(
findEditableItem
().
props
(
'
isDirty
'
)).
toBe
(
false
);
});
});
it
(
'
does not render invite members link on non-issue sidebar
'
,
async
()
=>
{
createComponent
({
props
:
{
issuableType
:
IssuableType
.
MergeRequest
}
});
await
waitForPromises
();
expect
(
findInviteMembersLink
().
exists
()).
toBe
(
false
);
});
it
(
'
does not render invite members link if `directlyInviteMembers` and `indirectlyInviteMembers` were not passed
'
,
async
()
=>
{
createComponent
();
await
waitForPromises
();
expect
(
findInviteMembersLink
().
exists
()).
toBe
(
false
);
});
it
(
'
renders invite members link if `directlyInviteMembers` is true
'
,
async
()
=>
{
createComponent
({
provide
:
{
directlyInviteMembers
:
true
,
},
});
await
waitForPromises
();
expect
(
findInviteMembersLink
().
exists
()).
toBe
(
true
);
});
it
(
'
renders invite members link if `indirectlyInviteMembers` is true
'
,
async
()
=>
{
createComponent
({
provide
:
{
indirectlyInviteMembers
:
true
,
},
});
await
waitForPromises
();
expect
(
findInviteMembersLink
().
exists
()).
toBe
(
true
);
});
});
spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
View file @
acdd883d
...
...
@@ -5,7 +5,7 @@ import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'
describe
(
'
boards sidebar remove issue
'
,
()
=>
{
let
wrapper
;
const
findLoader
=
()
=>
wrapper
.
find
(
GlLoadingIcon
);
const
findLoader
=
()
=>
wrapper
.
find
Component
(
GlLoadingIcon
);
const
findEditButton
=
()
=>
wrapper
.
find
(
'
[data-testid="edit-button"]
'
);
const
findTitle
=
()
=>
wrapper
.
find
(
'
[data-testid="title"]
'
);
const
findCollapsed
=
()
=>
wrapper
.
find
(
'
[data-testid="collapsed-content"]
'
);
...
...
@@ -117,4 +117,35 @@ describe('boards sidebar remove issue', () => {
expect
(
wrapper
.
emitted
().
close
).
toBeUndefined
();
});
it
(
'
renders `Edit` test when passed `isDirty` prop is false
'
,
()
=>
{
createComponent
({
props
:
{
isDirty
:
false
},
canUpdate
:
true
});
expect
(
findEditButton
().
text
()).
toBe
(
'
Edit
'
);
});
it
(
'
renders `Apply` test when passed `isDirty` prop is true
'
,
()
=>
{
createComponent
({
props
:
{
isDirty
:
true
},
canUpdate
:
true
});
expect
(
findEditButton
().
text
()).
toBe
(
'
Apply
'
);
});
describe
(
'
when initial loading is true
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
props
:
{
initialLoading
:
true
}
});
});
it
(
'
renders loading icon
'
,
()
=>
{
expect
(
findLoader
().
exists
()).
toBe
(
true
);
});
it
(
'
does not render edit button
'
,
()
=>
{
expect
(
findEditButton
().
exists
()).
toBe
(
false
);
});
it
(
'
does not render collapsed and expanded content
'
,
()
=>
{
expect
(
findCollapsed
().
exists
()).
toBe
(
false
);
expect
(
findExpanded
().
exists
()).
toBe
(
false
);
});
});
});
spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
0 → 100644
View file @
acdd883d
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
InviteMemberModal
from
'
~/invite_member/components/invite_member_modal.vue
'
;
import
InviteMemberTrigger
from
'
~/invite_member/components/invite_member_trigger.vue
'
;
import
InviteMembersTrigger
from
'
~/invite_members/components/invite_members_trigger.vue
'
;
import
SidebarInviteMembers
from
'
~/sidebar/components/assignees/sidebar_invite_members.vue
'
;
const
testProjectMembersPath
=
'
test-path
'
;
describe
(
'
Sidebar invite members component
'
,
()
=>
{
let
wrapper
;
const
findDirectInviteLink
=
()
=>
wrapper
.
findComponent
(
InviteMembersTrigger
);
const
findIndirectInviteLink
=
()
=>
wrapper
.
findComponent
(
InviteMemberTrigger
);
const
findInviteModal
=
()
=>
wrapper
.
findComponent
(
InviteMemberModal
);
const
createComponent
=
({
directlyInviteMembers
=
false
}
=
{})
=>
{
wrapper
=
shallowMount
(
SidebarInviteMembers
,
{
provide
:
{
directlyInviteMembers
,
projectMembersPath
:
testProjectMembersPath
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
when directly inviting members
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
directlyInviteMembers
:
true
});
});
it
(
'
renders a direct link to project members path
'
,
()
=>
{
expect
(
findDirectInviteLink
().
exists
()).
toBe
(
true
);
});
it
(
'
does not render invite members trigger and modal components
'
,
()
=>
{
expect
(
findIndirectInviteLink
().
exists
()).
toBe
(
false
);
expect
(
findInviteModal
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
when indirectly inviting members
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
does not render a direct link to project members path
'
,
()
=>
{
expect
(
findDirectInviteLink
().
exists
()).
toBe
(
false
);
});
it
(
'
does not render invite members trigger and modal components
'
,
()
=>
{
expect
(
findIndirectInviteLink
().
exists
()).
toBe
(
true
);
expect
(
findInviteModal
().
exists
()).
toBe
(
true
);
expect
(
findInviteModal
().
props
(
'
membersPath
'
)).
toBe
(
testProjectMembersPath
);
});
});
});
spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
0 → 100644
View file @
acdd883d
import
{
GlAvatarLabeled
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
SidebarParticipant
from
'
~/sidebar/components/assignees/sidebar_participant.vue
'
;
const
user
=
{
name
:
'
John Doe
'
,
username
:
'
johndoe
'
,
webUrl
:
'
/link
'
,
avatarUrl
:
'
/avatar
'
,
};
describe
(
'
Sidebar participant component
'
,
()
=>
{
let
wrapper
;
const
findAvatar
=
()
=>
wrapper
.
findComponent
(
GlAvatarLabeled
);
const
createComponent
=
(
status
=
null
)
=>
{
wrapper
=
shallowMount
(
SidebarParticipant
,
{
propsData
:
{
user
:
{
...
user
,
status
,
},
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
when user is not busy
'
,
()
=>
{
createComponent
();
expect
(
findAvatar
().
props
(
'
label
'
)).
toBe
(
user
.
name
);
});
it
(
'
when user is busy
'
,
()
=>
{
createComponent
({
availability
:
'
BUSY
'
});
expect
(
findAvatar
().
props
(
'
label
'
)).
toBe
(
`
${
user
.
name
}
(Busy)`
);
});
});
spec/frontend/sidebar/issuable_assignees_spec.js
View file @
acdd883d
...
...
@@ -5,12 +5,15 @@ import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_
describe
(
'
IssuableAssignees
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
(
props
=
{
users
:
[]
})
=>
{
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
IssuableAssignees
,
{
provide
:
{
rootPath
:
''
,
},
propsData
:
{
...
props
},
propsData
:
{
users
:
[],
...
props
,
},
});
};
const
findUncollapsedAssigneeList
=
()
=>
wrapper
.
find
(
UncollapsedAssigneeList
);
...
...
@@ -22,12 +25,14 @@ describe('IssuableAssignees', () => {
});
describe
(
'
when no assignees are present
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
it
(
'
renders "None - assign yourself" when user is logged in
'
,
()
=>
{
createComponent
({
signedIn
:
true
});
expect
(
findEmptyAssignee
().
text
()).
toBe
(
'
None - assign yourself
'
);
});
it
(
'
renders "None - assign yourself"
'
,
()
=>
{
expect
(
findEmptyAssignee
().
text
()).
toBe
(
'
None - assign yourself
'
);
it
(
'
renders "None" when user is not logged in
'
,
()
=>
{
createComponent
();
expect
(
findEmptyAssignee
().
text
()).
toBe
(
'
None
'
);
});
});
...
...
@@ -41,7 +46,7 @@ describe('IssuableAssignees', () => {
describe
(
'
when clicking "assign yourself"
'
,
()
=>
{
it
(
'
emits "assign-self"
'
,
()
=>
{
createComponent
();
createComponent
(
{
signedIn
:
true
}
);
wrapper
.
find
(
'
[data-testid="assign-yourself"]
'
).
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
assign-self
'
)).
toHaveLength
(
1
);
});
...
...
spec/frontend/sidebar/mock_data.js
View file @
acdd883d
...
...
@@ -245,4 +245,147 @@ export const issueReferenceResponse = (reference) => ({
},
},
});
export
const
issuableQueryResponse
=
{
data
:
{
workspace
:
{
__typename
:
'
Project
'
,
issuable
:
{
__typename
:
'
Issue
'
,
id
:
'
gid://gitlab/Issue/1
'
,
iid
:
'
1
'
,
participants
:
{
nodes
:
[
{
id
:
'
gid://gitlab/User/1
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
name
:
'
Administrator
'
,
username
:
'
root
'
,
webUrl
:
'
/root
'
,
status
:
null
,
},
{
id
:
'
gid://gitlab/User/2
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80
\
u0026d=identicon
'
,
name
:
'
Jacki Kub
'
,
username
:
'
francina.skiles
'
,
webUrl
:
'
/franc
'
,
status
:
{
availability
:
'
BUSY
'
,
},
},
{
id
:
'
gid://gitlab/User/3
'
,
avatarUrl
:
'
/avatar
'
,
name
:
'
John Doe
'
,
username
:
'
johndoe
'
,
webUrl
:
'
/john
'
,
status
:
null
,
},
],
},
assignees
:
{
nodes
:
[
{
id
:
'
gid://gitlab/User/2
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80
\
u0026d=identicon
'
,
name
:
'
Jacki Kub
'
,
username
:
'
francina.skiles
'
,
webUrl
:
'
/franc
'
,
status
:
null
,
},
],
},
},
},
},
};
export
const
searchQueryResponse
=
{
data
:
{
workspace
:
{
__typename
:
'
Project
'
,
users
:
{
nodes
:
[
{
user
:
{
id
:
'
1
'
,
avatarUrl
:
'
/avatar
'
,
name
:
'
root
'
,
username
:
'
root
'
,
webUrl
:
'
root
'
,
status
:
null
,
},
},
{
user
:
{
id
:
'
2
'
,
avatarUrl
:
'
/avatar2
'
,
name
:
'
rookie
'
,
username
:
'
rookie
'
,
webUrl
:
'
rookie
'
,
status
:
null
,
},
},
],
},
},
},
};
export
const
updateIssueAssigneesMutationResponse
=
{
data
:
{
issuableSetAssignees
:
{
issuable
:
{
id
:
'
gid://gitlab/Issue/1
'
,
iid
:
'
1
'
,
assignees
:
{
nodes
:
[
{
__typename
:
'
User
'
,
id
:
'
gid://gitlab/User/1
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
name
:
'
Administrator
'
,
username
:
'
root
'
,
webUrl
:
'
/root
'
,
status
:
null
,
},
],
__typename
:
'
UserConnection
'
,
},
participants
:
{
nodes
:
[
{
__typename
:
'
User
'
,
id
:
'
gid://gitlab/User/1
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
name
:
'
Administrator
'
,
username
:
'
root
'
,
webUrl
:
'
/root
'
,
status
:
null
,
},
{
__typename
:
'
User
'
,
id
:
'
gid://gitlab/User/2
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80
\
u0026d=identicon
'
,
name
:
'
Jacki Kub
'
,
username
:
'
francina.skiles
'
,
webUrl
:
'
/franc
'
,
status
:
null
,
},
],
__typename
:
'
UserConnection
'
,
},
__typename
:
'
Issue
'
,
},
},
},
};
export
default
mockData
;
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