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
417f6cbe
Commit
417f6cbe
authored
Aug 11, 2021
by
Florie Guibert
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use dropdown widget in board scope assignee select
Changed: updated EE: true
parent
6d294541
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
100 additions
and
109 deletions
+100
-109
app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
...d/components/dropdown/dropdown_widget/dropdown_widget.vue
+21
-3
ee/app/assets/javascripts/boards/components/assignee_select.vue
.../assets/javascripts/boards/components/assignee_select.vue
+30
-83
ee/app/assets/javascripts/boards/constants.js
ee/app/assets/javascripts/boards/constants.js
+6
-0
ee/spec/frontend/boards/components/assignee_select_spec.js
ee/spec/frontend/boards/components/assignee_select_spec.js
+12
-18
ee/spec/frontend/boards/milestone_select_spec.js
ee/spec/frontend/boards/milestone_select_spec.js
+12
-1
locale/gitlab.pot
locale/gitlab.pot
+0
-3
spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
...nd/vue_shared/components/dropdown/dropdown_widget_spec.js
+19
-1
No files found.
app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
View file @
417f6cbe
...
...
@@ -54,6 +54,11 @@ export default {
required
:
false
,
default
:
''
,
},
isUserDropdown
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
computed
:
{
isSearchEmpty
()
{
...
...
@@ -68,7 +73,12 @@ export default {
this
.
$emit
(
'
set-option
'
,
option
||
null
);
},
isSelected
(
option
)
{
return
this
.
selected
&&
this
.
selected
.
title
===
option
.
title
;
return
(
this
.
selected
&&
(
this
.
isUserDropdown
?
this
.
selected
.
name
===
option
.
name
:
this
.
selected
.
title
===
option
.
title
)
);
},
showDropdown
()
{
this
.
$refs
.
dropdown
.
show
();
...
...
@@ -79,6 +89,12 @@ export default {
setSearchTerm
(
search
)
{
this
.
$emit
(
'
set-search
'
,
search
);
},
avatarUrl
(
option
)
{
return
this
.
isUserDropdown
?
option
.
avatar_url
||
option
.
avatarUrl
:
null
;
},
secondaryText
(
option
)
{
return
this
.
isUserDropdown
?
option
.
username
:
null
;
},
},
i18n
:
{
noMatchingResults
:
__
(
'
No matching results
'
),
...
...
@@ -121,7 +137,7 @@ export default {
:is-check-item=
"true"
@
click=
"selectOption(option)"
>
{{
option
.
title
}}
{{
option
.
title
||
option
.
name
}}
</gl-dropdown-item>
<gl-dropdown-divider
/>
</
template
>
...
...
@@ -131,10 +147,12 @@ export default {
:is-checked=
"isSelected(option)"
:is-check-centered=
"true"
:is-check-item=
"true"
:avatar-url=
"avatarUrl(option)"
:secondary-text=
"secondaryText(option)"
data-testid=
"unselected-option"
@
click=
"selectOption(option)"
>
{{ option.title }}
{{ option.title
|| option.name
}}
</gl-dropdown-item>
<gl-dropdown-item
v-if=
"noOptionsFound"
class=
"gl-pl-6!"
>
{{ $options.i18n.noMatchingResults }}
...
...
ee/app/assets/javascripts/boards/components/assignee_select.vue
View file @
417f6cbe
<
script
>
import
{
GlButton
,
GlDropdown
,
GlDropdownForm
,
GlDropdownDivider
,
GlDropdownItem
,
GlSearchBoxByType
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
{
GlButton
}
from
'
@gitlab/ui
'
;
import
{
isEmpty
}
from
'
lodash
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
searchGroupUsers
from
'
~/graphql_shared/queries/group_users_search.query.graphql
'
;
import
searchProjectUsers
from
'
~/graphql_shared/queries/users_search.query.graphql
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
ASSIGNEES_DEBOUNCE_DELAY
}
from
'
~/sidebar/constants
'
;
import
DropdownWidget
from
'
~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
'
;
import
UserAvatarImage
from
'
~/vue_shared/components/user_avatar/user_avatar_image.vue
'
;
import
{
AssigneesPreset
,
ANY_ASSIGNEE
}
from
'
../constants
'
;
export
default
{
AssigneesPreset
,
components
:
{
UserAvatarImage
,
DropdownWidget
,
GlButton
,
GlDropdown
,
GlDropdownForm
,
GlDropdownDivider
,
GlDropdownItem
,
GlSearchBoxByType
,
GlLoadingIcon
,
},
inject
:
[
'
fullPath
'
],
props
:
{
...
...
@@ -86,18 +77,12 @@ export default {
},
computed
:
{
...
mapGetters
([
'
isProjectBoard
'
]),
anyAssignee
()
{
return
this
.
selected
.
name
===
ANY_ASSIGNEE
.
name
;
},
isLoading
()
{
return
this
.
$apollo
.
queries
.
searchUsers
.
loading
;
},
isSearchEmpty
()
{
return
this
.
search
===
''
&&
!
this
.
isLoading
;
},
selectedIsEmpty
()
{
return
isEmpty
(
this
.
selected
);
},
noUsersFound
()
{
return
!
this
.
isSearchEmpty
&&
this
.
users
.
length
===
0
;
},
users
()
{
const
filteredUsers
=
this
.
searchUsers
.
filter
(
(
user
)
=>
user
.
name
.
includes
(
this
.
search
)
||
user
.
username
.
includes
(
this
.
search
),
...
...
@@ -113,6 +98,11 @@ export default {
);
},
},
created
()
{
if
(
isEmpty
(
this
.
board
.
assignee
))
{
this
.
selected
=
ANY_ASSIGNEE
;
}
},
methods
:
{
...
mapActions
([
'
setError
'
]),
selectAssignee
(
user
)
{
...
...
@@ -129,25 +119,21 @@ export default {
this
.
isDropdownShowing
=
false
;
}
},
isSelected
(
user
)
{
return
this
.
selected
?.
username
===
user
.
username
;
},
showDropdown
()
{
this
.
$refs
.
editDropdown
.
show
();
this
.
$refs
.
editDropdown
.
show
Dropdown
();
this
.
isDropdownShowing
=
true
;
},
setFocus
()
{
this
.
$refs
.
search
.
focusInput
();
},
hideDropdown
()
{
this
.
isEditing
=
false
;
},
setSearch
(
search
)
{
this
.
search
=
search
;
},
},
i18n
:
{
label
:
s__
(
'
BoardScope|Assignee
'
),
anyAssignee
:
s__
(
'
BoardScope|Any assignee
'
),
selectAssignee
:
s__
(
'
BoardScope|Select assignee
'
),
noMatchingResults
:
s__
(
'
BoardScope|No matching results
'
),
errorSearchingUsers
:
s__
(
'
BoardScope|An error occurred while searching for users, please try again.
'
,
),
...
...
@@ -171,7 +157,7 @@ export default {
</gl-button>
</div>
<div
v-if=
"!isEditing"
data-testid=
"selected-assignee"
>
<div
v-if=
"!
selectedIsEmpty
"
class=
"gl-display-flex gl-align-items-center"
>
<div
v-if=
"!
anyAssignee
"
class=
"gl-display-flex gl-align-items-center"
>
<user-avatar-image
:img-src=
"selected.avatarUrl || selected.avatar_url"
:size=
"32"
/>
<div>
<div
class=
"gl-font-weight-bold"
>
{{
selected
.
name
}}
</div>
...
...
@@ -181,58 +167,19 @@ export default {
<div
v-else
class=
"gl-text-gray-500"
>
{{
$options
.
i18n
.
anyAssignee
}}
</div>
</div>
<
gl-dropdown
<
dropdown-widget
v-show=
"isEditing"
ref=
"editDropdown"
:text=
"$options.i18n.selectAssignee"
lazy
menu-class=
"gl-w-full!"
class=
"gl-w-full"
@
shown=
"setFocus"
:select-text=
"$options.i18n.selectAssignee"
:preset-options=
"$options.AssigneesPreset"
:options=
"users"
:is-loading=
"isLoading"
:selected=
"selected"
:search-term=
"search"
:is-user-dropdown=
"true"
@
hide=
"hideDropdown"
>
<template
#header
>
<gl-search-box-by-type
ref=
"search"
v-model.trim=
"search"
class=
"js-dropdown-input-field"
/>
</
template
>
<gl-dropdown-form
class=
"gl-relative gl-min-h-7"
>
<gl-loading-icon
v-if=
"isLoading"
size=
"md"
class=
"gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<
template
v-else
>
<gl-dropdown-item
v-if=
"isSearchEmpty"
:is-checked=
"selectedIsEmpty"
:is-check-centered=
"true"
@
click=
"selectAssignee(null)"
>
<span
:class=
"selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'"
class=
"gl-font-weight-bold"
>
{{
$options
.
i18n
.
anyAssignee
}}
</span>
</gl-dropdown-item>
<gl-dropdown-divider
/>
<gl-dropdown-item
v-for=
"user in users"
:key=
"user.id"
:is-checked=
"isSelected(user)"
:is-check-centered=
"true"
:is-check-item=
"true"
:avatar-url=
"user.avatar_url || user.avatarUrl"
:secondary-text=
"user.username"
data-testid=
"unselected-user"
@
click=
"selectAssignee(user)"
>
{{
user
.
name
}}
</gl-dropdown-item>
<gl-dropdown-item
v-if=
"noUsersFound"
class=
"gl-pl-6!"
>
{{
$options
.
i18n
.
noMatchingResults
}}
</gl-dropdown-item>
</
template
>
</gl-dropdown-form>
<
template
#footer
>
<slot
name=
"footer"
></slot>
</
template
>
</gl-dropdown>
@
set-option=
"selectAssignee"
@
set-search=
"setSearch"
/>
</div>
</
template
>
ee/app/assets/javascripts/boards/constants.js
View file @
417f6cbe
...
...
@@ -79,6 +79,12 @@ export const MilestonesPreset = [
STARTED_MILESTONE
,
];
export
const
ANY_ASSIGNEE
=
{
id
:
'
gid://gitlab/User/-1
'
,
title
:
s__
(
'
BoardScope|Any assignee
'
),
};
export
const
AssigneesPreset
=
[
ANY_ASSIGNEE
];
export
const
WeightFilterType
=
{
none
:
'
None
'
,
};
...
...
ee/spec/frontend/boards/components/assignee_select_spec.js
View file @
417f6cbe
import
{
GlButton
,
GlDropdown
}
from
'
@gitlab/ui
'
;
import
{
GlButton
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
...
...
@@ -15,6 +15,7 @@ import defaultStore from '~/boards/stores';
import
searchGroupUsersQuery
from
'
~/graphql_shared/queries/group_users_search.query.graphql
'
;
import
searchProjectUsersQuery
from
'
~/graphql_shared/queries/users_search.query.graphql
'
;
import
{
ASSIGNEES_DEBOUNCE_DELAY
}
from
'
~/sidebar/constants
'
;
import
DropdownWidget
from
'
~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
VueApollo
);
...
...
@@ -26,7 +27,7 @@ describe('Assignee select component', () => {
const
selectedText
=
()
=>
wrapper
.
find
(
'
[data-testid="selected-assignee"]
'
).
text
();
const
findEditButton
=
()
=>
wrapper
.
findComponent
(
GlButton
);
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
DropdownWidget
);
const
usersQueryHandlerSuccess
=
jest
.
fn
().
mockResolvedValue
(
projectMembersResponse
);
const
groupUsersQueryHandlerSuccess
=
jest
.
fn
().
mockResolvedValue
(
groupMembersResponse
);
...
...
@@ -85,6 +86,13 @@ describe('Assignee select component', () => {
expect
(
usersQueryHandlerSuccess
).
not
.
toHaveBeenCalled
();
expect
(
findDropdown
().
isVisible
()).
toBe
(
false
);
});
it
(
'
renders selected assignee
'
,
async
()
=>
{
wrapper
.
vm
.
selected
=
mockUser2
;
await
waitForPromises
();
expect
(
selectedText
()).
toContain
(
mockUser2
.
username
);
});
});
describe
(
'
when editing
'
,
()
=>
{
...
...
@@ -96,22 +104,8 @@ describe('Assignee select component', () => {
expect
(
usersQueryHandlerSuccess
).
toHaveBeenCalled
();
expect
(
findDropdown
().
isVisible
()).
toBe
(
true
);
expect
(
wrapper
.
findAll
(
'
[data-testid="unselected-user"]
'
)).
toHaveLength
(
3
);
// 2 users + Any assignee item
});
it
(
'
renders selected assignee
'
,
async
()
=>
{
findEditButton
().
vm
.
$emit
(
'
click
'
);
await
waitForPromises
();
jest
.
advanceTimersByTime
(
ASSIGNEES_DEBOUNCE_DELAY
);
await
nextTick
();
wrapper
.
findAll
(
'
[data-testid="unselected-user"]
'
)
.
at
(
1
)
.
vm
.
$emit
(
'
click
'
,
new
Event
(
'
click
'
));
await
waitForPromises
();
expect
(
selectedText
()).
toContain
(
mockUser2
.
username
);
expect
(
findDropdown
().
props
(
'
options
'
)).
toHaveLength
(
3
);
expect
(
findDropdown
().
props
(
'
presetOptions
'
)).
toHaveLength
(
1
);
});
});
...
...
ee/spec/frontend/boards/milestone_select_spec.js
View file @
417f6cbe
...
...
@@ -9,7 +9,11 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
boardObj
}
from
'
jest/boards/mock_data
'
;
import
{
mockProjectMilestonesResponse
,
mockGroupMilestonesResponse
}
from
'
jest/sidebar/mock_data
'
;
import
{
mockProjectMilestonesResponse
,
mockGroupMilestonesResponse
,
mockMilestone1
,
}
from
'
jest/sidebar/mock_data
'
;
import
defaultStore
from
'
~/boards/stores
'
;
import
groupMilestonesQuery
from
'
~/sidebar/queries/group_milestones.query.graphql
'
;
...
...
@@ -92,6 +96,13 @@ describe('Milestone select component', () => {
expect
(
milestonesQueryHandlerSuccess
).
not
.
toHaveBeenCalled
();
expect
(
findDropdown
().
isVisible
()).
toBe
(
false
);
});
it
(
'
renders selected milestone
'
,
async
()
=>
{
wrapper
.
vm
.
selected
=
mockMilestone1
;
await
waitForPromises
();
expect
(
selectedText
()).
toContain
(
mockMilestone1
.
title
);
});
});
describe
(
'
when editing
'
,
()
=>
{
...
...
locale/gitlab.pot
View file @
417f6cbe
...
...
@@ -5422,9 +5422,6 @@ msgstr ""
msgid "BoardScope|Milestone"
msgstr ""
msgid "BoardScope|No matching results"
msgstr ""
msgid "BoardScope|No milestone"
msgstr ""
...
...
spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
View file @
417f6cbe
...
...
@@ -13,7 +13,6 @@ describe('DropdownWidget component', () => {
const
createComponent
=
({
props
=
{}
}
=
{})
=>
{
wrapper
=
shallowMount
(
DropdownWidget
,
{
propsData
:
{
...
props
,
options
:
[
{
id
:
'
1
'
,
...
...
@@ -24,6 +23,7 @@ describe('DropdownWidget component', () => {
title
:
'
Option 2
'
,
},
],
...
props
,
},
stubs
:
{
GlDropdown
,
...
...
@@ -76,4 +76,22 @@ describe('DropdownWidget component', () => {
expect
(
wrapper
.
emitted
(
'
set-option
'
)).
toEqual
([[
wrapper
.
props
().
options
[
1
]]]);
});
});
describe
(
'
when user dropdown
'
,
()
=>
{
const
mockUser
=
{
id
:
1
,
name
:
'
User name
'
,
username
:
'
username
'
,
avatarUrl
:
'
foo/bar
'
,
};
beforeEach
(()
=>
{
createComponent
({
props
:
{
isUserDropdown
:
true
,
options
:
[
mockUser
]
}
});
});
it
(
'
passes user related props to dropdown item
'
,
()
=>
{
expect
(
findDropdownItems
().
at
(
0
).
props
(
'
avatarUrl
'
)).
toBe
(
mockUser
.
avatarUrl
);
expect
(
findDropdownItems
().
at
(
0
).
props
(
'
secondaryText
'
)).
toBe
(
mockUser
.
username
);
});
});
});
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