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
5ddfe091
Commit
5ddfe091
authored
Mar 09, 2021
by
Simon Knox
Committed by
Savas Vedova
Mar 09, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Split new column form into data/display components
Makes it easier to extend for different types of list in EE
parent
cbd3726e
Changes
19
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
850 additions
and
19 deletions
+850
-19
app/assets/javascripts/boards/components/board_add_new_column.vue
...ts/javascripts/boards/components/board_add_new_column.vue
+9
-5
app/assets/javascripts/boards/components/board_add_new_column_form.vue
...vascripts/boards/components/board_add_new_column_form.vue
+13
-4
app/assets/javascripts/boards/components/board_content.vue
app/assets/javascripts/boards/components/board_content.vue
+1
-1
ee/app/assets/javascripts/boards/components/board_add_new_column.vue
...ts/javascripts/boards/components/board_add_new_column.vue
+303
-0
ee/app/assets/javascripts/boards/components/epics_swimlanes.vue
.../assets/javascripts/boards/components/epics_swimlanes.vue
+4
-5
ee/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
...ripts/boards/graphql/group_board_milestones.query.graphql
+11
-0
ee/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
...pts/boards/graphql/project_board_milestones.query.graphql
+10
-0
ee/app/assets/javascripts/boards/stores/actions.js
ee/app/assets/javascripts/boards/stores/actions.js
+46
-0
ee/app/assets/javascripts/boards/stores/getters.js
ee/app/assets/javascripts/boards/stores/getters.js
+28
-1
ee/app/assets/javascripts/boards/stores/mutation_types.js
ee/app/assets/javascripts/boards/stores/mutation_types.js
+3
-0
ee/app/assets/javascripts/boards/stores/mutations.js
ee/app/assets/javascripts/boards/stores/mutations.js
+15
-1
ee/app/assets/javascripts/boards/stores/state.js
ee/app/assets/javascripts/boards/stores/state.js
+2
-0
ee/spec/features/boards/user_adds_lists_to_board_spec.rb
ee/spec/features/boards/user_adds_lists_to_board_spec.rb
+78
-0
ee/spec/frontend/boards/components/board_add_new_column_spec.js
...c/frontend/boards/components/board_add_new_column_spec.js
+155
-0
ee/spec/frontend/boards/components/epics_swimlanes_spec.js
ee/spec/frontend/boards/components/epics_swimlanes_spec.js
+1
-1
ee/spec/frontend/boards/mock_data.js
ee/spec/frontend/boards/mock_data.js
+40
-0
ee/spec/frontend/boards/stores/actions_spec.js
ee/spec/frontend/boards/stores/actions_spec.js
+87
-1
ee/spec/frontend/boards/stores/getters_spec.js
ee/spec/frontend/boards/stores/getters_spec.js
+32
-0
locale/gitlab.pot
locale/gitlab.pot
+12
-0
No files found.
app/assets/javascripts/boards/components/board_add_new_column.vue
View file @
5ddfe091
...
...
@@ -30,7 +30,7 @@ export default {
},
computed
:
{
...
mapState
([
'
labels
'
,
'
labelsLoading
'
]),
...
mapGetters
([
'
getListByLabelId
'
,
'
shouldUseGraphQL
'
,
'
isEpicBoard
'
]),
...
mapGetters
([
'
getListByLabelId
'
,
'
shouldUseGraphQL
'
]),
selectedLabel
()
{
if
(
!
this
.
selectedId
)
{
return
null
;
...
...
@@ -47,7 +47,7 @@ export default {
methods
:
{
...
mapActions
([
'
createList
'
,
'
fetchLabels
'
,
'
highlightList
'
,
'
setAddColumnFormVisibility
'
]),
highlight
(
listId
)
{
if
(
this
.
shouldUseGraphQL
||
this
.
isEpicBoard
)
{
if
(
this
.
shouldUseGraphQL
)
{
this
.
highlightList
(
listId
);
}
else
{
const
list
=
boardsStore
.
state
.
lists
.
find
(({
id
})
=>
id
===
listId
);
...
...
@@ -70,7 +70,7 @@ export default {
return
;
}
if
(
this
.
shouldUseGraphQL
||
this
.
isEpicBoard
)
{
if
(
this
.
shouldUseGraphQL
)
{
this
.
createList
({
labelId
:
this
.
selectedId
});
}
else
{
const
listObj
=
{
...
...
@@ -118,13 +118,17 @@ export default {
</
template
>
<
template
slot=
"items"
>
<gl-form-radio-group
v-model=
"selectedId"
class=
"gl-overflow-y-auto gl-px-5 gl-pt-3"
>
<gl-form-radio-group
v-if=
"labels.length > 0"
v-model=
"selectedId"
class=
"gl-overflow-y-auto gl-px-5 gl-pt-3"
>
<label
v-for=
"label in labels"
:key=
"label.id"
class=
"gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
>
<gl-form-radio
:value=
"label.id"
class=
"gl-mb-0
gl-mr-3
"
/>
<gl-form-radio
:value=
"label.id"
class=
"gl-mb-0"
/>
<span
class=
"dropdown-label-box gl-top-0"
:style=
"
{
...
...
app/assets/javascripts/boards/components/board_add_new_column_form.vue
View file @
5ddfe091
...
...
@@ -5,10 +5,11 @@ import { __ } from '~/locale';
export
default
{
i18n
:
{
add
:
__
(
'
Add
'
),
add
:
__
(
'
Add
to board
'
),
cancel
:
__
(
'
Cancel
'
),
newList
:
__
(
'
New list
'
),
noneSelected
:
__
(
'
None
'
),
noResults
:
__
(
'
No matching results
'
),
selected
:
__
(
'
Selected
'
),
},
components
:
{
...
...
@@ -40,6 +41,11 @@ export default {
default
:
null
,
},
},
data
()
{
return
{
searchValue
:
''
,
};
},
methods
:
{
...
mapActions
([
'
setAddColumnFormVisibility
'
]),
},
...
...
@@ -83,6 +89,7 @@ export default {
>
<gl-search-box-by-type
id=
"board-available-column-entities"
v-model=
"searchValue"
debounce=
"250"
:placeholder=
"searchPlaceholder"
@
input=
"$emit('filter-items', $event)"
...
...
@@ -97,10 +104,12 @@ export default {
</gl-skeleton-loader>
</div>
<slot
v-else
name=
"items"
></slot>
<slot
v-else
name=
"items"
>
<p
class=
"gl-mx-5"
>
{{
$options
.
i18n
.
noResults
}}
</p>
</slot>
</div>
<div
class=
"gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10"
class=
"gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10
gl-rounded-bottom-left-base gl-rounded-bottom-right-base
"
>
<gl-button
data-testid=
"cancelAddNewColumn"
...
...
@@ -111,7 +120,7 @@ export default {
<gl-button
data-testid=
"addNewColumnButton"
:disabled=
"!selectedId"
variant=
"
success
"
variant=
"
confirm
"
class=
"gl-mr-4"
@
click=
"$emit('add-list')"
>
{{
$options
.
i18n
.
add
}}
</gl-button
...
...
app/assets/javascripts/boards/components/board_content.vue
View file @
5ddfe091
...
...
@@ -3,10 +3,10 @@ import { GlAlert } from '@gitlab/ui';
import
{
sortBy
}
from
'
lodash
'
;
import
Draggable
from
'
vuedraggable
'
;
import
{
mapState
,
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
BoardAddNewColumn
from
'
ee_else_ce/boards/components/board_add_new_column.vue
'
;
import
{
sortableEnd
,
sortableStart
}
from
'
~/boards/mixins/sortable_default_options
'
;
import
defaultSortableConfig
from
'
~/sortable/sortable_config
'
;
import
glFeatureFlagMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
BoardAddNewColumn
from
'
./board_add_new_column.vue
'
;
import
BoardColumn
from
'
./board_column.vue
'
;
import
BoardColumnDeprecated
from
'
./board_column_deprecated.vue
'
;
...
...
ee/app/assets/javascripts/boards/components/board_add_new_column.vue
0 → 100644
View file @
5ddfe091
<
script
>
import
{
GlFormGroup
,
GlFormRadio
,
GlFormRadioGroup
,
GlFormSelect
,
GlLabel
,
GlTooltipDirective
as
GlTooltip
,
}
from
'
@gitlab/ui
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
BoardAddNewColumnForm
from
'
~/boards/components/board_add_new_column_form.vue
'
;
import
{
ListType
}
from
'
~/boards/constants
'
;
import
boardsStore
from
'
~/boards/stores/boards_store
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
isScopedLabel
}
from
'
~/lib/utils/common_utils
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
i18n
:
{
listType
:
__
(
'
List type
'
),
labelListDescription
:
__
(
'
A label list displays issues with the selected label.
'
),
milestoneListDescription
:
__
(
'
A milestone list displays issues in the selected milestone.
'
),
selectLabel
:
__
(
'
Select label
'
),
selectMilestone
:
__
(
'
Select milestone
'
),
searchLabels
:
__
(
'
Search labels
'
),
searchMilestones
:
__
(
'
Search milestones
'
),
},
columnTypes
:
[
{
value
:
ListType
.
label
,
text
:
__
(
'
Label
'
)
},
{
value
:
ListType
.
milestone
,
text
:
__
(
'
Milestone
'
)
},
],
components
:
{
BoardAddNewColumnForm
,
GlFormGroup
,
GlFormRadio
,
GlFormRadioGroup
,
GlFormSelect
,
GlLabel
,
},
directives
:
{
GlTooltip
,
},
inject
:
[
'
scopedLabelsAvailable
'
],
data
()
{
return
{
selectedId
:
null
,
columnType
:
ListType
.
label
,
};
},
computed
:
{
...
mapState
([
'
labels
'
,
'
labelsLoading
'
,
'
milestones
'
,
'
milestonesLoading
'
]),
...
mapGetters
([
'
getListByTypeId
'
,
'
shouldUseGraphQL
'
,
'
isEpicBoard
'
]),
items
()
{
if
(
this
.
labelTypeSelected
)
{
return
this
.
labels
;
}
if
(
this
.
milestoneTypeSelected
)
{
return
this
.
milestones
;
}
return
[];
},
labelTypeSelected
()
{
return
this
.
columnType
===
ListType
.
label
;
},
milestoneTypeSelected
()
{
return
this
.
columnType
===
ListType
.
milestone
;
},
selectedLabel
()
{
if
(
!
this
.
labelTypeSelected
)
{
return
null
;
}
return
this
.
labels
.
find
(({
id
})
=>
id
===
this
.
selectedId
);
},
selectedMilestone
()
{
if
(
!
this
.
milestoneTypeSelected
)
{
return
null
;
}
return
this
.
milestones
.
find
(({
id
})
=>
id
===
this
.
selectedId
);
},
selectedItem
()
{
if
(
!
this
.
selectedId
)
{
return
null
;
}
if
(
this
.
labelTypeSelected
)
{
return
this
.
selectedLabel
;
}
if
(
this
.
milestoneTypeSelected
)
{
return
this
.
selectedMilestone
;
}
return
null
;
},
columnForSelected
()
{
if
(
!
this
.
columnType
)
{
return
false
;
}
const
key
=
`
${
this
.
columnType
}
Id`
;
return
this
.
getListByTypeId
({
[
key
]:
this
.
selectedId
,
});
},
loading
()
{
if
(
this
.
columnType
===
ListType
.
label
)
{
return
this
.
labelsLoading
;
}
if
(
this
.
columnType
===
ListType
.
milestone
)
{
return
this
.
milestonesLoading
;
}
return
false
;
},
formDescription
()
{
if
(
this
.
labelTypeSelected
)
{
return
this
.
$options
.
i18n
.
labelListDescription
;
}
if
(
this
.
milestoneTypeSelected
)
{
return
this
.
$options
.
i18n
.
milestoneListDescription
;
}
return
null
;
},
searchLabel
()
{
if
(
this
.
labelTypeSelected
)
{
return
this
.
$options
.
i18n
.
selectLabel
;
}
if
(
this
.
milestoneTypeSelected
)
{
return
this
.
$options
.
i18n
.
selectMilestone
;
}
return
null
;
},
searchPlaceholder
()
{
if
(
this
.
labelTypeSelected
)
{
return
this
.
$options
.
i18n
.
searchLabels
;
}
if
(
this
.
milestoneTypeSelected
)
{
return
this
.
$options
.
i18n
.
searchMilestones
;
}
return
null
;
},
},
created
()
{
this
.
filterItems
();
},
methods
:
{
...
mapActions
([
'
createList
'
,
'
fetchLabels
'
,
'
highlightList
'
,
'
setAddColumnFormVisibility
'
,
'
fetchMilestones
'
,
]),
highlight
(
listId
)
{
if
(
this
.
shouldUseGraphQL
||
this
.
isEpicBoard
)
{
this
.
highlightList
(
listId
);
}
else
{
const
list
=
boardsStore
.
state
.
lists
.
find
(({
id
})
=>
id
===
listId
);
list
.
highlighted
=
true
;
setTimeout
(()
=>
{
list
.
highlighted
=
false
;
},
2000
);
}
},
addList
()
{
if
(
!
this
.
selectedItem
)
{
return
;
}
this
.
setAddColumnFormVisibility
(
false
);
if
(
this
.
columnForSelected
)
{
const
listId
=
this
.
columnForSelected
.
id
;
this
.
highlight
(
listId
);
return
;
}
if
(
this
.
shouldUseGraphQL
||
this
.
isEpicBoard
)
{
// eslint-disable-next-line @gitlab/require-i18n-strings
this
.
createList
({
[
`
${
this
.
columnType
}
Id`
]:
this
.
selectedId
});
}
else
{
const
{
length
}
=
boardsStore
.
state
.
lists
;
const
position
=
this
.
hideClosed
?
length
-
1
:
length
-
2
;
const
listObj
=
{
// eslint-disable-next-line @gitlab/require-i18n-strings
[
`
${
this
.
columnType
}
Id`
]:
getIdFromGraphQLId
(
this
.
selectedId
),
title
:
this
.
selectedItem
.
title
,
position
,
list_type
:
this
.
columnType
,
};
if
(
this
.
labelTypeSelected
)
{
listObj
.
label
=
this
.
selectedLabel
;
}
else
if
(
this
.
milestoneTypeSelected
)
{
listObj
.
milestone
=
{
...
this
.
selectedMilestone
,
id
:
getIdFromGraphQLId
(
this
.
selectedMilestone
.
id
),
};
}
boardsStore
.
new
(
listObj
);
}
},
filterItems
(
searchTerm
)
{
switch
(
this
.
columnType
)
{
case
ListType
.
milestone
:
this
.
fetchMilestones
(
searchTerm
);
break
;
case
ListType
.
label
:
default
:
this
.
fetchLabels
(
searchTerm
);
}
},
showScopedLabels
(
label
)
{
return
this
.
scopedLabelsAvailable
&&
isScopedLabel
(
label
);
},
setColumnType
()
{
this
.
selectedId
=
null
;
this
.
filterItems
();
},
},
};
</
script
>
<
template
>
<board-add-new-column-form
:loading=
"loading"
:form-description=
"formDescription"
:search-label=
"searchLabel"
:search-placeholder=
"searchPlaceholder"
:selected-id=
"selectedId"
@
filter-items=
"filterItems"
@
add-list=
"addList"
>
<template
slot=
"select-list-type"
>
<gl-form-group
v-if=
"!isEpicBoard"
:label=
"$options.i18n.listType"
class=
"gl-px-5 gl-py-0 gl-mt-5"
label-for=
"list-type"
>
<gl-form-select
id=
"list-type"
v-model=
"columnType"
:options=
"$options.columnTypes"
@
change=
"setColumnType"
/>
</gl-form-group>
</
template
>
<
template
slot=
"selected"
>
<div
v-if=
"selectedLabel"
>
<gl-label
v-gl-tooltip
:title=
"selectedLabel.title"
:description=
"selectedLabel.description"
:background-color=
"selectedLabel.color"
:scoped=
"showScopedLabels(selectedLabel)"
/>
</div>
<div
v-else-if=
"selectedMilestone"
class=
"gl-text-truncate"
>
{{
selectedMilestone
.
title
}}
</div>
</
template
>
<
template
slot=
"items"
>
<gl-form-radio-group
v-if=
"items.length > 0"
v-model=
"selectedId"
class=
"gl-overflow-y-auto gl-px-5 gl-pt-3"
>
<label
v-for=
"item in items"
:key=
"item.id"
class=
"gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
>
<gl-form-radio
:value=
"item.id"
class=
"gl-mb-0"
/>
<span
v-if=
"labelTypeSelected"
class=
"dropdown-label-box gl-top-0"
:style=
"
{
backgroundColor: item.color,
}"
>
</span>
<span>
{{
item
.
title
}}
</span>
</label>
</gl-form-radio-group>
</
template
>
</board-add-new-column-form>
</template>
ee/app/assets/javascripts/boards/components/epics_swimlanes.vue
View file @
5ddfe091
...
...
@@ -2,9 +2,9 @@
import
{
GlButton
,
GlIcon
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
Draggable
from
'
vuedraggable
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
BoardAddNewColumn
from
'
ee_else_ce/boards/components/board_add_new_column.vue
'
;
import
BoardListHeader
from
'
ee_else_ce/boards/components/board_list_header.vue
'
;
import
{
isListDraggable
}
from
'
~/boards/boards_util
'
;
import
BoardAddNewColumn
from
'
~/boards/components/board_add_new_column.vue
'
;
import
{
n__
}
from
'
~/locale
'
;
import
defaultSortableConfig
from
'
~/sortable/sortable_config
'
;
import
{
DRAGGABLE_TAG
}
from
'
../constants
'
;
...
...
@@ -48,10 +48,9 @@ export default {
return
(
listId
)
=>
this
.
getUnassignedIssues
(
listId
);
},
unassignedIssuesCount
()
{
return
this
.
lists
.
reduce
(
(
total
,
list
)
=>
total
+
this
.
listsFlags
[
list
.
id
]?.
unassignedIssuesCount
||
0
,
0
,
);
return
this
.
lists
.
reduce
((
total
,
list
)
=>
{
return
total
+
(
this
.
listsFlags
[
list
.
id
]?.
unassignedIssuesCount
||
0
);
},
0
);
},
unassignedIssuesCountTooltipText
()
{
return
n__
(
`%d unassigned issue`
,
`%d unassigned issues`
,
this
.
unassignedIssuesCount
);
...
...
ee/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
0 → 100644
View file @
5ddfe091
query
GroupBoardMilestones
(
$fullPath
:
ID
!,
$searchTerm
:
String
)
{
group
(
fullPath
:
$fullPath
)
{
# TODO: add includeAncestors: https://gitlab.com/gitlab-org/gitlab/-/issues/323433
milestones
(
searchTitle
:
$searchTerm
)
{
nodes
{
id
title
}
}
}
}
ee/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
0 → 100644
View file @
5ddfe091
query
ProjectBoardMilestones
(
$fullPath
:
ID
!,
$searchTerm
:
String
)
{
project
(
fullPath
:
$fullPath
)
{
milestones
(
searchTitle
:
$searchTerm
,
includeAncestors
:
true
)
{
nodes
{
id
title
}
}
}
}
ee/app/assets/javascripts/boards/stores/actions.js
View file @
5ddfe091
...
...
@@ -33,11 +33,13 @@ import epicQuery from '../graphql/epic.query.graphql';
import
createEpicBoardListMutation
from
'
../graphql/epic_board_list_create.mutation.graphql
'
;
import
epicBoardListsQuery
from
'
../graphql/epic_board_lists.query.graphql
'
;
import
epicsSwimlanesQuery
from
'
../graphql/epics_swimlanes.query.graphql
'
;
import
groupBoardMilestonesQuery
from
'
../graphql/group_board_milestones.query.graphql
'
;
import
issueMoveListMutation
from
'
../graphql/issue_move_list.mutation.graphql
'
;
import
issueSetEpicMutation
from
'
../graphql/issue_set_epic.mutation.graphql
'
;
import
issueSetWeightMutation
from
'
../graphql/issue_set_weight.mutation.graphql
'
;
import
listUpdateLimitMetricsMutation
from
'
../graphql/list_update_limit_metrics.mutation.graphql
'
;
import
listsEpicsQuery
from
'
../graphql/lists_epics.query.graphql
'
;
import
projectBoardMilestonesQuery
from
'
../graphql/project_board_milestones.query.graphql
'
;
import
updateBoardEpicUserPreferencesMutation
from
'
../graphql/updateBoardEpicUserPreferences.mutation.graphql
'
;
import
boardsStoreEE
from
'
./boards_store_ee
'
;
...
...
@@ -557,6 +559,50 @@ export default {
.
catch
(()
=>
commit
(
types
.
RECEIVE_BOARD_LISTS_FAILURE
));
},
fetchMilestones
({
state
,
commit
},
searchTerm
)
{
commit
(
types
.
RECEIVE_MILESTONES_REQUEST
);
const
{
fullPath
,
boardType
}
=
state
;
const
variables
=
{
fullPath
,
searchTerm
,
};
let
query
;
if
(
boardType
===
BoardType
.
project
)
{
query
=
projectBoardMilestonesQuery
;
}
if
(
boardType
===
BoardType
.
group
)
{
query
=
groupBoardMilestonesQuery
;
}
if
(
!
query
)
{
// eslint-disable-next-line @gitlab/require-i18n-strings
throw
new
Error
(
'
Unknown board type
'
);
}
return
gqlClient
.
query
({
query
,
variables
,
})
.
then
(({
data
})
=>
{
const
errors
=
data
[
boardType
]?.
errors
;
const
milestones
=
data
[
boardType
]?.
milestones
.
nodes
;
if
(
errors
?.[
0
])
{
throw
new
Error
(
errors
[
0
]);
}
commit
(
types
.
RECEIVE_MILESTONES_SUCCESS
,
milestones
);
})
.
catch
((
e
)
=>
{
commit
(
types
.
RECEIVE_MILESTONES_FAILURE
);
throw
e
;
});
},
createList
:
({
getters
,
dispatch
},
{
backlog
,
labelId
,
milestoneId
,
assigneeId
})
=>
{
if
(
!
getters
.
isEpicBoard
)
{
dispatch
(
'
createIssueList
'
,
{
backlog
,
labelId
,
milestoneId
,
assigneeId
});
...
...
ee/app/assets/javascripts/boards/stores/getters.js
View file @
5ddfe091
import
{
issuableTypes
}
from
'
~/boards/constants
'
;
import
{
find
}
from
'
lodash
'
;
import
{
issuableTypes
,
ListType
}
from
'
~/boards/constants
'
;
import
gettersCE
from
'
~/boards/stores/getters
'
;
export
default
{
...
...
@@ -7,6 +8,32 @@ export default {
isSwimlanesOn
:
(
state
)
=>
{
return
Boolean
(
gon
?.
features
?.
swimlanes
&&
state
.
isShowingEpicsSwimlanes
);
},
getListByTypeId
:
(
state
)
=>
({
assigneeId
,
labelId
,
milestoneId
})
=>
{
if
(
assigneeId
)
{
return
find
(
state
.
boardLists
,
(
l
)
=>
l
.
listType
===
ListType
.
assignee
&&
l
.
assignee
?.
id
===
assigneeId
,
);
}
if
(
labelId
)
{
return
find
(
state
.
boardLists
,
(
l
)
=>
l
.
listType
===
ListType
.
label
&&
l
.
label
?.
id
===
labelId
,
);
}
if
(
milestoneId
)
{
return
find
(
state
.
boardLists
,
(
l
)
=>
l
.
listType
===
ListType
.
milestone
&&
l
.
milestone
?.
id
===
milestoneId
,
);
}
return
null
;
},
getIssuesByEpic
:
(
state
,
getters
)
=>
(
listId
,
epicId
)
=>
{
return
getters
.
getBoardItemsByList
(
listId
)
...
...
ee/app/assets/javascripts/boards/stores/mutation_types.js
View file @
5ddfe091
...
...
@@ -33,3 +33,6 @@ export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
export
const
MOVE_ISSUE_FAILURE
=
'
MOVE_ISSUE_FAILURE
'
;
export
const
CREATE_LIST_FAILURE
=
'
CREATE_LIST_FAILURE
'
;
export
const
SET_BOARD_EPIC_USER_PREFERENCES
=
'
SET_BOARD_EPIC_USER_PREFERENCES
'
;
export
const
RECEIVE_MILESTONES_REQUEST
=
'
RECEIVE_MILESTONES_REQUEST
'
;
export
const
RECEIVE_MILESTONES_SUCCESS
=
'
RECEIVE_MILESTONES_SUCCESS
'
;
export
const
RECEIVE_MILESTONES_FAILURE
=
'
RECEIVE_MILESTONES_FAILURE
'
;
ee/app/assets/javascripts/boards/stores/mutations.js
View file @
5ddfe091
...
...
@@ -3,7 +3,7 @@ import Vue from 'vue';
import
{
moveIssueListHelper
}
from
'
~/boards/boards_util
'
;
import
{
issuableTypes
}
from
'
~/boards/constants
'
;
import
mutationsCE
,
{
addIssueToList
,
removeIssueFromList
}
from
'
~/boards/stores/mutations
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
ErrorMessages
}
from
'
../constants
'
;
import
*
as
mutationTypes
from
'
./mutation_types
'
;
...
...
@@ -189,4 +189,18 @@ export default {
Vue
.
set
(
epic
,
'
userPreferences
'
,
userPreferences
);
}
},
[
mutationTypes
.
RECEIVE_MILESTONES_REQUEST
](
state
)
{
state
.
milestonesLoading
=
true
;
},
[
mutationTypes
.
RECEIVE_MILESTONES_SUCCESS
](
state
,
milestones
)
{
state
.
milestones
=
milestones
;
state
.
milestonesLoading
=
false
;
},
[
mutationTypes
.
RECEIVE_MILESTONES_FAILURE
](
state
)
{
state
.
milestonesLoading
=
false
;
state
.
error
=
__
(
'
Failed to load milestones.
'
);
},
};
ee/app/assets/javascripts/boards/stores/state.js
View file @
5ddfe091
...
...
@@ -12,4 +12,6 @@ export default () => ({
epicsCacheById
:
{},
epicFetchInProgress
:
false
,
epicsFlags
:
{},
milestones
:
[],
milestonesLoading
:
false
,
});
ee/spec/features/boards/user_adds_lists_to_board_spec.rb
0 → 100644
View file @
5ddfe091
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
'User adds milestone lists'
,
:js
do
using
RSpec
::
Parameterized
::
TableSyntax
let_it_be
(
:group
)
{
create
(
:group
,
:nested
)
}
let_it_be
(
:project
)
{
create
(
:project
,
:public
,
namespace:
group
)
}
let_it_be
(
:group_board
)
{
create
(
:board
,
group:
group
)
}
let_it_be
(
:project_board
)
{
create
(
:board
,
project:
project
)
}
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:milestone
)
{
create
(
:milestone
,
group:
group
)
}
let_it_be
(
:group_backlog_list
)
{
create
(
:backlog_list
,
board:
group_board
)
}
let_it_be
(
:issue
)
{
create
(
:issue
,
project:
project
,
milestone:
milestone
)
}
before_all
do
project
.
add_maintainer
(
user
)
group
.
add_owner
(
user
)
end
where
(
:board_type
,
:graphql_board_lists_enabled
)
do
:project
|
true
:project
|
false
:group
|
true
:group
|
false
end
with_them
do
before
do
stub_licensed_features
(
board_milestone_lists:
true
)
sign_in
(
user
)
set_cookie
(
'sidebar_collapsed'
,
'true'
)
stub_feature_flags
(
graphql_board_lists:
graphql_board_lists_enabled
,
board_new_list:
true
)
if
board_type
==
:project
visit
project_board_path
(
project
,
project_board
)
elsif
board_type
==
:group
visit
group_board_path
(
group
,
group_board
)
end
wait_for_all_requests
end
it
'creates milestone column'
do
click_button
button_text
wait_for_all_requests
select
(
'Milestone'
,
from:
'List type'
)
add_milestone_list
(
milestone
)
wait_for_all_requests
expect
(
page
).
to
have_selector
(
'.board'
,
text:
milestone
.
title
)
expect
(
find
(
'.board:nth-child(2) .board-card'
)).
to
have_content
(
issue
.
title
)
end
end
def
add_milestone_list
(
milestone
)
page
.
within
(
'.board-add-new-list'
)
do
find
(
'label'
,
text:
milestone
.
title
).
click
click_button
'Add'
end
end
def
button_text
'Create list'
end
end
ee/spec/frontend/boards/components/board_add_new_column_spec.js
0 → 100644
View file @
5ddfe091
import
{
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
,
{
nextTick
}
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
BoardAddNewColumn
from
'
ee/boards/components/board_add_new_column.vue
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
BoardAddNewColumnForm
from
'
~/boards/components/board_add_new_column_form.vue
'
;
import
defaultState
from
'
~/boards/stores/state
'
;
import
{
mockLists
}
from
'
../mock_data
'
;
const
mockLabelList
=
mockLists
[
1
];
Vue
.
use
(
Vuex
);
describe
(
'
Board card layout
'
,
()
=>
{
let
wrapper
;
let
shouldUseGraphQL
;
const
createStore
=
({
actions
=
{},
getters
=
{},
state
=
{}
}
=
{})
=>
{
return
new
Vuex
.
Store
({
state
:
{
...
defaultState
,
...
state
,
},
actions
,
getters
,
});
};
const
mountComponent
=
({
selectedId
,
labels
=
[],
getListByTypeId
=
jest
.
fn
(),
actions
=
{},
}
=
{})
=>
{
wrapper
=
extendedWrapper
(
shallowMount
(
BoardAddNewColumn
,
{
stubs
:
{
BoardAddNewColumnForm
,
},
data
()
{
return
{
selectedId
,
};
},
store
:
createStore
({
actions
:
{
fetchLabels
:
jest
.
fn
(),
setAddColumnFormVisibility
:
jest
.
fn
(),
...
actions
,
},
getters
:
{
shouldUseGraphQL
:
()
=>
shouldUseGraphQL
,
getListByTypeId
:
()
=>
getListByTypeId
,
isEpicBoard
:
()
=>
false
,
},
state
:
{
labels
,
labelsLoading
:
false
,
},
}),
provide
:
{
scopedLabelsAvailable
:
true
,
},
}),
);
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
const
formTitle
=
()
=>
wrapper
.
findByTestId
(
'
board-add-column-form-title
'
).
text
();
const
findSearchInput
=
()
=>
wrapper
.
find
(
GlSearchBoxByType
);
const
cancelButton
=
()
=>
wrapper
.
findByTestId
(
'
cancelAddNewColumn
'
);
const
submitButton
=
()
=>
wrapper
.
findByTestId
(
'
addNewColumnButton
'
);
beforeEach
(()
=>
{
shouldUseGraphQL
=
true
;
});
it
(
'
shows form title & search input
'
,
()
=>
{
mountComponent
();
expect
(
formTitle
()).
toEqual
(
BoardAddNewColumnForm
.
i18n
.
newList
);
expect
(
findSearchInput
().
exists
()).
toBe
(
true
);
});
it
(
'
clicking cancel hides the form
'
,
()
=>
{
const
setAddColumnFormVisibility
=
jest
.
fn
();
mountComponent
({
actions
:
{
setAddColumnFormVisibility
,
},
});
cancelButton
().
vm
.
$emit
(
'
click
'
);
expect
(
setAddColumnFormVisibility
).
toHaveBeenCalledWith
(
expect
.
anything
(),
false
);
});
describe
(
'
Add list button
'
,
()
=>
{
it
(
'
is disabled if no item is selected
'
,
()
=>
{
mountComponent
();
expect
(
submitButton
().
props
(
'
disabled
'
)).
toBe
(
true
);
});
it
(
'
adds a new list on click
'
,
async
()
=>
{
const
labelId
=
mockLabelList
.
label
.
id
;
const
highlightList
=
jest
.
fn
();
const
createList
=
jest
.
fn
();
mountComponent
({
labels
:
[
mockLabelList
.
label
],
selectedId
:
labelId
,
actions
:
{
createList
,
highlightList
,
},
});
await
nextTick
();
submitButton
().
vm
.
$emit
(
'
click
'
);
expect
(
highlightList
).
not
.
toHaveBeenCalled
();
expect
(
createList
).
toHaveBeenCalledWith
(
expect
.
anything
(),
{
labelId
});
});
it
(
'
highlights existing list if trying to re-add
'
,
async
()
=>
{
const
getListByTypeId
=
jest
.
fn
().
mockReturnValue
(
mockLabelList
);
const
highlightList
=
jest
.
fn
();
const
createList
=
jest
.
fn
();
mountComponent
({
labels
:
[
mockLabelList
.
label
],
selectedId
:
mockLabelList
.
label
.
id
,
getListByTypeId
,
actions
:
{
createList
,
highlightList
,
},
});
await
nextTick
();
submitButton
().
vm
.
$emit
(
'
click
'
);
expect
(
highlightList
).
toHaveBeenCalledWith
(
expect
.
anything
(),
mockLabelList
.
id
);
expect
(
createList
).
not
.
toHaveBeenCalled
();
});
});
});
ee/spec/frontend/boards/components/epics_swimlanes_spec.js
View file @
5ddfe091
...
...
@@ -86,7 +86,7 @@ describe('EpicsSwimlanes', () => {
});
it
(
'
displays BoardListHeader components for lists
'
,
()
=>
{
expect
(
wrapper
.
findAll
(
BoardListHeader
)).
toHaveLength
(
2
);
expect
(
wrapper
.
findAll
(
BoardListHeader
)).
toHaveLength
(
4
);
});
it
(
'
displays EpicLane components for epic
'
,
()
=>
{
...
...
ee/spec/frontend/boards/mock_data.js
View file @
5ddfe091
...
...
@@ -36,6 +36,35 @@ export const mockLists = [
milestone
:
null
,
preset
:
false
,
},
{
id
:
'
gid://gitlab/List/3
'
,
title
:
'
Assignee list
'
,
position
:
0
,
listType
:
'
assignee
'
,
collapsed
:
false
,
label
:
null
,
maxIssueCount
:
0
,
assignee
:
{
id
:
'
gid://gitlab/
'
,
},
milestone
:
null
,
preset
:
false
,
},
{
id
:
'
gid://gitlab/List/4
'
,
title
:
'
Milestone list
'
,
position
:
0
,
listType
:
'
milestone
'
,
collapsed
:
false
,
label
:
null
,
maxIssueCount
:
0
,
assignee
:
null
,
milestone
:
{
id
:
'
gid://gitlab/Milestone/1
'
,
title
:
'
A milestone
'
,
},
preset
:
false
,
},
];
export
const
mockListsWithModel
=
mockLists
.
map
((
listMock
)
=>
...
...
@@ -57,6 +86,17 @@ const assignees = [
},
];
export
const
mockMilestones
=
[
{
id
:
'
gid://gitlab/Milestone/1
'
,
title
:
'
Milestone 1
'
,
},
{
id
:
'
gid://gitlab/Milestone/2
'
,
title
:
'
Milestone 2
'
,
},
];
const
labels
=
[
{
id
:
'
gid://gitlab/GroupLabel/5
'
,
...
...
ee/spec/frontend/boards/stores/actions_spec.js
View file @
5ddfe091
import
axios
from
'
axios
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
{
GroupByParamType
}
from
'
ee/boards/constants
'
;
import
actions
,
{
gqlClient
}
from
'
ee/boards/stores/actions
'
;
import
boardsStoreEE
from
'
ee/boards/stores/boards_store_ee
'
;
import
*
as
types
from
'
ee/boards/stores/mutation_types
'
;
import
mutations
from
'
ee/boards/stores/mutations
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
{
formatListIssues
,
formatBoardLists
}
from
'
~/boards/boards_util
'
;
...
...
@@ -11,7 +14,9 @@ import { issuableTypes } from '~/boards/constants';
import
*
as
typesCE
from
'
~/boards/stores/mutation_types
'
;
import
*
as
commonUtils
from
'
~/lib/utils/common_utils
'
;
import
{
mergeUrlParams
,
removeParams
}
from
'
~/lib/utils/url_utility
'
;
import
{
mockLists
,
mockIssue
,
mockIssue2
,
mockEpic
,
rawIssue
}
from
'
../mock_data
'
;
import
{
mockLists
,
mockIssue
,
mockIssue2
,
mockEpic
,
rawIssue
,
mockMilestones
}
from
'
../mock_data
'
;
Vue
.
use
(
Vuex
);
const
expectNotImplemented
=
(
action
)
=>
{
it
(
'
is not implemented
'
,
()
=>
{
...
...
@@ -1059,3 +1064,84 @@ describe('moveIssue', () => {
});
});
});
describe
(
'
fetchMilestones
'
,
()
=>
{
const
queryResponse
=
{
data
:
{
project
:
{
milestones
:
{
nodes
:
mockMilestones
,
},
},
},
};
const
queryErrors
=
{
data
:
{
project
:
{
errors
:
[
'
You cannot view these milestones
'
],
milestones
:
{},
},
},
};
function
createStore
({
state
=
{
boardType
:
'
project
'
,
fullPath
:
'
gitlab-org/gitlab
'
,
milestones
:
[],
milestonesLoading
:
false
,
},
}
=
{})
{
return
new
Vuex
.
Store
({
state
,
mutations
,
});
}
it
(
'
throws error if state.boardType is not group or project
'
,
()
=>
{
const
store
=
createStore
({
state
:
{
boardType
:
'
invalid
'
,
},
});
expect
(()
=>
actions
.
fetchMilestones
(
store
)).
toThrow
(
new
Error
(
'
Unknown board type
'
));
});
it
(
'
sets milestonesLoading to true
'
,
async
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
query
'
).
mockResolvedValue
(
queryResponse
);
const
store
=
createStore
();
actions
.
fetchMilestones
(
store
);
expect
(
store
.
state
.
milestonesLoading
).
toBe
(
true
);
});
describe
(
'
success
'
,
()
=>
{
it
(
'
sets state.milestones from query result
'
,
async
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
query
'
).
mockResolvedValue
(
queryResponse
);
const
store
=
createStore
();
await
actions
.
fetchMilestones
(
store
);
expect
(
store
.
state
.
milestonesLoading
).
toBe
(
false
);
expect
(
store
.
state
.
milestones
).
toBe
(
mockMilestones
);
});
});
describe
(
'
failure
'
,
()
=>
{
it
(
'
sets state.milestones from query result
'
,
async
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
query
'
).
mockResolvedValue
(
queryErrors
);
const
store
=
createStore
();
await
expect
(
actions
.
fetchMilestones
(
store
)).
rejects
.
toThrow
();
expect
(
store
.
state
.
milestonesLoading
).
toBe
(
false
);
expect
(
store
.
state
.
error
).
toBe
(
'
Failed to load milestones.
'
);
});
});
});
ee/spec/frontend/boards/stores/getters_spec.js
View file @
5ddfe091
...
...
@@ -7,6 +7,7 @@ import {
mockIssuesByListId
,
mockEpics
,
issues
,
mockLists
,
}
from
'
../mock_data
'
;
describe
(
'
EE Boards Store Getters
'
,
()
=>
{
...
...
@@ -90,4 +91,35 @@ describe('EE Boards Store Getters', () => {
).
toEqual
([
mockIssue3
,
mockIssue4
]);
});
});
describe
(
'
getListByTypeId
'
,
()
=>
{
const
[,
labelList
,
assigneeList
,
milestoneList
]
=
mockLists
;
it
(
'
returns label list by labelId
'
,
()
=>
{
const
labelId
=
labelList
.
label
.
id
;
expect
(
getters
.
getListByTypeId
({
boardLists
:
mockLists
})({
labelId
})).
toEqual
(
labelList
);
});
it
(
'
returns assignee list by assigneeId
'
,
()
=>
{
const
assigneeId
=
assigneeList
.
assignee
.
id
;
expect
(
getters
.
getListByTypeId
({
boardLists
:
mockLists
})({
assigneeId
})).
toEqual
(
assigneeList
,
);
});
it
(
'
returns milestone list by milestoneId
'
,
()
=>
{
const
milestoneId
=
milestoneList
.
milestone
.
id
;
expect
(
getters
.
getListByTypeId
({
boardLists
:
mockLists
})({
milestoneId
})).
toEqual
(
milestoneList
,
);
});
it
(
'
returns nothing if not results
'
,
()
=>
{
expect
(
getters
.
getListByTypeId
({
boardLists
:
mockLists
})({
labelId
:
'
not found
'
}),
).
toBeUndefined
();
});
});
});
locale/gitlab.pot
View file @
5ddfe091
...
...
@@ -1348,6 +1348,9 @@ msgstr ""
msgid "A merge request hasn't yet been merged"
msgstr ""
msgid "A milestone list displays issues in the selected milestone."
msgstr ""
msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details"
msgstr ""
...
...
@@ -1929,6 +1932,9 @@ msgstr ""
msgid "Add to Slack"
msgstr ""
msgid "Add to board"
msgstr ""
msgid "Add to epic"
msgstr ""
...
...
@@ -12587,6 +12593,9 @@ msgstr ""
msgid "Failed to load labels. Please try again."
msgstr ""
msgid "Failed to load milestones."
msgstr ""
msgid "Failed to load milestones. Please try again."
msgstr ""
...
...
@@ -18170,6 +18179,9 @@ msgstr ""
msgid "List the merge requests that must be merged before this one."
msgstr ""
msgid "List type"
msgstr ""
msgid "List view"
msgstr ""
...
...
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