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
21dac779
Commit
21dac779
authored
Jun 11, 2020
by
Florie Guibert
Committed by
Kushal Pandya
Jun 11, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Swimlanes column headers
- Move board header out of board column for reusability
parent
59e5a8ef
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
646 additions
and
385 deletions
+646
-385
app/assets/javascripts/boards/components/board_column.vue
app/assets/javascripts/boards/components/board_column.vue
+15
-239
app/assets/javascripts/boards/components/board_content.vue
app/assets/javascripts/boards/components/board_content.vue
+25
-10
app/assets/javascripts/boards/components/board_delete.js
app/assets/javascripts/boards/components/board_delete.js
+7
-0
app/assets/javascripts/boards/components/board_list.vue
app/assets/javascripts/boards/components/board_list.vue
+2
-2
app/assets/javascripts/boards/components/board_list_header.vue
...ssets/javascripts/boards/components/board_list_header.vue
+296
-0
app/assets/javascripts/boards/components/board_new_issue.vue
app/assets/javascripts/boards/components/board_new_issue.vue
+1
-1
app/assets/stylesheets/pages/boards.scss
app/assets/stylesheets/pages/boards.scss
+6
-0
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+1
-1
ee/app/assets/javascripts/boards/components/board_column.vue
ee/app/assets/javascripts/boards/components/board_column.vue
+0
-45
ee/app/assets/javascripts/boards/components/board_list_header.vue
...ssets/javascripts/boards/components/board_list_header.vue
+52
-0
ee/app/assets/javascripts/boards/components/epics_swimlanes.vue
.../assets/javascripts/boards/components/epics_swimlanes.vue
+66
-0
ee/spec/frontend/boards/components/board_list_header_spec.js
ee/spec/frontend/boards/components/board_list_header_spec.js
+3
-3
spec/frontend/boards/board_list_spec.js
spec/frontend/boards/board_list_spec.js
+1
-1
spec/frontend/boards/components/board_column_spec.js
spec/frontend/boards/components/board_column_spec.js
+5
-83
spec/frontend/boards/components/board_list_header_spec.js
spec/frontend/boards/components/board_list_header_spec.js
+166
-0
No files found.
app/assets/javascripts/boards/components/board_column.vue
View file @
21dac779
<
script
>
import
$
from
'
jquery
'
;
import
Sortable
from
'
sortablejs
'
;
import
{
GlButtonGroup
,
GlDeprecatedButton
,
GlLabel
,
GlTooltip
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
isWipLimitsOn
from
'
ee_else_ce/boards/mixins/is_wip_limits
'
;
import
{
s__
,
__
,
sprintf
}
from
'
~/locale
'
;
import
Tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
EmptyComponent
from
'
~/vue_shared/components/empty_component
'
;
import
AccessorUtilities
from
'
../../lib/utils/accessor
'
;
import
BoardBlankState
from
'
./board_blank_state.vue
'
;
import
Board
Delete
from
'
./board_delet
e
'
;
import
Board
ListHeader
from
'
ee_else_ce/boards/components/board_list_header.vu
e
'
;
import
BoardList
from
'
./board_list.vue
'
;
import
IssueCount
from
'
./issue_count.vue
'
;
import
boardsStore
from
'
../stores/boards_store
'
;
import
eventHub
from
'
../eventhub
'
;
import
{
getBoardSortableDefaultOptions
,
sortableEnd
}
from
'
../mixins/sortable_default_options
'
;
import
{
ListType
}
from
'
../constants
'
;
import
{
isScopedLabel
}
from
'
~/lib/utils/common_utils
'
;
export
default
{
components
:
{
BoardPromotionState
:
EmptyComponent
,
BoardBlankState
,
Board
Delete
,
Board
ListHeader
,
BoardList
,
GlButtonGroup
,
IssueCount
,
GlDeprecatedButton
,
GlLabel
,
GlTooltip
,
GlIcon
,
},
directives
:
{
Tooltip
,
...
...
@@ -70,42 +59,9 @@ export default {
return
{
detailIssue
:
boardsStore
.
detail
,
filter
:
boardsStore
.
filter
,
weightFeatureAvailable
:
false
,
};
},
computed
:
{
isLoggedIn
()
{
return
Boolean
(
gon
.
current_user_id
);
},
showListHeaderButton
()
{
return
(
!
this
.
disabled
&&
this
.
list
.
type
!==
ListType
.
closed
&&
this
.
list
.
type
!==
ListType
.
blank
&&
this
.
list
.
type
!==
ListType
.
promotion
);
},
issuesTooltip
()
{
const
{
issuesSize
}
=
this
.
list
;
return
sprintf
(
__
(
'
%{issuesSize} issues
'
),
{
issuesSize
});
},
// Only needed to make karma pass.
weightCountToolTip
()
{},
// eslint-disable-line vue/return-in-computed-property
caretTooltip
()
{
return
this
.
list
.
isExpanded
?
s__
(
'
Boards|Collapse
'
)
:
s__
(
'
Boards|Expand
'
);
},
isNewIssueShown
()
{
return
this
.
list
.
type
===
ListType
.
backlog
||
this
.
showListHeaderButton
;
},
isSettingsShown
()
{
return
(
this
.
list
.
type
!==
ListType
.
backlog
&&
this
.
showListHeaderButton
&&
this
.
list
.
isExpanded
&&
this
.
isWipLimitsOn
);
},
showBoardListAndBoardInfo
()
{
return
this
.
list
.
type
!==
ListType
.
blank
&&
this
.
list
.
type
!==
ListType
.
promotion
;
},
...
...
@@ -151,41 +107,9 @@ export default {
Sortable
.
create
(
this
.
$el
.
parentNode
,
sortableOptions
);
},
created
()
{
if
(
this
.
list
.
isExpandable
&&
AccessorUtilities
.
isLocalStorageAccessSafe
()
&&
!
this
.
isLoggedIn
)
{
const
isCollapsed
=
localStorage
.
getItem
(
`
${
this
.
uniqueKey
}
.expanded`
)
===
'
false
'
;
this
.
list
.
isExpanded
=
!
isCollapsed
;
}
},
methods
:
{
showScopedLabels
(
label
)
{
return
boardsStore
.
scopedLabels
.
enabled
&&
isScopedLabel
(
label
);
},
showNewIssueForm
()
{
this
.
$refs
[
'
board-list
'
].
showIssueForm
=
!
this
.
$refs
[
'
board-list
'
].
showIssueForm
;
},
toggleExpanded
()
{
if
(
this
.
list
.
isExpandable
)
{
this
.
list
.
isExpanded
=
!
this
.
list
.
isExpanded
;
if
(
AccessorUtilities
.
isLocalStorageAccessSafe
()
&&
!
this
.
isLoggedIn
)
{
localStorage
.
setItem
(
`
${
this
.
uniqueKey
}
.expanded`
,
this
.
list
.
isExpanded
);
}
if
(
this
.
isLoggedIn
)
{
this
.
list
.
update
();
}
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
$
(
'
.tooltip
'
).
tooltip
(
'
hide
'
);
}
showListNewIssueForm
(
listId
)
{
eventHub
.
$emit
(
'
showForm
'
,
listId
);
},
},
};
...
...
@@ -200,166 +124,18 @@ export default {
'board-type-assignee': list.type === 'assignee',
}"
:data-id="list.id"
class="board
h-100 px-2 align-top ws
-normal"
class="board
gl-h-full gl-px-3 gl-vertical-align-top gl-white-space
-normal"
data-qa-selector="board_list"
>
<div
class=
"board-inner d-flex flex-column position-relative h-100 rounded"
>
<header
:class=
"
{
'has-border': list.label
&&
list.label.color,
'position-relative': list.isExpanded,
'position-absolute position-top-0 position-left-0 w-100 h-100': !list.isExpanded,
}"
:style="{ borderTopColor: list.label
&&
list.label.color ? list.label.color : null }"
class="board-header"
data-qa-selector="board_list_header"
>
<h3
:class=
"
{
'user-can-drag': !disabled
&&
!list.preset,
'border-bottom-0': !list.isExpanded,
}"
class="board-title m-0 d-flex js-board-handle"
>
<div
v-if=
"list.isExpandable"
v-tooltip=
""
:aria-label=
"caretTooltip"
:title=
"caretTooltip"
aria-hidden=
"true"
class=
"board-title-caret no-drag"
data-placement=
"bottom"
@
click=
"toggleExpanded"
>
<i
:class=
"
{ 'fa-caret-right': list.isExpanded, 'fa-caret-down': !list.isExpanded }"
class="fa fa-fw"
>
</i>
</div>
<!-- The following is only true in EE and if it is a milestone -->
<span
v-if=
"list.type === 'milestone' && list.milestone"
aria-hidden=
"true"
class=
"append-right-5 milestone-icon"
>
<gl-icon
name=
"timer"
/>
</span>
<a
v-if=
"list.type === 'assignee'"
:href=
"list.assignee.path"
class=
"user-avatar-link js-no-trigger"
>
<img
:alt=
"list.assignee.name"
:src=
"list.assignee.avatar"
class=
"avatar s20 has-tooltip"
height=
"20"
width=
"20"
/>
</a>
<div
class=
"board-title-text"
>
<span
v-if=
"list.type !== 'label'"
:class=
"
{
'has-tooltip': !['backlog', 'closed'].includes(list.type),
'd-block': list.type === 'milestone',
}"
:title="(list.label
&&
list.label.description) || list.title || ''"
class="board-title-main-text block-truncated"
data-container="body"
>
{{
list
.
title
}}
</span>
<span
v-if=
"list.type === 'assignee'"
:title=
"(list.assignee && list.assignee.username) || ''"
class=
"board-title-sub-text prepend-left-5 has-tooltip"
>
@
{{
list
.
assignee
.
username
}}
</span>
<gl-label
v-if=
"list.type === 'label'"
:background-color=
"list.label.color"
:description=
"list.label.description"
:scoped=
"showScopedLabels(list.label)"
:size=
"!list.isExpanded ? 'sm' : ''"
:title=
"list.label.title"
tooltip-placement=
"bottom"
/>
</div>
<board-delete
v-if=
"canAdminList && !list.preset && list.id"
:list=
"list"
inline-template=
"true"
>
<button
:class=
"
{ 'd-none': !list.isExpanded }"
:aria-label="__(`Delete list`)"
class="board-delete no-drag p-0 border-0 has-tooltip float-right"
data-placement="bottom"
title="Delete list"
type="button"
@click.stop="deleteBoard"
>
<i
aria-hidden=
"true"
data-hidden=
"true"
class=
"fa fa-trash"
></i>
</button>
</board-delete>
<div
v-if=
"showBoardListAndBoardInfo"
class=
"issue-count-badge pr-0 no-drag text-secondary"
>
<span
class=
"d-inline-flex"
>
<gl-tooltip
:target=
"() => $refs.issueCount"
:title=
"issuesTooltip"
/>
<span
ref=
"issueCount"
class=
"issue-count-badge-count"
>
<gl-icon
class=
"mr-1"
name=
"issues"
/>
<issue-count
:issues-size=
"list.issuesSize"
:max-issue-count=
"list.maxIssueCount"
/>
</span>
<!-- The following is only true in EE. -->
<template
v-if=
"weightFeatureAvailable"
>
<gl-tooltip
:target=
"() => $refs.weightTooltip"
:title=
"weightCountToolTip"
/>
<span
ref=
"weightTooltip"
class=
"d-inline-flex ml-2"
>
<gl-icon
class=
"mr-1"
name=
"weight"
/>
{{
list
.
totalWeight
}}
</span>
</
template
>
</span>
</div>
<gl-button-group
v-if=
"isNewIssueShown || isSettingsShown"
class=
"board-list-button-group pl-2"
>
<gl-deprecated-button
v-if=
"isNewIssueShown"
ref=
"newIssueBtn"
:class=
"{
'd-none': !list.isExpanded,
'rounded-right': isNewIssueShown && !isSettingsShown,
}"
:aria-label=
"__(`New issue`)"
class=
"issue-count-badge-add-button no-drag"
type=
"button"
@
click=
"showNewIssueForm"
>
<i
aria-hidden=
"true"
data-hidden=
"true"
class=
"fa fa-plus"
></i>
</gl-deprecated-button>
<gl-tooltip
:target=
"() => $refs.newIssueBtn"
>
{{ __('New Issue') }}
</gl-tooltip>
<gl-deprecated-button
v-if=
"isSettingsShown"
ref=
"settingsBtn"
:aria-label=
"__(`List settings`)"
class=
"no-drag rounded-right js-board-settings-button"
title=
"List settings"
type=
"button"
@
click=
"openSidebarSettings"
>
<gl-icon
name=
"settings"
/>
</gl-deprecated-button>
<gl-tooltip
:target=
"() => $refs.settingsBtn"
>
{{ __('List settings') }}
</gl-tooltip>
</gl-button-group>
</h3>
</header>
<div
class=
"board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
>
<board-list-header
:can-admin-list=
"canAdminList"
:list=
"list"
:disabled=
"disabled"
:board-id=
"boardId"
/>
<board-list
v-if=
"showBoardListAndBoardInfo"
ref=
"board-list"
...
...
app/assets/javascripts/boards/components/board_content.vue
View file @
21dac779
...
...
@@ -2,10 +2,12 @@
import
{
mapState
}
from
'
vuex
'
;
import
glFeatureFlagMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
BoardColumn
from
'
ee_else_ce/boards/components/board_column.vue
'
;
import
EpicsSwimlanes
from
'
ee_component/boards/components/epics_swimlanes.vue
'
;
export
default
{
components
:
{
BoardColumn
,
EpicsSwimlanes
,
},
mixins
:
[
glFeatureFlagMixin
()],
props
:
{
...
...
@@ -49,18 +51,31 @@ export default {
</
script
>
<
template
>
<div
v-if=
"!isSwimlanesOn"
class=
"boards-list w-100 py-3 px-2 text-nowrap"
data-qa-selector=
"boards_list"
>
<board-column
v-for=
"list in lists"
:key=
"list.id"
ref=
"board"
<div>
<div
v-if=
"!isSwimlanesOn"
class=
"boards-list w-100 py-3 px-2 text-nowrap"
data-qa-selector=
"boards_list"
>
<board-column
v-for=
"list in lists"
:key=
"list.id"
ref=
"board"
:can-admin-list=
"canAdminList"
:group-id=
"groupId"
:list=
"list"
:disabled=
"disabled"
:issue-link-base=
"issueLinkBase"
:root-path=
"rootPath"
:board-id=
"boardId"
/>
</div>
<epics-swimlanes
v-else
ref=
"swimlanes"
:lists=
"lists"
:can-admin-list=
"canAdminList"
:group-id=
"groupId"
:list=
"list"
:disabled=
"disabled"
:issue-link-base=
"issueLinkBase"
:root-path=
"rootPath"
...
...
app/assets/javascripts/boards/components/board_delete.js
View file @
21dac779
import
$
from
'
jquery
'
;
import
Vue
from
'
vue
'
;
import
{
GlButton
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
Vue
.
extend
({
components
:
{
GlButton
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
list
:
{
type
:
Object
,
...
...
app/assets/javascripts/boards/components/board_list.vue
View file @
21dac779
...
...
@@ -104,7 +104,7 @@ export default {
},
},
created
()
{
eventHub
.
$on
(
`
hid
e-issue-form-
${
this
.
list
.
id
}
`
,
this
.
toggleForm
);
eventHub
.
$on
(
`
toggl
e-issue-form-
${
this
.
list
.
id
}
`
,
this
.
toggleForm
);
eventHub
.
$on
(
`scroll-board-list-
${
this
.
list
.
id
}
`
,
this
.
scrollToTop
);
},
mounted
()
{
...
...
@@ -381,7 +381,7 @@ export default {
this
.
$refs
.
list
.
addEventListener
(
'
scroll
'
,
this
.
onScroll
);
},
beforeDestroy
()
{
eventHub
.
$off
(
`
hid
e-issue-form-
${
this
.
list
.
id
}
`
,
this
.
toggleForm
);
eventHub
.
$off
(
`
toggl
e-issue-form-
${
this
.
list
.
id
}
`
,
this
.
toggleForm
);
eventHub
.
$off
(
`scroll-board-list-
${
this
.
list
.
id
}
`
,
this
.
scrollToTop
);
this
.
$refs
.
list
.
removeEventListener
(
'
scroll
'
,
this
.
onScroll
);
},
...
...
app/assets/javascripts/boards/components/board_list_header.vue
0 → 100644
View file @
21dac779
<
script
>
import
{
GlButton
,
GlButtonGroup
,
GlDeprecatedButton
,
GlLabel
,
GlTooltip
,
GlIcon
,
GlTooltipDirective
,
}
from
'
@gitlab/ui
'
;
import
isWipLimitsOn
from
'
ee_else_ce/boards/mixins/is_wip_limits
'
;
import
{
s__
,
__
,
sprintf
}
from
'
~/locale
'
;
import
AccessorUtilities
from
'
../../lib/utils/accessor
'
;
import
BoardDelete
from
'
./board_delete
'
;
import
IssueCount
from
'
./issue_count.vue
'
;
import
boardsStore
from
'
../stores/boards_store
'
;
import
eventHub
from
'
../eventhub
'
;
import
{
ListType
}
from
'
../constants
'
;
import
{
isScopedLabel
}
from
'
~/lib/utils/common_utils
'
;
export
default
{
components
:
{
BoardDelete
,
GlButtonGroup
,
GlButton
,
GlDeprecatedButton
,
GlLabel
,
GlTooltip
,
GlIcon
,
IssueCount
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
mixins
:
[
isWipLimitsOn
],
props
:
{
list
:
{
type
:
Object
,
default
:
()
=>
({}),
required
:
false
,
},
disabled
:
{
type
:
Boolean
,
required
:
true
,
},
boardId
:
{
type
:
String
,
required
:
true
,
},
canAdminList
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isSwimlanesHeader
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
weightFeatureAvailable
:
false
,
};
},
computed
:
{
isLoggedIn
()
{
return
Boolean
(
gon
.
current_user_id
);
},
listType
()
{
return
this
.
list
.
type
;
},
listAssignee
()
{
return
this
.
list
?.
assignee
?.
username
||
''
;
},
listTitle
()
{
return
this
.
list
?.
label
?.
description
||
this
.
list
.
title
||
''
;
},
showListHeaderButton
()
{
return
(
!
this
.
disabled
&&
this
.
listType
!==
ListType
.
closed
&&
this
.
listType
!==
ListType
.
blank
&&
this
.
listType
!==
ListType
.
promotion
);
},
issuesTooltip
()
{
const
{
issuesSize
}
=
this
.
list
;
return
sprintf
(
__
(
'
%{issuesSize} issues
'
),
{
issuesSize
});
},
caretTooltip
()
{
return
this
.
list
.
isExpanded
?
s__
(
'
Boards|Collapse
'
)
:
s__
(
'
Boards|Expand
'
);
},
isNewIssueShown
()
{
return
this
.
listType
===
ListType
.
backlog
||
this
.
showListHeaderButton
;
},
isSettingsShown
()
{
return
(
this
.
listType
!==
ListType
.
backlog
&&
this
.
showListHeaderButton
&&
this
.
list
.
isExpanded
&&
this
.
isWipLimitsOn
);
},
showBoardListAndBoardInfo
()
{
return
this
.
listType
!==
ListType
.
blank
&&
this
.
listType
!==
ListType
.
promotion
;
},
uniqueKey
()
{
// eslint-disable-next-line @gitlab/require-i18n-strings
return
`boards.
${
this
.
boardId
}
.
${
this
.
listType
}
.
${
this
.
list
.
id
}
`
;
},
},
methods
:
{
showScopedLabels
(
label
)
{
return
boardsStore
.
scopedLabels
.
enabled
&&
isScopedLabel
(
label
);
},
showNewIssueForm
()
{
eventHub
.
$emit
(
`toggle-issue-form-
${
this
.
list
.
id
}
`
);
},
toggleExpanded
()
{
if
(
this
.
list
.
isExpandable
)
{
this
.
list
.
isExpanded
=
!
this
.
list
.
isExpanded
;
if
(
AccessorUtilities
.
isLocalStorageAccessSafe
()
&&
!
this
.
isLoggedIn
)
{
localStorage
.
setItem
(
`
${
this
.
uniqueKey
}
.expanded`
,
this
.
list
.
isExpanded
);
}
if
(
this
.
isLoggedIn
)
{
this
.
list
.
update
();
}
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
this
.
$root
.
$emit
(
'
bv::hide::tooltip
'
);
}
},
},
};
</
script
>
<
template
>
<header
:class=
"
{
'has-border': list.label
&&
list.label.color,
'gl-relative': list.isExpanded,
'gl-h-full': !list.isExpanded,
'board-inner gl-rounded-base gl-border-b-0': isSwimlanesHeader,
}"
:style="{ borderTopColor: list.label
&&
list.label.color ? list.label.color : null }"
class="board-header gl-relative"
data-qa-selector="board_list_header"
data-testid="board-list-header"
>
<h3
:class=
"
{
'user-can-drag': !disabled
&&
!list.preset,
'gl-border-b-0': !list.isExpanded,
}"
class="board-title gl-m-0 gl-display-flex js-board-handle"
>
<div
v-if=
"list.isExpandable"
v-gl-tooltip
.
hover
.
bottom
:aria-label=
"caretTooltip"
:title=
"caretTooltip"
aria-hidden=
"true"
class=
"board-title-caret no-drag"
@
click=
"toggleExpanded"
>
<i
:class=
"
{ 'fa-caret-right': list.isExpanded, 'fa-caret-down': !list.isExpanded }"
class="fa fa-fw"
>
</i>
</div>
<!-- The following is only true in EE and if it is a milestone -->
<span
v-if=
"list.type === 'milestone' && list.milestone"
aria-hidden=
"true"
class=
"gl-mr-2 milestone-icon"
>
<gl-icon
name=
"timer"
/>
</span>
<a
v-if=
"list.type === 'assignee'"
:href=
"list.assignee.path"
class=
"user-avatar-link js-no-trigger"
>
<img
v-gl-tooltip
.
hover
.
bottom
:title=
"listAssignee"
:alt=
"list.assignee.name"
:src=
"list.assignee.avatar"
class=
"avatar s20"
height=
"20"
width=
"20"
/>
</a>
<div
class=
"board-title-text"
>
<span
v-if=
"list.type !== 'label'"
v-gl-tooltip
.
hover
:class=
"
{
'gl-display-inline-block': list.type === 'milestone',
}"
:title="listTitle"
class="board-title-main-text block-truncated"
>
{{
list
.
title
}}
</span>
<span
v-if=
"list.type === 'assignee'"
class=
"board-title-sub-text gl-ml-2"
>
@
{{
list
.
assignee
.
username
}}
</span>
<gl-label
v-if=
"list.type === 'label'"
v-gl-tooltip
.
hover
.
bottom
:background-color=
"list.label.color"
:description=
"list.label.description"
:scoped=
"showScopedLabels(list.label)"
:size=
"!list.isExpanded ? 'sm' : ''"
:title=
"list.label.title"
/>
</div>
<board-delete
v-if=
"canAdminList && !list.preset && list.id"
:list=
"list"
inline-template=
"true"
>
<gl-button
v-gl-tooltip
.
hover
.
bottom
:class=
"
{ 'gl-display-none': !list.isExpanded }"
:aria-label="__('Delete list')"
class="board-delete no-drag gl-pr-0 gl-shadow-none"
:title="__('Delete list')"
icon="remove"
size="small"
@click.stop="deleteBoard"
/>
</board-delete>
<div
v-if=
"showBoardListAndBoardInfo"
class=
"issue-count-badge gl-pr-0 no-drag text-secondary"
>
<span
class=
"gl-display-inline-flex"
>
<gl-tooltip
:target=
"() => $refs.issueCount"
:title=
"issuesTooltip"
/>
<span
ref=
"issueCount"
class=
"issue-count-badge-count"
>
<gl-icon
class=
"gl-mr-2"
name=
"issues"
/>
<issue-count
:issues-size=
"list.issuesSize"
:max-issue-count=
"list.maxIssueCount"
/>
</span>
<!-- The following is only true in EE. -->
<template
v-if=
"weightFeatureAvailable"
>
<gl-tooltip
:target=
"() => $refs.weightTooltip"
:title=
"weightCountToolTip"
/>
<span
ref=
"weightTooltip"
class=
"gl-display-inline-flex gl-ml-3"
>
<gl-icon
class=
"gl-mr-2"
name=
"weight"
/>
{{
list
.
totalWeight
}}
</span>
</
template
>
</span>
</div>
<gl-button-group
v-if=
"isNewIssueShown || isSettingsShown"
class=
"board-list-button-group pl-2"
>
<gl-deprecated-button
v-if=
"isNewIssueShown"
ref=
"newIssueBtn"
:class=
"{
'gl-display-none': !list.isExpanded,
}"
:aria-label=
"__(`New issue`)"
class=
"issue-count-badge-add-button no-drag"
type=
"button"
@
click=
"showNewIssueForm"
>
<i
aria-hidden=
"true"
data-hidden=
"true"
class=
"fa fa-plus"
></i>
</gl-deprecated-button>
<gl-tooltip
:target=
"() => $refs.newIssueBtn"
>
{{ __('New Issue') }}
</gl-tooltip>
<gl-deprecated-button
v-if=
"isSettingsShown"
ref=
"settingsBtn"
:aria-label=
"__(`List settings`)"
class=
"no-drag js-board-settings-button"
title=
"List settings"
type=
"button"
@
click=
"openSidebarSettings"
>
<gl-icon
name=
"settings"
/>
</gl-deprecated-button>
<gl-tooltip
:target=
"() => $refs.settingsBtn"
>
{{ __('List settings') }}
</gl-tooltip>
</gl-button-group>
</h3>
</header>
</template>
app/assets/javascripts/boards/components/board_new_issue.vue
View file @
21dac779
...
...
@@ -92,7 +92,7 @@ export default {
},
cancel
()
{
this
.
title
=
''
;
eventHub
.
$emit
(
`
hid
e-issue-form-
${
this
.
list
.
id
}
`
);
eventHub
.
$emit
(
`
toggl
e-issue-form-
${
this
.
list
.
id
}
`
);
},
setSelectedProject
(
selectedProject
)
{
this
.
selectedProject
=
selectedProject
;
...
...
app/assets/stylesheets/pages/boards.scss
View file @
21dac779
...
...
@@ -206,6 +206,7 @@
&
:hover
{
color
:
$blue-600
;
box-shadow
:
none
;
}
}
...
...
@@ -573,3 +574,8 @@
top
:
0
;
}
}
.board-epics-swimlanes
{
overflow-x
:
auto
;
min-height
:
600px
;
}
app/views/shared/issuable/_search_bar.html.haml
View file @
21dac779
...
...
@@ -173,7 +173,7 @@
=
render
'shared/issuable/board_create_list_dropdown'
,
board:
board
-
if
@project
#js-add-issues-btn
.prepend-left-10
{
data:
{
can_admin_list:
can?
(
current_user
,
:admin_list
,
@project
)
}
}
-
if
Feature
.
enabled?
(
:boards_with_swimlanes
)
-
if
Feature
.
enabled?
(
:boards_with_swimlanes
,
@group
)
#js-board-epics-swimlanes-toggle
#js-toggle-focus-btn
-
elsif
is_not_boards_modal_or_productivity_analytics
&&
show_sorting_dropdown
...
...
ee/app/assets/javascripts/boards/components/board_column.vue
View file @
21dac779
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
BoardColumnFoss
from
'
~/boards/components/board_column.vue
'
;
import
{
__
,
sprintf
,
s__
}
from
'
~/locale
'
;
import
boardsStore
from
'
~/boards/stores/boards_store
'
;
import
{
inactiveListId
}
from
'
~/boards/constants
'
;
import
BoardPromotionState
from
'
ee/boards/components/board_promotion_state
'
;
import
eventHub
from
'
~/sidebar/event_hub
'
;
export
default
{
components
:
{
BoardPromotionState
,
},
extends
:
BoardColumnFoss
,
data
()
{
return
{
weightFeatureAvailable
:
boardsStore
.
weightFeatureAvailable
,
};
},
computed
:
{
...
mapState
([
'
activeListId
'
]),
issuesTooltip
()
{
const
{
issuesSize
,
maxIssueCount
}
=
this
.
list
;
if
(
maxIssueCount
>
0
)
{
return
sprintf
(
__
(
'
%{issuesSize} issues with a limit of %{maxIssueCount}
'
),
{
issuesSize
,
maxIssueCount
,
});
}
// TODO: Remove this pattern.
return
BoardColumnFoss
.
computed
.
issuesTooltip
.
call
(
this
);
},
weightCountToolTip
()
{
const
{
totalWeight
}
=
this
.
list
;
if
(
this
.
weightFeatureAvailable
)
{
return
sprintf
(
s__
(
'
%{totalWeight} total weight
'
),
{
totalWeight
});
}
return
null
;
},
},
methods
:
{
...
mapActions
([
'
setActiveListId
'
]),
openSidebarSettings
()
{
if
(
this
.
activeListId
===
inactiveListId
)
{
eventHub
.
$emit
(
'
sidebar.closeAll
'
);
}
this
.
setActiveListId
(
this
.
list
.
id
);
},
},
};
</
script
>
ee/app/assets/javascripts/boards/components/board_list_header.vue
0 → 100644
View file @
21dac779
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
BoardListHeaderFoss
from
'
~/boards/components/board_list_header.vue
'
;
import
{
__
,
sprintf
,
s__
}
from
'
~/locale
'
;
import
boardsStore
from
'
~/boards/stores/boards_store
'
;
import
{
inactiveListId
}
from
'
~/boards/constants
'
;
import
eventHub
from
'
~/sidebar/event_hub
'
;
export
default
{
extends
:
BoardListHeaderFoss
,
data
()
{
return
{
weightFeatureAvailable
:
boardsStore
.
weightFeatureAvailable
,
};
},
computed
:
{
...
mapState
([
'
activeListId
'
]),
issuesTooltip
()
{
const
{
issuesSize
,
maxIssueCount
}
=
this
.
list
;
if
(
maxIssueCount
>
0
)
{
return
sprintf
(
__
(
'
%{issuesSize} issues with a limit of %{maxIssueCount}
'
),
{
issuesSize
,
maxIssueCount
,
});
}
// TODO: Remove this pattern.
return
BoardListHeaderFoss
.
computed
.
issuesTooltip
.
call
(
this
);
},
weightCountToolTip
()
{
const
{
totalWeight
}
=
this
.
list
;
if
(
this
.
weightFeatureAvailable
)
{
return
sprintf
(
s__
(
'
%{totalWeight} total weight
'
),
{
totalWeight
});
}
return
null
;
},
},
methods
:
{
...
mapActions
([
'
setActiveListId
'
]),
openSidebarSettings
()
{
if
(
this
.
activeListId
===
inactiveListId
)
{
eventHub
.
$emit
(
'
sidebar.closeAll
'
);
}
this
.
setActiveListId
(
this
.
list
.
id
);
},
},
};
</
script
>
ee/app/assets/javascripts/boards/components/epics_swimlanes.vue
0 → 100644
View file @
21dac779
<
script
>
import
BoardListHeader
from
'
ee_else_ce/boards/components/board_list_header.vue
'
;
export
default
{
components
:
{
BoardListHeader
,
},
props
:
{
lists
:
{
type
:
Array
,
required
:
true
,
},
disabled
:
{
type
:
Boolean
,
required
:
true
,
},
issueLinkBase
:
{
type
:
String
,
required
:
true
,
},
rootPath
:
{
type
:
String
,
required
:
true
,
},
boardId
:
{
type
:
String
,
required
:
true
,
},
canAdminList
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
groupId
:
{
type
:
Number
,
required
:
false
,
default
:
null
,
},
},
};
</
script
>
<
template
>
<div
class=
"board-epics-swimlanes gl-white-space-nowrap gl-py-5 gl-px-3"
data_qa_selector=
"board_epics_swimlanes"
>
<div
v-for=
"list in lists"
:key=
"list.id"
:class=
"
{
'is-expandable': list.isExpandable,
'is-collapsed': !list.isExpanded,
}"
class="board gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
>
<board-list-header
:can-admin-list=
"canAdminList"
:list=
"list"
:disabled=
"disabled"
:board-id=
"boardId"
:is-swimlanes-header=
"true"
/>
</div>
</div>
</
template
>
ee/spec/frontend/boards/components/board_
column
_spec.js
→
ee/spec/frontend/boards/components/board_
list_header
_spec.js
View file @
21dac779
...
...
@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
AxiosMockAdapter
from
'
axios-mock-adapter
'
;
import
Board
from
'
ee/boards/components/board_column
.vue
'
;
import
Board
ListHeader
from
'
ee/boards/components/board_list_header
.vue
'
;
import
List
from
'
~/boards/models/list
'
;
import
{
ListType
,
inactiveListId
}
from
'
~/boards/constants
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
...
...
@@ -20,7 +20,7 @@ const localVue = createLocalVue();
localVue
.
use
(
Vuex
);
describe
(
'
Board
Column
Component
'
,
()
=>
{
describe
(
'
Board
List Header
Component
'
,
()
=>
{
let
store
;
let
wrapper
;
let
axiosMock
;
...
...
@@ -69,7 +69,7 @@ describe('Board Column Component', () => {
);
}
wrapper
=
shallowMount
(
Board
,
{
wrapper
=
shallowMount
(
Board
ListHeader
,
{
store
,
localVue
,
propsData
:
{
...
...
spec/frontend/boards/board_list_spec.js
View file @
21dac779
...
...
@@ -118,7 +118,7 @@ describe('Board list component', () => {
});
it
(
'
shows new issue form after eventhub event
'
,
()
=>
{
eventHub
.
$emit
(
`
hid
e-issue-form-
${
component
.
list
.
id
}
`
);
eventHub
.
$emit
(
`
toggl
e-issue-form-
${
component
.
list
.
id
}
`
);
return
Vue
.
nextTick
().
then
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-new-issue-form
'
)).
not
.
toBeNull
();
...
...
spec/frontend/boards/components/board_column_spec.js
View file @
21dac779
...
...
@@ -70,37 +70,6 @@ describe('Board Column Component', () => {
const
isExpandable
=
()
=>
wrapper
.
classes
(
'
is-expandable
'
);
const
isCollapsed
=
()
=>
wrapper
.
classes
(
'
is-collapsed
'
);
const
findAddIssueButton
=
()
=>
wrapper
.
find
({
ref
:
'
newIssueBtn
'
});
describe
(
'
Add issue button
'
,
()
=>
{
const
hasNoAddButton
=
[
ListType
.
promotion
,
ListType
.
blank
,
ListType
.
closed
];
const
hasAddButton
=
[
ListType
.
backlog
,
ListType
.
label
,
ListType
.
milestone
,
ListType
.
assignee
];
it
.
each
(
hasNoAddButton
)(
'
does not render when List Type is `%s`
'
,
listType
=>
{
createComponent
({
listType
});
expect
(
findAddIssueButton
().
exists
()).
toBe
(
false
);
});
it
.
each
(
hasAddButton
)(
'
does render when List Type is `%s`
'
,
listType
=>
{
createComponent
({
listType
});
expect
(
findAddIssueButton
().
exists
()).
toBe
(
true
);
});
it
(
'
has a test for each list type
'
,
()
=>
{
Object
.
values
(
ListType
).
forEach
(
value
=>
{
expect
([...
hasAddButton
,
...
hasNoAddButton
]).
toContain
(
value
);
});
});
it
(
'
does render when logged out
'
,
()
=>
{
createComponent
();
expect
(
findAddIssueButton
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
Given different list types
'
,
()
=>
{
it
(
'
is expandable when List Type is `backlog`
'
,
()
=>
{
createComponent
({
listType
:
ListType
.
backlog
});
...
...
@@ -109,64 +78,17 @@ describe('Board Column Component', () => {
});
});
describe
(
'
expanding / collapsing the column
'
,
()
=>
{
it
(
'
does not collapse when clicking the header
'
,
()
=>
{
createComponent
();
expect
(
isCollapsed
()).
toBe
(
false
);
wrapper
.
find
(
'
.board-header
'
).
trigger
(
'
click
'
);
describe
(
'
expanded / collaped column
'
,
()
=>
{
it
(
'
has class is-collapsed when list is collapsed
'
,
()
=>
{
createComponent
({
collapsed
:
false
});
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
isCollapsed
()).
toBe
(
false
);
});
});
it
(
'
collapses expanded Column when clicking the collapse icon
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
vm
.
list
.
isExpanded
).
toBe
(
true
);
wrapper
.
find
(
'
.board-title-caret
'
).
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
isCollapsed
()).
toBe
(
true
);
});
});
it
(
'
expands collapsed Column when clicking the expand icon
'
,
()
=>
{
it
(
'
does not have class is-collapsed when list is expanded
'
,
()
=>
{
createComponent
({
collapsed
:
true
});
expect
(
isCollapsed
()).
toBe
(
true
);
wrapper
.
find
(
'
.board-title-caret
'
).
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
isCollapsed
()).
toBe
(
false
);
});
});
it
(
"
when logged in it calls list update and doesn't set localStorage
"
,
()
=>
{
jest
.
spyOn
(
List
.
prototype
,
'
update
'
);
window
.
gon
.
current_user_id
=
1
;
createComponent
({
withLocalStorage
:
false
});
wrapper
.
find
(
'
.board-title-caret
'
).
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
list
.
update
).
toHaveBeenCalledTimes
(
1
);
expect
(
localStorage
.
getItem
(
`
${
wrapper
.
vm
.
uniqueKey
}
.expanded`
)).
toBe
(
null
);
});
});
it
(
"
when logged out it doesn't call list update and sets localStorage
"
,
()
=>
{
jest
.
spyOn
(
List
.
prototype
,
'
update
'
);
createComponent
();
wrapper
.
find
(
'
.board-title-caret
'
).
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
list
.
update
).
toHaveBeenCalledTimes
(
0
);
expect
(
localStorage
.
getItem
(
`
${
wrapper
.
vm
.
uniqueKey
}
.expanded`
)).
toBe
(
String
(
wrapper
.
vm
.
list
.
isExpanded
),
);
});
expect
(
isCollapsed
()).
toBe
(
true
);
});
});
});
spec/frontend/boards/components/board_list_header_spec.js
0 → 100644
View file @
21dac779
import
Vue
from
'
vue
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
AxiosMockAdapter
from
'
axios-mock-adapter
'
;
import
BoardListHeader
from
'
~/boards/components/board_list_header.vue
'
;
import
List
from
'
~/boards/models/list
'
;
import
{
ListType
}
from
'
~/boards/constants
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
{
listObj
}
from
'
jest/boards/mock_data
'
;
describe
(
'
Board List Header Component
'
,
()
=>
{
let
wrapper
;
let
axiosMock
;
beforeEach
(()
=>
{
window
.
gon
=
{};
axiosMock
=
new
AxiosMockAdapter
(
axios
);
axiosMock
.
onGet
(
`
${
TEST_HOST
}
/lists/1/issues`
).
reply
(
200
,
{
issues
:
[]
});
});
afterEach
(()
=>
{
axiosMock
.
restore
();
wrapper
.
destroy
();
localStorage
.
clear
();
});
const
createComponent
=
({
listType
=
ListType
.
backlog
,
collapsed
=
false
,
withLocalStorage
=
true
,
}
=
{})
=>
{
const
boardId
=
'
1
'
;
const
listMock
=
{
...
listObj
,
list_type
:
listType
,
collapsed
,
};
if
(
listType
===
ListType
.
assignee
)
{
delete
listMock
.
label
;
listMock
.
user
=
{};
}
// Making List reactive
const
list
=
Vue
.
observable
(
new
List
(
listMock
));
if
(
withLocalStorage
)
{
localStorage
.
setItem
(
`boards.
${
boardId
}
.
${
list
.
type
}
.
${
list
.
id
}
.expanded`
,
(
!
collapsed
).
toString
(),
);
}
wrapper
=
shallowMount
(
BoardListHeader
,
{
propsData
:
{
boardId
,
disabled
:
false
,
issueLinkBase
:
'
/
'
,
rootPath
:
'
/
'
,
list
,
},
});
};
const
isCollapsed
=
()
=>
!
wrapper
.
props
().
list
.
isExpanded
;
const
isExpanded
=
()
=>
wrapper
.
vm
.
list
.
isExpanded
;
const
findAddIssueButton
=
()
=>
wrapper
.
find
({
ref
:
'
newIssueBtn
'
});
const
findCaret
=
()
=>
wrapper
.
find
(
'
.board-title-caret
'
);
describe
(
'
Add issue button
'
,
()
=>
{
const
hasNoAddButton
=
[
ListType
.
promotion
,
ListType
.
blank
,
ListType
.
closed
];
const
hasAddButton
=
[
ListType
.
backlog
,
ListType
.
label
,
ListType
.
milestone
,
ListType
.
assignee
];
it
.
each
(
hasNoAddButton
)(
'
does not render when List Type is `%s`
'
,
listType
=>
{
createComponent
({
listType
});
expect
(
findAddIssueButton
().
exists
()).
toBe
(
false
);
});
it
.
each
(
hasAddButton
)(
'
does render when List Type is `%s`
'
,
listType
=>
{
createComponent
({
listType
});
expect
(
findAddIssueButton
().
exists
()).
toBe
(
true
);
});
it
(
'
has a test for each list type
'
,
()
=>
{
Object
.
values
(
ListType
).
forEach
(
value
=>
{
expect
([...
hasAddButton
,
...
hasNoAddButton
]).
toContain
(
value
);
});
});
it
(
'
does render when logged out
'
,
()
=>
{
createComponent
();
expect
(
findAddIssueButton
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
expanding / collapsing the column
'
,
()
=>
{
it
(
'
does not collapse when clicking the header
'
,
()
=>
{
createComponent
();
expect
(
isCollapsed
()).
toBe
(
false
);
wrapper
.
find
(
'
[data-testid="board-list-header"]
'
).
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
isCollapsed
()).
toBe
(
false
);
});
});
it
(
'
collapses expanded Column when clicking the collapse icon
'
,
()
=>
{
createComponent
();
expect
(
isExpanded
()).
toBe
(
true
);
findCaret
().
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
isCollapsed
()).
toBe
(
true
);
});
});
it
(
'
expands collapsed Column when clicking the expand icon
'
,
()
=>
{
createComponent
({
collapsed
:
true
});
expect
(
isCollapsed
()).
toBe
(
true
);
findCaret
().
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
isCollapsed
()).
toBe
(
false
);
});
});
it
(
"
when logged in it calls list update and doesn't set localStorage
"
,
()
=>
{
jest
.
spyOn
(
List
.
prototype
,
'
update
'
);
window
.
gon
.
current_user_id
=
1
;
createComponent
({
withLocalStorage
:
false
});
findCaret
().
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
list
.
update
).
toHaveBeenCalledTimes
(
1
);
expect
(
localStorage
.
getItem
(
`
${
wrapper
.
vm
.
uniqueKey
}
.expanded`
)).
toBe
(
null
);
});
});
it
(
"
when logged out it doesn't call list update and sets localStorage
"
,
()
=>
{
jest
.
spyOn
(
List
.
prototype
,
'
update
'
);
createComponent
();
findCaret
().
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
list
.
update
).
not
.
toHaveBeenCalled
();
expect
(
localStorage
.
getItem
(
`
${
wrapper
.
vm
.
uniqueKey
}
.expanded`
)).
toBe
(
String
(
isExpanded
()));
});
});
});
});
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