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
d88a3e5a
Commit
d88a3e5a
authored
Aug 02, 2021
by
Florie Guibert
Committed by
Natalia Tepluhina
Aug 02, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor board scope milestone dropdown
parent
61bce846
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
622 additions
and
228 deletions
+622
-228
app/assets/javascripts/boards/components/board_form.vue
app/assets/javascripts/boards/components/board_form.vue
+12
-7
app/assets/javascripts/graphql_shared/utils.js
app/assets/javascripts/graphql_shared/utils.js
+19
-0
app/assets/javascripts/sidebar/queries/group_milestones.query.graphql
...avascripts/sidebar/queries/group_milestones.query.graphql
+20
-0
app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
...nents/dropdown/dropdown_widget/dropdown_widget.stories.js
+27
-0
app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
...d/components/dropdown/dropdown_widget/dropdown_widget.vue
+148
-0
ee/app/assets/javascripts/boards/components/board_scope.vue
ee/app/assets/javascripts/boards/components/board_scope.vue
+1
-0
ee/app/assets/javascripts/boards/components/milestone_select.vue
...assets/javascripts/boards/components/milestone_select.vue
+115
-89
ee/app/assets/javascripts/boards/constants.js
ee/app/assets/javascripts/boards/constants.js
+24
-0
ee/spec/features/boards/scoped_issue_board_spec.rb
ee/spec/features/boards/scoped_issue_board_spec.rb
+4
-4
ee/spec/frontend/boards/components/board_scope_spec.js
ee/spec/frontend/boards/components/board_scope_spec.js
+1
-0
ee/spec/frontend/boards/milestone_select_spec.js
ee/spec/frontend/boards/milestone_select_spec.js
+114
-128
locale/gitlab.pot
locale/gitlab.pot
+30
-0
spec/frontend/graphql_shared/utils_spec.js
spec/frontend/graphql_shared/utils_spec.js
+15
-0
spec/frontend/sidebar/mock_data.js
spec/frontend/sidebar/mock_data.js
+13
-0
spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
...nd/vue_shared/components/dropdown/dropdown_widget_spec.js
+79
-0
No files found.
app/assets/javascripts/boards/components/board_form.vue
View file @
d88a3e5a
...
@@ -18,7 +18,7 @@ const boardDefaults = {
...
@@ -18,7 +18,7 @@ const boardDefaults = {
id
:
false
,
id
:
false
,
name
:
''
,
name
:
''
,
labels
:
[],
labels
:
[],
milestone
_id
:
undefined
,
milestone
:
{}
,
iteration_id
:
undefined
,
iteration_id
:
undefined
,
assignee
:
{},
assignee
:
{},
weight
:
null
,
weight
:
null
,
...
@@ -190,10 +190,9 @@ export default {
...
@@ -190,10 +190,9 @@ export default {
return
{
return
{
weight
:
this
.
board
.
weight
,
weight
:
this
.
board
.
weight
,
assigneeId
:
this
.
board
.
assignee
?.
id
||
null
,
assigneeId
:
this
.
board
.
assignee
?.
id
||
null
,
milestoneId
:
milestoneId
:
this
.
board
.
milestone
?.
id
this
.
board
.
milestone
?.
id
||
this
.
board
.
milestone
?.
id
===
0
?
convertToGraphQLId
(
TYPE_MILESTONE
,
this
.
board
.
milestone
.
id
)
?
convertToGraphQLId
(
TYPE_MILESTONE
,
this
.
board
.
milestone
.
id
)
:
null
,
:
null
,
iterationId
:
this
.
board
.
iteration_id
iterationId
:
this
.
board
.
iteration_id
?
convertToGraphQLId
(
TYPE_ITERATION
,
this
.
board
.
iteration_id
)
?
convertToGraphQLId
(
TYPE_ITERATION
,
this
.
board
.
iteration_id
)
:
null
,
:
null
,
...
@@ -304,9 +303,14 @@ export default {
...
@@ -304,9 +303,14 @@ export default {
});
});
},
},
setAssignee
(
assigneeId
)
{
setAssignee
(
assigneeId
)
{
this
.
board
.
assignee
=
{
this
.
$set
(
this
.
board
,
'
assignee
'
,
{
id
:
assigneeId
,
id
:
assigneeId
,
};
});
},
setMilestone
(
milestoneId
)
{
this
.
$set
(
this
.
board
,
'
milestone
'
,
{
id
:
milestoneId
,
});
},
},
},
},
};
};
...
@@ -376,6 +380,7 @@ export default {
...
@@ -376,6 +380,7 @@ export default {
@
set-iteration=
"setIteration"
@
set-iteration=
"setIteration"
@
set-board-labels=
"setBoardLabels"
@
set-board-labels=
"setBoardLabels"
@
set-assignee=
"setAssignee"
@
set-assignee=
"setAssignee"
@
set-milestone=
"setMilestone"
/>
/>
</form>
</form>
</gl-modal>
</gl-modal>
...
...
app/assets/javascripts/graphql_shared/utils.js
View file @
d88a3e5a
import
{
isArray
}
from
'
lodash
'
;
import
{
isArray
}
from
'
lodash
'
;
/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method checks if the passed id follows that format
*
* @param {String|Number} id The id value
* @returns {Boolean}
*/
export
const
isGid
=
(
id
)
=>
{
if
(
typeof
id
===
'
string
'
&&
id
.
startsWith
(
'
gid://gitlab/
'
))
{
return
true
;
}
return
false
;
};
/**
/**
* Ids generated by GraphQL endpoints are usually in the format
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method extracts Id number
* gid://gitlab/Environments/123. This method extracts Id number
...
@@ -35,6 +50,10 @@ export const convertToGraphQLId = (type, id) => {
...
@@ -35,6 +50,10 @@ export const convertToGraphQLId = (type, id) => {
throw
new
TypeError
(
`id must be a number or string; got
${
typeof
id
}
`
);
throw
new
TypeError
(
`id must be a number or string; got
${
typeof
id
}
`
);
}
}
if
(
isGid
(
id
))
{
return
id
;
}
return
`gid://gitlab/
${
type
}
/
${
id
}
`
;
return
`gid://gitlab/
${
type
}
/
${
id
}
`
;
};
};
...
...
app/assets/javascripts/sidebar/queries/group_milestones.query.graphql
0 → 100644
View file @
d88a3e5a
#import "./milestone.fragment.graphql"
query
groupMilestones
(
$fullPath
:
ID
!,
$title
:
String
,
$state
:
MilestoneStateEnum
)
{
workspace
:
group
(
fullPath
:
$fullPath
)
{
__typename
id
attributes
:
milestones
(
searchTitle
:
$title
state
:
$state
sort
:
EXPIRED_LAST_DUE_DATE_ASC
first
:
20
includeAncestors
:
true
)
{
nodes
{
...
MilestoneFragment
state
}
}
}
}
app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
0 → 100644
View file @
d88a3e5a
/* eslint-disable @gitlab/require-i18n-strings */
import
{
__
}
from
'
~/locale
'
;
import
DropdownWidget
from
'
./dropdown_widget.vue
'
;
export
default
{
component
:
DropdownWidget
,
title
:
'
vue_shared/components/dropdown/dropdown_widget/dropdown_widget
'
,
};
const
Template
=
(
args
,
{
argTypes
})
=>
({
components
:
{
DropdownWidget
},
props
:
Object
.
keys
(
argTypes
),
template
:
'
<dropdown-widget v-bind="$props" v-on="$props" />
'
,
});
export
const
Default
=
Template
.
bind
({});
Default
.
args
=
{
options
:
[
{
id
:
'
gid://gitlab/Milestone/-1
'
,
title
:
__
(
'
Any Milestone
'
)
},
{
id
:
'
gid://gitlab/Milestone/0
'
,
title
:
__
(
'
No Milestone
'
)
},
{
id
:
'
gid://gitlab/Milestone/-2
'
,
title
:
__
(
'
Upcoming
'
)
},
{
id
:
'
gid://gitlab/Milestone/-3
'
,
title
:
__
(
'
Started
'
)
},
],
selectText
:
'
Select
'
,
searchText
:
'
Search
'
,
};
app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
0 → 100644
View file @
d88a3e5a
<
script
>
import
{
GlLoadingIcon
,
GlDropdown
,
GlDropdownForm
,
GlDropdownDivider
,
GlDropdownItem
,
GlSearchBoxByType
,
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
components
:
{
GlLoadingIcon
,
GlDropdown
,
GlDropdownForm
,
GlDropdownDivider
,
GlDropdownItem
,
GlSearchBoxByType
,
},
props
:
{
selectText
:
{
type
:
String
,
required
:
false
,
default
:
__
(
'
Select
'
),
},
searchText
:
{
type
:
String
,
required
:
false
,
default
:
__
(
'
Search
'
),
},
presetOptions
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
options
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
isLoading
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
selected
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
{},
},
searchTerm
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
computed
:
{
isSearchEmpty
()
{
return
this
.
searchTerm
===
''
&&
!
this
.
isLoading
;
},
noOptionsFound
()
{
return
!
this
.
isSearchEmpty
&&
this
.
options
.
length
===
0
;
},
},
methods
:
{
selectOption
(
option
)
{
this
.
$emit
(
'
set-option
'
,
option
||
null
);
},
isSelected
(
option
)
{
return
this
.
selected
&&
this
.
selected
.
title
===
option
.
title
;
},
showDropdown
()
{
this
.
$refs
.
dropdown
.
show
();
},
setFocus
()
{
this
.
$refs
.
search
.
focusInput
();
},
setSearchTerm
(
search
)
{
this
.
$emit
(
'
set-search
'
,
search
);
},
},
i18n
:
{
noMatchingResults
:
__
(
'
No matching results
'
),
},
};
</
script
>
<
template
>
<gl-dropdown
ref=
"dropdown"
:text=
"selectText"
lazy
menu-class=
"gl-w-full!"
class=
"gl-w-full"
v-on=
"$listeners"
@
shown=
"setFocus"
>
<template
#header
>
<gl-search-box-by-type
ref=
"search"
:value=
"searchTerm"
:placeholder=
"searchText"
class=
"js-dropdown-input-field"
@
input=
"setSearchTerm"
/>
</
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
>
<template
v-if=
"isSearchEmpty && presetOptions.length > 0"
>
<gl-dropdown-item
v-for=
"option in presetOptions"
:key=
"option.id"
:is-checked=
"isSelected(option)"
:is-check-centered=
"true"
:is-check-item=
"true"
@
click=
"selectOption(option)"
>
{{
option
.
title
}}
</gl-dropdown-item>
<gl-dropdown-divider
/>
</
template
>
<gl-dropdown-item
v-for=
"option in options"
:key=
"option.id"
:is-checked=
"isSelected(option)"
:is-check-centered=
"true"
:is-check-item=
"true"
data-testid=
"unselected-option"
@
click=
"selectOption(option)"
>
{{ option.title }}
</gl-dropdown-item>
<gl-dropdown-item
v-if=
"noOptionsFound"
class=
"gl-pl-6!"
>
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
</template>
</gl-dropdown-form>
<
template
#footer
>
<slot
name=
"footer"
></slot>
</
template
>
</gl-dropdown>
</template>
ee/app/assets/javascripts/boards/components/board_scope.vue
View file @
d88a3e5a
...
@@ -101,6 +101,7 @@ export default {
...
@@ -101,6 +101,7 @@ export default {
:group-id=
"groupId"
:group-id=
"groupId"
:project-id=
"projectId"
:project-id=
"projectId"
:can-edit=
"canAdminBoard"
:can-edit=
"canAdminBoard"
@
set-milestone=
"$emit('set-milestone', $event)"
/>
/>
<board-scope-current-iteration
<board-scope-current-iteration
...
...
ee/app/assets/javascripts/boards/components/milestone_select.vue
View file @
d88a3e5a
<
script
>
<
script
>
import
{
GlLoadingIcon
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
GlButton
}
from
'
@gitlab/ui
'
;
import
MilestoneSelect
from
'
~/milestone_select
'
;
import
{
isEmpty
}
from
'
lodash
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
s__
}
from
'
~/locale
'
;
const
ANY_MILESTONE
=
'
Any milestone
'
;
import
groupMilestonesQuery
from
'
~/sidebar/queries/group_milestones.query.graphql
'
;
const
NO_MILESTONE
=
'
No milestone
'
;
import
projectMilestonesQuery
from
'
~/sidebar/queries/project_milestones.query.graphql
'
;
import
DropdownWidget
from
'
~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
'
;
import
{
MilestonesPreset
,
ANY_MILESTONE
}
from
'
../constants
'
;
export
default
{
export
default
{
MilestonesPreset
,
components
:
{
components
:
{
Gl
LoadingIc
on
,
Gl
Butt
on
,
GlIcon
,
DropdownWidget
,
},
},
inject
:
[
'
fullPath
'
],
props
:
{
props
:
{
board
:
{
board
:
{
type
:
Object
,
type
:
Object
,
...
@@ -31,48 +39,93 @@ export default {
...
@@ -31,48 +39,93 @@ export default {
default
:
false
,
default
:
false
,
},
},
},
},
data
()
{
computed
:
{
return
{
milestoneTitle
()
{
search
:
''
,
if
(
this
.
noMilestone
)
return
NO_MILESTONE
;
milestones
:
[],
return
this
.
board
.
milestone
?
this
.
board
.
milestone
.
title
:
ANY_MILESTONE
;
selected
:
this
.
board
.
milestone
,
isEditing
:
false
,
isDropdownShowing
:
false
,
};
},
apollo
:
{
milestones
:
{
query
()
{
return
this
.
isProjectBoard
?
projectMilestonesQuery
:
groupMilestonesQuery
;
},
variables
()
{
return
{
fullPath
:
this
.
fullPath
,
title
:
this
.
search
,
first
:
20
,
};
},
skip
()
{
return
!
this
.
isEditing
;
},
update
(
data
)
{
return
data
?.
workspace
?.
attributes
?.
nodes
||
[];
},
error
()
{
this
.
setError
({
message
:
this
.
$options
.
i18n
.
errorSearchingMilestones
});
},
},
},
noMilestone
()
{
},
return
this
.
milestoneId
===
0
;
computed
:
{
...
mapGetters
([
'
isProjectBoard
'
]),
anyMilestone
()
{
return
this
.
selected
.
title
===
ANY_MILESTONE
.
title
;
},
},
milestone
Id
()
{
milestone
Title
()
{
return
this
.
board
.
milestone_id
;
return
this
.
selected
.
title
;
},
},
milestoneTitleClass
()
{
milestoneTitleClass
()
{
return
this
.
milestoneTitle
===
ANY_MILESTONE
?
'
text-secondary
'
:
'
bold
'
;
return
this
.
anyMilestone
?
'
gl-text-gray-500
'
:
'
gl-font-weight-
bold
'
;
},
},
selected
()
{
isLoading
()
{
if
(
this
.
noMilestone
)
return
NO_MILESTONE
;
return
this
.
$apollo
.
queries
.
milestones
.
loading
;
return
this
.
board
.
milestone
?
this
.
board
.
milestone
.
name
:
''
;
},
},
},
},
moun
ted
()
{
crea
ted
()
{
this
.
milestoneDropdown
=
new
MilestoneSelect
(
null
,
this
.
$refs
.
dropdownButton
,
{
if
(
isEmpty
(
this
.
board
.
milestone
))
{
handleClick
:
this
.
selectMilestone
,
this
.
selected
=
ANY_MILESTONE
;
}
);
}
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
setError
'
]),
selectMilestone
(
milestone
)
{
selectMilestone
(
milestone
)
{
let
{
id
}
=
milestone
;
this
.
selected
=
milestone
;
// swap the IDs of 'Any' and 'No' milestone to what backend requires
this
.
toggleEdit
();
if
(
milestone
.
title
===
ANY_MILESTONE
)
{
this
.
$emit
(
'
set-milestone
'
,
milestone
?.
id
||
null
);
id
=
-
1
;
},
}
else
if
(
milestone
.
title
===
NO_MILESTONE
)
{
toggleEdit
()
{
id
=
0
;
if
(
!
this
.
isEditing
&&
!
this
.
isDropdownShowing
)
{
this
.
isEditing
=
true
;
this
.
showDropdown
();
}
else
{
this
.
isEditing
=
false
;
this
.
isDropdownShowing
=
false
;
}
}
// eslint-disable-next-line vue/no-mutating-props
this
.
board
.
milestone_id
=
id
;
// eslint-disable-next-line vue/no-mutating-props
this
.
board
.
milestone
=
{
...
milestone
,
id
,
};
},
},
showDropdown
()
{
this
.
$refs
.
editDropdown
.
showDropdown
();
this
.
isDropdownShowing
=
true
;
},
hideDropdown
()
{
this
.
isEditing
=
false
;
},
setSearch
(
search
)
{
this
.
search
=
search
;
},
},
i18n
:
{
label
:
s__
(
'
BoardScope|Milestone
'
),
errorSearchingMilestones
:
s__
(
'
BoardScope|An error occurred while getting milestones, please try again.
'
,
),
searchMilestones
:
s__
(
'
BoardScope|Search milestones
'
),
selectMilestone
:
s__
(
'
BoardScope|Select milestone
'
),
edit
:
s__
(
'
BoardScope|Edit
'
),
},
},
};
};
</
script
>
</
script
>
...
@@ -80,60 +133,33 @@ export default {
...
@@ -80,60 +133,33 @@ export default {
<
template
>
<
template
>
<div
class=
"block milestone"
>
<div
class=
"block milestone"
>
<div
class=
"title gl-mb-3"
>
<div
class=
"title gl-mb-3"
>
{{
__
(
'
Milestone
'
)
}}
{{
$options
.
i18n
.
label
}}
<button
v-if=
"canEdit"
type=
"button"
class=
"edit-link btn btn-blank float-right"
>
<gl-button
{{
__
(
'
Edit
'
)
}}
v-if=
"canEdit"
</button>
variant=
"link"
class=
"edit-link float-right gl-text-gray-900!"
@
click=
"toggleEdit"
>
{{
$options
.
i18n
.
edit
}}
</gl-button>
</div>
</div>
<div
:class=
"milestoneTitleClass"
class=
"value"
>
{{
milestoneTitle
}}
</div>
<div
v-if=
"!isEditing"
:class=
"milestoneTitleClass"
data-testid=
"selected-milestone"
>
<div
class=
"selectbox"
style=
"display: none"
>
{{
milestoneTitle
}}
<input
:value=
"milestoneId"
name=
"milestone_id"
type=
"hidden"
/>
<div
class=
"dropdown"
>
<!-- eslint-disable @gitlab/vue-no-data-toggle -->
<button
ref=
"dropdownButton"
:data-selected=
"selected"
:data-project-id=
"projectId"
:data-group-id=
"groupId"
:data-show-no=
"true"
:data-show-any=
"true"
:data-show-started=
"true"
:data-show-upcoming=
"true"
:data-use-id=
"true"
class=
"dropdown-menu-toggle wide"
data-toggle=
"dropdown"
type=
"button"
>
{{
__
(
'
Milestone
'
)
}}
<gl-icon
name=
"chevron-down"
class=
"gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
:size=
"16"
/>
</button>
<!-- eslint-enable @gitlab/vue-no-data-toggle -->
<div
class=
"dropdown-menu dropdown-select dropdown-menu-selectable"
>
<div
class=
"dropdown-input"
>
<input
type=
"search"
class=
"dropdown-input-field"
:placeholder=
"__('Search milestones')"
autocomplete=
"off"
/>
<gl-icon
name=
"search"
class=
"dropdown-input-search gl-absolute gl-top-3 gl-right-5 gl-text-gray-300 gl-pointer-events-none"
/>
<gl-icon
name=
"close"
class=
"dropdown-input-clear js-dropdown-input-clear gl-right-5 gl-absolute gl-top-3 gl-text-gray-500"
/>
</div>
<div
class=
"dropdown-content"
></div>
<div
class=
"dropdown-loading"
><gl-loading-icon
size=
"sm"
/></div>
</div>
</div>
</div>
</div>
<dropdown-widget
v-show=
"isEditing"
ref=
"editDropdown"
:select-text=
"$options.i18n.selectMilestone"
:search-text=
"$options.i18n.searchMilestones"
:preset-options=
"$options.MilestonesPreset"
:options=
"milestones"
:is-loading=
"isLoading"
:selected=
"selected"
:search-term=
"search"
@
hide=
"hideDropdown"
@
set-option=
"selectMilestone"
@
set-search=
"setSearch"
/>
</div>
</div>
</
template
>
</
template
>
ee/app/assets/javascripts/boards/constants.js
View file @
d88a3e5a
...
@@ -55,6 +55,30 @@ export const MilestoneIDs = {
...
@@ -55,6 +55,30 @@ export const MilestoneIDs = {
NONE
:
0
,
NONE
:
0
,
};
};
export
const
ANY_MILESTONE
=
{
id
:
'
gid://gitlab/Milestone/-1
'
,
title
:
s__
(
'
BoardScope|Any Milestone
'
),
};
export
const
NO_MILESTONE
=
{
id
:
'
gid://gitlab/Milestone/0
'
,
title
:
s__
(
'
BoardScope|No milestone
'
),
};
export
const
UPCOMING_MILESTONE
=
{
id
:
'
gid://gitlab/Milestone/-2
'
,
title
:
s__
(
'
BoardScope|Upcoming
'
),
};
export
const
STARTED_MILESTONE
=
{
id
:
'
gid://gitlab/Milestone/-3
'
,
title
:
s__
(
'
BoardScope|Started
'
),
};
export
const
MilestonesPreset
=
[
ANY_MILESTONE
,
NO_MILESTONE
,
UPCOMING_MILESTONE
,
STARTED_MILESTONE
,
];
export
const
WeightFilterType
=
{
export
const
WeightFilterType
=
{
none
:
'
None
'
,
none
:
'
None
'
,
};
};
...
...
ee/spec/features/boards/scoped_issue_board_spec.rb
View file @
d88a3e5a
...
@@ -65,8 +65,8 @@ RSpec.describe 'Scoped issue boards', :js do
...
@@ -65,8 +65,8 @@ RSpec.describe 'Scoped issue boards', :js do
expect
(
page
).
to
have_selector
(
'.board-card'
,
count:
2
)
expect
(
page
).
to
have_selector
(
'.board-card'
,
count:
2
)
end
end
it
'creates board filtering by Any
m
ilestone'
do
it
'creates board filtering by Any
M
ilestone'
do
create_board_milestone
(
'Any
m
ilestone'
)
create_board_milestone
(
'Any
M
ilestone'
)
expect
(
find
(
'.tokens-container'
)).
to
have_content
(
""
)
expect
(
find
(
'.tokens-container'
)).
to
have_content
(
""
)
expect
(
page
).
to
have_selector
(
'.board-card'
,
count:
3
)
expect
(
page
).
to
have_selector
(
'.board-card'
,
count:
3
)
...
@@ -228,7 +228,7 @@ RSpec.describe 'Scoped issue boards', :js do
...
@@ -228,7 +228,7 @@ RSpec.describe 'Scoped issue boards', :js do
edit_board
.
click
edit_board
.
click
expect
(
find
(
'
.milestone .value
'
)).
to
have_content
(
milestone
.
title
)
expect
(
find
(
'
[data-testid="selected-milestone"]
'
)).
to
have_content
(
milestone
.
title
)
expect
(
find
(
'[data-testid="selected-assignee"]'
)).
to
have_content
(
user
.
name
)
expect
(
find
(
'[data-testid="selected-assignee"]'
)).
to
have_content
(
user
.
name
)
expect
(
find
(
'.weight .value'
)).
to
have_content
(
2
)
expect
(
find
(
'.weight .value'
)).
to
have_content
(
2
)
end
end
...
@@ -242,7 +242,7 @@ RSpec.describe 'Scoped issue boards', :js do
...
@@ -242,7 +242,7 @@ RSpec.describe 'Scoped issue boards', :js do
end
end
it
'sets board to any milestone'
do
it
'sets board to any milestone'
do
update_board_milestone
(
'Any
m
ilestone'
)
update_board_milestone
(
'Any
M
ilestone'
)
expect
(
find
(
'.tokens-container'
)).
not_to
have_content
(
milestone
.
title
)
expect
(
find
(
'.tokens-container'
)).
not_to
have_content
(
milestone
.
title
)
...
...
ee/spec/frontend/boards/components/board_scope_spec.js
View file @
d88a3e5a
...
@@ -37,6 +37,7 @@ describe('BoardScope', () => {
...
@@ -37,6 +37,7 @@ describe('BoardScope', () => {
},
},
stubs
:
{
stubs
:
{
AssigneeSelect
:
true
,
AssigneeSelect
:
true
,
BoardMilestoneSelect
:
true
,
},
},
});
});
}
}
...
...
ee/spec/frontend/boards/milestone_select_spec.js
View file @
d88a3e5a
import
Vue
from
'
vue
'
;
import
{
GlButton
,
GlDropdown
,
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
Vuex
from
'
vuex
'
;
import
MilestoneSelect
from
'
ee/boards/components/milestone_select.vue
'
;
import
MilestoneSelect
from
'
ee/boards/components/milestone_select.vue
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
boardObj
}
from
'
jest/boards/mock_data
'
;
import
{
boardObj
}
from
'
jest/boards/mock_data
'
;
import
Api
from
'
~/api
'
;
import
{
mockProjectMilestonesResponse
,
mockGroupMilestonesResponse
}
from
'
jest/sidebar/mock_data
'
;
import
IssuableContext
from
'
~/issuable_context
'
;
let
vm
;
function
selectedText
()
{
return
vm
.
$el
.
querySelector
(
'
.value
'
).
innerText
.
trim
();
}
function
activeDropdownItem
(
index
)
{
const
items
=
vm
.
$el
.
querySelectorAll
(
'
.is-active
'
);
if
(
!
items
[
index
])
return
''
;
return
items
[
index
].
innerText
.
trim
();
}
const
milestone
=
{
id
:
1
,
title
:
'
first milestone
'
,
name
:
'
first milestone
'
,
due_date
:
'
2015-05-05
'
,
expired
:
true
,
};
const
milestone2
=
{
id
:
2
,
title
:
'
second milestone
'
,
name
:
'
second milestone
'
,
due_date
:
null
,
expired
:
false
,
};
describe
(
'
Milestone select component
'
,
()
=>
{
import
defaultStore
from
'
~/boards/stores
'
;
beforeEach
((
done
)
=>
{
import
groupMilestonesQuery
from
'
~/sidebar/queries/group_milestones.query.graphql
'
;
setFixtures
(
'
<div class="test-container"></div>
'
);
import
projectMilestonesQuery
from
'
~/sidebar/queries/project_milestones.query.graphql
'
;
import
DropdownWidget
from
'
~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
'
;
// eslint-disable-next-line no-new
const
localVue
=
createLocalVue
();
new
IssuableContext
(
);
localVue
.
use
(
VueApollo
);
const
Component
=
Vue
.
extend
(
MilestoneSelect
);
describe
(
'
Milestone select component
'
,
()
=>
{
vm
=
new
Component
({
let
wrapper
;
let
fakeApollo
;
let
store
;
const
selectedText
=
()
=>
wrapper
.
find
(
'
[data-testid="selected-milestone"]
'
).
text
();
const
findEditButton
=
()
=>
wrapper
.
findComponent
(
GlButton
);
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
DropdownWidget
);
const
milestonesQueryHandlerSuccess
=
jest
.
fn
().
mockResolvedValue
(
mockProjectMilestonesResponse
);
const
groupUsersQueryHandlerSuccess
=
jest
.
fn
().
mockResolvedValue
(
mockGroupMilestonesResponse
);
const
createStore
=
({
isGroupBoard
=
false
,
isProjectBoard
=
false
}
=
{})
=>
{
store
=
new
Vuex
.
Store
({
...
defaultStore
,
getters
:
{
isGroupBoard
:
()
=>
isGroupBoard
,
isProjectBoard
:
()
=>
isProjectBoard
,
},
});
};
const
createComponent
=
({
props
=
{},
milestonesQueryHandler
=
milestonesQueryHandlerSuccess
,
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([
[
projectMilestonesQuery
,
milestonesQueryHandler
],
[
groupMilestonesQuery
,
groupUsersQueryHandlerSuccess
],
]);
wrapper
=
shallowMount
(
MilestoneSelect
,
{
localVue
,
store
,
apolloProvider
:
fakeApollo
,
propsData
:
{
propsData
:
{
board
:
boardObj
,
board
:
boardObj
,
groupId
:
2
,
projectId
:
2
,
canEdit
:
true
,
canEdit
:
true
,
...
props
,
},
provide
:
{
fullPath
:
'
gitlab-org
'
,
},
stubs
:
{
GlDropdown
,
GlDropdownItem
,
},
},
})
.
$mount
(
'
.test-container
'
)
;
});
setImmediate
(
done
);
// We need to mock out `showDropdown` which
});
// invokes `show` method of BDropdown used inside GlDropdown.
jest
.
spyOn
(
wrapper
.
vm
,
'
showDropdown
'
).
mockImplementation
();
};
describe
(
'
canEdit
'
,
()
=>
{
beforeEach
(()
=>
{
it
(
'
hides Edit button
'
,
(
done
)
=>
{
createStore
({
isProjectBoard
:
true
});
vm
.
canEdit
=
false
;
createComponent
();
Vue
.
nextTick
(()
=>
{
});
expect
(
vm
.
$el
.
querySelector
(
'
.edit-link
'
)).
toBeFalsy
();
done
();
});
});
it
(
'
shows Edit button if true
'
,
(
done
)
=>
{
afterEach
(()
=>
{
vm
.
canEdit
=
true
;
wrapper
.
destroy
();
Vue
.
nextTick
(()
=>
{
fakeApollo
=
null
;
expect
(
vm
.
$el
.
querySelector
(
'
.edit-link
'
)).
toBeTruthy
();
store
=
null
;
done
();
});
});
});
});
describe
(
'
selected value
'
,
()
=>
{
describe
(
'
when not editing
'
,
()
=>
{
it
(
'
defaults to Any milestone
'
,
()
=>
{
it
(
'
defaults to Any milestone
'
,
()
=>
{
expect
(
selectedText
()).
toContain
(
'
Any
m
ilestone
'
);
expect
(
selectedText
()).
toContain
(
'
Any
M
ilestone
'
);
});
});
it
(
'
shows No milestone
'
,
(
done
)
=>
{
it
(
'
skips the queries and does not render dropdown
'
,
()
=>
{
vm
.
board
.
milestone_id
=
0
;
expect
(
milestonesQueryHandlerSuccess
).
not
.
toHaveBeenCalled
();
Vue
.
nextTick
(()
=>
{
expect
(
findDropdown
().
isVisible
()).
toBe
(
false
);
expect
(
selectedText
()).
toContain
(
'
No milestone
'
);
done
();
});
});
});
});
it
(
'
shows selected milestone title
'
,
(
done
)
=>
{
describe
(
'
when editing
'
,
()
=>
{
vm
.
board
.
milestone_id
=
20
;
it
(
'
trigger query and renders dropdown with passed milestones
'
,
async
()
=>
{
vm
.
board
.
milestone
=
{
findEditButton
().
vm
.
$emit
(
'
click
'
);
id
:
20
,
await
waitForPromises
();
title
:
'
Selected milestone
'
,
await
nextTick
();
};
expect
(
milestonesQueryHandlerSuccess
).
toHaveBeenCalled
();
Vue
.
nextTick
(()
=>
{
expect
(
selectedText
()).
toContain
(
'
Selected milestone
'
);
done
();
});
});
describe
(
'
clicking dropdown items
'
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
Api
,
'
projectMilestones
'
).
mockResolvedValue
({
data
:
[
milestone
,
milestone2
]
});
});
it
(
'
sets Any milestone
'
,
async
(
done
)
=>
{
vm
.
board
.
milestone_id
=
0
;
vm
.
$el
.
querySelector
(
'
.edit-link
'
).
click
();
await
vm
.
$nextTick
();
jest
.
runOnlyPendingTimers
();
setImmediate
(()
=>
{
vm
.
$el
.
querySelectorAll
(
'
li a
'
)[
0
].
click
();
});
setImmediate
(()
=>
{
expect
(
findDropdown
().
isVisible
()).
toBe
(
true
);
expect
(
activeDropdownItem
(
0
)).
toEqual
(
'
Any milestone
'
);
expect
(
findDropdown
().
props
(
'
options
'
)).
toHaveLength
(
2
);
expect
(
selectedText
()).
toEqual
(
'
Any milestone
'
);
});
done
();
});
});
});
it
(
'
sets No milestone
'
,
(
done
)
=>
{
describe
(
'
canEdit
'
,
()
=>
{
vm
.
$el
.
querySelector
(
'
.edit-link
'
).
click
();
it
(
'
hides Edit button
'
,
async
()
=>
{
wrapper
.
setProps
({
canEdit
:
false
});
await
nextTick
();
jest
.
runOnlyPendingTimers
();
expect
(
findEditButton
().
exists
()).
toBe
(
false
);
});
setImmediate
(()
=>
{
it
(
'
shows Edit button if true
'
,
()
=>
{
vm
.
$el
.
querySelectorAll
(
'
li a
'
)[
1
].
click
();
expect
(
findEditButton
().
exists
()).
toBe
(
true
);
});
});
});
setImmediate
(()
=>
{
it
.
each
`
expect
(
activeDropdownItem
(
0
)).
toEqual
(
'
No milestone
'
);
boardType | mockedResponse | queryHandler | notCalledHandler
expect
(
selectedText
()).
toEqual
(
'
No milestone
'
);
${
'
group
'
}
|
${
mockGroupMilestonesResponse
}
|
${
groupUsersQueryHandlerSuccess
}
|
${
milestonesQueryHandlerSuccess
}
done
();
${
'
project
'
}
|
${
mockProjectMilestonesResponse
}
|
${
milestonesQueryHandlerSuccess
}
|
${
groupUsersQueryHandlerSuccess
}
});
`
(
'
fetches $boardType milestones
'
,
async
({
boardType
,
mockedResponse
,
queryHandler
,
notCalledHandler
})
=>
{
createStore
({
isProjectBoard
:
boardType
===
'
project
'
,
isGroupBoard
:
boardType
===
'
group
'
});
createComponent
({
[
queryHandler
]:
jest
.
fn
().
mockResolvedValue
(
mockedResponse
),
});
});
it
(
'
sets milestone
'
,
(
done
)
=>
{
findEditButton
().
vm
.
$emit
(
'
click
'
);
vm
.
$el
.
querySelector
(
'
.edit-link
'
).
click
();
await
waitForPromises
();
await
nextTick
();
jest
.
runOnlyPendingTimers
();
setImmediate
(()
=>
{
expect
(
queryHandler
).
toHaveBeenCalled
();
vm
.
$el
.
querySelectorAll
(
'
li a
'
)[
4
].
click
();
expect
(
notCalledHandler
).
not
.
toHaveBeenCalled
();
});
},
);
setImmediate
(()
=>
{
// "second milestone" is not expired, hence it shows up to the top.
expect
(
activeDropdownItem
(
0
)).
toBe
(
'
second milestone
'
);
expect
(
selectedText
()).
toBe
(
'
second milestone
'
);
expect
(
vm
.
board
.
milestone
).
toEqual
(
milestone2
);
done
();
});
});
});
});
});
});
locale/gitlab.pot
View file @
d88a3e5a
...
@@ -3891,6 +3891,9 @@ msgstr ""
...
@@ -3891,6 +3891,9 @@ msgstr ""
msgid "Any Author"
msgid "Any Author"
msgstr ""
msgstr ""
msgid "Any Milestone"
msgstr ""
msgid "Any branch"
msgid "Any branch"
msgstr ""
msgstr ""
...
@@ -5309,9 +5312,15 @@ msgstr ""
...
@@ -5309,9 +5312,15 @@ msgstr ""
msgid "BoardNewIssue|Select a project"
msgid "BoardNewIssue|Select a project"
msgstr ""
msgstr ""
msgid "BoardScope|An error occurred while getting milestones, please try again."
msgstr ""
msgid "BoardScope|An error occurred while searching for users, please try again."
msgid "BoardScope|An error occurred while searching for users, please try again."
msgstr ""
msgstr ""
msgid "BoardScope|Any Milestone"
msgstr ""
msgid "BoardScope|Any assignee"
msgid "BoardScope|Any assignee"
msgstr ""
msgstr ""
...
@@ -5321,12 +5330,30 @@ msgstr ""
...
@@ -5321,12 +5330,30 @@ msgstr ""
msgid "BoardScope|Edit"
msgid "BoardScope|Edit"
msgstr ""
msgstr ""
msgid "BoardScope|Milestone"
msgstr ""
msgid "BoardScope|No matching results"
msgid "BoardScope|No matching results"
msgstr ""
msgstr ""
msgid "BoardScope|No milestone"
msgstr ""
msgid "BoardScope|Search milestones"
msgstr ""
msgid "BoardScope|Select assignee"
msgid "BoardScope|Select assignee"
msgstr ""
msgstr ""
msgid "BoardScope|Select milestone"
msgstr ""
msgid "BoardScope|Started"
msgstr ""
msgid "BoardScope|Upcoming"
msgstr ""
msgid "Boards"
msgid "Boards"
msgstr ""
msgstr ""
...
@@ -22181,6 +22208,9 @@ msgstr ""
...
@@ -22181,6 +22208,9 @@ msgstr ""
msgid "No Matching Results"
msgid "No Matching Results"
msgstr ""
msgstr ""
msgid "No Milestone"
msgstr ""
msgid "No Scopes"
msgid "No Scopes"
msgstr ""
msgstr ""
...
...
spec/frontend/graphql_shared/utils_spec.js
View file @
d88a3e5a
import
{
import
{
isGid
,
getIdFromGraphQLId
,
getIdFromGraphQLId
,
convertToGraphQLId
,
convertToGraphQLId
,
convertToGraphQLIds
,
convertToGraphQLIds
,
...
@@ -10,6 +11,16 @@ const mockType = 'Group';
...
@@ -10,6 +11,16 @@ const mockType = 'Group';
const
mockId
=
12
;
const
mockId
=
12
;
const
mockGid
=
`gid://gitlab/Group/12`
;
const
mockGid
=
`gid://gitlab/Group/12`
;
describe
(
'
isGid
'
,
()
=>
{
it
(
'
returns true if passed id is gid
'
,
()
=>
{
expect
(
isGid
(
mockGid
)).
toBe
(
true
);
});
it
(
'
returns false if passed id is not gid
'
,
()
=>
{
expect
(
isGid
(
mockId
)).
toBe
(
false
);
});
});
describe
(
'
getIdFromGraphQLId
'
,
()
=>
{
describe
(
'
getIdFromGraphQLId
'
,
()
=>
{
[
[
{
{
...
@@ -67,6 +78,10 @@ describe('convertToGraphQLId', () => {
...
@@ -67,6 +78,10 @@ describe('convertToGraphQLId', () => {
`
(
'
throws TypeError with "$message" if a param is missing
'
,
({
type
,
id
,
message
})
=>
{
`
(
'
throws TypeError with "$message" if a param is missing
'
,
({
type
,
id
,
message
})
=>
{
expect
(()
=>
convertToGraphQLId
(
type
,
id
)).
toThrow
(
new
TypeError
(
message
));
expect
(()
=>
convertToGraphQLId
(
type
,
id
)).
toThrow
(
new
TypeError
(
message
));
});
});
it
(
'
returns id as is if it follows the gid format
'
,
()
=>
{
expect
(
convertToGraphQLId
(
mockType
,
mockGid
)).
toStrictEqual
(
mockGid
);
});
});
});
describe
(
'
convertToGraphQLIds
'
,
()
=>
{
describe
(
'
convertToGraphQLIds
'
,
()
=>
{
...
...
spec/frontend/sidebar/mock_data.js
View file @
d88a3e5a
...
@@ -585,6 +585,19 @@ export const mockProjectMilestonesResponse = {
...
@@ -585,6 +585,19 @@ export const mockProjectMilestonesResponse = {
},
},
};
};
export
const
mockGroupMilestonesResponse
=
{
data
:
{
workspace
:
{
id
:
'
gid://gitlab/Group/1
'
,
attributes
:
{
nodes
:
[
mockMilestone1
,
mockMilestone2
],
},
__typename
:
'
MilestoneConnection
'
,
},
__typename
:
'
Group
'
,
},
};
export
const
noCurrentMilestoneResponse
=
{
export
const
noCurrentMilestoneResponse
=
{
data
:
{
data
:
{
workspace
:
{
workspace
:
{
...
...
spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
0 → 100644
View file @
d88a3e5a
import
{
GlDropdown
,
GlSearchBoxByType
,
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
DropdownWidget
from
'
~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
'
;
describe
(
'
DropdownWidget component
'
,
()
=>
{
let
wrapper
;
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findDropdownItems
=
()
=>
wrapper
.
findAll
(
GlDropdownItem
);
const
findSearch
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
const
createComponent
=
({
props
=
{}
}
=
{})
=>
{
wrapper
=
shallowMount
(
DropdownWidget
,
{
propsData
:
{
...
props
,
options
:
[
{
id
:
'
1
'
,
title
:
'
Option 1
'
,
},
{
id
:
'
2
'
,
title
:
'
Option 2
'
,
},
],
},
stubs
:
{
GlDropdown
,
},
});
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
jest
.
spyOn
(
wrapper
.
vm
,
'
showDropdown
'
).
mockImplementation
();
};
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
(
'
passes default selectText prop to dropdown
'
,
()
=>
{
expect
(
findDropdown
().
props
(
'
text
'
)).
toBe
(
'
Select
'
);
});
describe
(
'
when dropdown is open
'
,
()
=>
{
beforeEach
(
async
()
=>
{
findDropdown
().
vm
.
$emit
(
'
show
'
);
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
emits search event when typing in search box
'
,
()
=>
{
const
searchTerm
=
'
searchTerm
'
;
findSearch
().
vm
.
$emit
(
'
input
'
,
searchTerm
);
expect
(
wrapper
.
emitted
(
'
set-search
'
)).
toEqual
([[
searchTerm
]]);
});
it
(
'
renders one selectable item per passed option
'
,
async
()
=>
{
expect
(
findDropdownItems
()).
toHaveLength
(
2
);
});
it
(
'
emits set-option event when clicking on an option
'
,
async
()
=>
{
wrapper
.
findAll
(
'
[data-testid="unselected-option"]
'
)
.
at
(
1
)
.
vm
.
$emit
(
'
click
'
,
new
Event
(
'
click
'
));
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
emitted
(
'
set-option
'
)).
toEqual
([[
wrapper
.
props
().
options
[
1
]]]);
});
});
});
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