Commit 21dac779 authored by Florie Guibert's avatar Florie Guibert Committed by Kushal Pandya

Swimlanes column headers

- Move board header out of board column for reusability
parent 59e5a8ef
<script> <script>
import $ from 'jquery';
import Sortable from 'sortablejs'; 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 isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
import { s__, __, sprintf } from '~/locale';
import Tooltip from '~/vue_shared/directives/tooltip'; import Tooltip from '~/vue_shared/directives/tooltip';
import EmptyComponent from '~/vue_shared/components/empty_component'; import EmptyComponent from '~/vue_shared/components/empty_component';
import AccessorUtilities from '../../lib/utils/accessor';
import BoardBlankState from './board_blank_state.vue'; import BoardBlankState from './board_blank_state.vue';
import BoardDelete from './board_delete'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import BoardList from './board_list.vue'; import BoardList from './board_list.vue';
import IssueCount from './issue_count.vue';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import eventHub from '../eventhub';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
import { ListType } from '../constants'; import { ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
BoardPromotionState: EmptyComponent, BoardPromotionState: EmptyComponent,
BoardBlankState, BoardBlankState,
BoardDelete, BoardListHeader,
BoardList, BoardList,
GlButtonGroup,
IssueCount,
GlDeprecatedButton,
GlLabel,
GlTooltip,
GlIcon,
}, },
directives: { directives: {
Tooltip, Tooltip,
...@@ -70,42 +59,9 @@ export default { ...@@ -70,42 +59,9 @@ export default {
return { return {
detailIssue: boardsStore.detail, detailIssue: boardsStore.detail,
filter: boardsStore.filter, filter: boardsStore.filter,
weightFeatureAvailable: false,
}; };
}, },
computed: { 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() { showBoardListAndBoardInfo() {
return this.list.type !== ListType.blank && this.list.type !== ListType.promotion; return this.list.type !== ListType.blank && this.list.type !== ListType.promotion;
}, },
...@@ -151,41 +107,9 @@ export default { ...@@ -151,41 +107,9 @@ export default {
Sortable.create(this.$el.parentNode, sortableOptions); 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: { methods: {
showScopedLabels(label) { showListNewIssueForm(listId) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label); eventHub.$emit('showForm', listId);
},
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');
}
}, },
}, },
}; };
...@@ -200,166 +124,18 @@ export default { ...@@ -200,166 +124,18 @@ export default {
'board-type-assignee': list.type === 'assignee', 'board-type-assignee': list.type === 'assignee',
}" }"
:data-id="list.id" :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" data-qa-selector="board_list"
> >
<div class="board-inner d-flex flex-column position-relative h-100 rounded"> <div
<header class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
:class="{ >
'has-border': list.label && list.label.color, <board-list-header
'position-relative': list.isExpanded, :can-admin-list="canAdminList"
'position-absolute position-top-0 position-left-0 w-100 h-100': !list.isExpanded, :list="list"
}" :disabled="disabled"
:style="{ borderTopColor: list.label && list.label.color ? list.label.color : null }" :board-id="boardId"
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>
<board-list <board-list
v-if="showBoardListAndBoardInfo" v-if="showBoardListAndBoardInfo"
ref="board-list" ref="board-list"
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
export default { export default {
components: { components: {
BoardColumn, BoardColumn,
EpicsSwimlanes,
}, },
mixins: [glFeatureFlagMixin()], mixins: [glFeatureFlagMixin()],
props: { props: {
...@@ -49,18 +51,31 @@ export default { ...@@ -49,18 +51,31 @@ export default {
</script> </script>
<template> <template>
<div <div>
v-if="!isSwimlanesOn" <div
class="boards-list w-100 py-3 px-2 text-nowrap" v-if="!isSwimlanesOn"
data-qa-selector="boards_list" class="boards-list w-100 py-3 px-2 text-nowrap"
> data-qa-selector="boards_list"
<board-column >
v-for="list in lists" <board-column
:key="list.id" v-for="list in lists"
ref="board" :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" :can-admin-list="canAdminList"
:group-id="groupId" :group-id="groupId"
:list="list"
:disabled="disabled" :disabled="disabled"
:issue-link-base="issueLinkBase" :issue-link-base="issueLinkBase"
:root-path="rootPath" :root-path="rootPath"
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default Vue.extend({ export default Vue.extend({
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
list: { list: {
type: Object, type: Object,
......
...@@ -104,7 +104,7 @@ export default { ...@@ -104,7 +104,7 @@ export default {
}, },
}, },
created() { created() {
eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
}, },
mounted() { mounted() {
...@@ -381,7 +381,7 @@ export default { ...@@ -381,7 +381,7 @@ export default {
this.$refs.list.addEventListener('scroll', this.onScroll); this.$refs.list.addEventListener('scroll', this.onScroll);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
this.$refs.list.removeEventListener('scroll', this.onScroll); this.$refs.list.removeEventListener('scroll', this.onScroll);
}, },
......
<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>
...@@ -92,7 +92,7 @@ export default { ...@@ -92,7 +92,7 @@ export default {
}, },
cancel() { cancel() {
this.title = ''; this.title = '';
eventHub.$emit(`hide-issue-form-${this.list.id}`); eventHub.$emit(`toggle-issue-form-${this.list.id}`);
}, },
setSelectedProject(selectedProject) { setSelectedProject(selectedProject) {
this.selectedProject = selectedProject; this.selectedProject = selectedProject;
......
...@@ -206,6 +206,7 @@ ...@@ -206,6 +206,7 @@
&:hover { &:hover {
color: $blue-600; color: $blue-600;
box-shadow: none;
} }
} }
...@@ -573,3 +574,8 @@ ...@@ -573,3 +574,8 @@
top: 0; top: 0;
} }
} }
.board-epics-swimlanes {
overflow-x: auto;
min-height: 600px;
}
...@@ -173,7 +173,7 @@ ...@@ -173,7 +173,7 @@
= render 'shared/issuable/board_create_list_dropdown', board: board = render 'shared/issuable/board_create_list_dropdown', board: board
- if @project - if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @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-board-epics-swimlanes-toggle
#js-toggle-focus-btn #js-toggle-focus-btn
- elsif is_not_boards_modal_or_productivity_analytics && show_sorting_dropdown - elsif is_not_boards_modal_or_productivity_analytics && show_sorting_dropdown
......
<script> <script>
import { mapState, mapActions } from 'vuex';
import BoardColumnFoss from '~/boards/components/board_column.vue'; 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 BoardPromotionState from 'ee/boards/components/board_promotion_state';
import eventHub from '~/sidebar/event_hub';
export default { export default {
components: { components: {
BoardPromotionState, BoardPromotionState,
}, },
extends: BoardColumnFoss, 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> </script>
<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>
<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>
...@@ -3,7 +3,7 @@ import Vuex from 'vuex'; ...@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import Board from 'ee/boards/components/board_column.vue'; import BoardListHeader from 'ee/boards/components/board_list_header.vue';
import List from '~/boards/models/list'; import List from '~/boards/models/list';
import { ListType, inactiveListId } from '~/boards/constants'; import { ListType, inactiveListId } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -20,7 +20,7 @@ const localVue = createLocalVue(); ...@@ -20,7 +20,7 @@ const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
describe('Board Column Component', () => { describe('Board List Header Component', () => {
let store; let store;
let wrapper; let wrapper;
let axiosMock; let axiosMock;
...@@ -69,7 +69,7 @@ describe('Board Column Component', () => { ...@@ -69,7 +69,7 @@ describe('Board Column Component', () => {
); );
} }
wrapper = shallowMount(Board, { wrapper = shallowMount(BoardListHeader, {
store, store,
localVue, localVue,
propsData: { propsData: {
......
...@@ -118,7 +118,7 @@ describe('Board list component', () => { ...@@ -118,7 +118,7 @@ describe('Board list component', () => {
}); });
it('shows new issue form after eventhub event', () => { it('shows new issue form after eventhub event', () => {
eventHub.$emit(`hide-issue-form-${component.list.id}`); eventHub.$emit(`toggle-issue-form-${component.list.id}`);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
......
...@@ -70,37 +70,6 @@ describe('Board Column Component', () => { ...@@ -70,37 +70,6 @@ describe('Board Column Component', () => {
const isExpandable = () => wrapper.classes('is-expandable'); const isExpandable = () => wrapper.classes('is-expandable');
const isCollapsed = () => wrapper.classes('is-collapsed'); 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', () => { describe('Given different list types', () => {
it('is expandable when List Type is `backlog`', () => { it('is expandable when List Type is `backlog`', () => {
createComponent({ listType: ListType.backlog }); createComponent({ listType: ListType.backlog });
...@@ -109,64 +78,17 @@ describe('Board Column Component', () => { ...@@ -109,64 +78,17 @@ describe('Board Column Component', () => {
}); });
}); });
describe('expanding / collapsing the column', () => { describe('expanded / collaped column', () => {
it('does not collapse when clicking the header', () => { it('has class is-collapsed when list is collapsed', () => {
createComponent(); createComponent({ collapsed: false });
expect(isCollapsed()).toBe(false);
wrapper.find('.board-header').trigger('click');
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); 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 }); 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'); expect(isCollapsed()).toBe(true);
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),
);
});
}); });
}); });
}); });
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()));
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment