Skip to content
Snippets Groups Projects
Commit 5c078128 authored by Martin Hanzel's avatar Martin Hanzel Committed by Mike Greiling
Browse files

Allow collapsing all issue boards

All issue boards can now be collapsed via a button, re-ordered by
dragging the header, and the vertical collapsed header style was
reworked.
parent 388a4964
No related branches found
No related tags found
No related merge requests found
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
import { n__ } from '~/locale';
import { n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor';
Loading
Loading
@@ -53,12 +54,19 @@ export default Vue.extend({
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
caretTooltip() {
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
},
isNewIssueShown() {
return (
this.list.type === 'backlog' ||
(!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank')
);
},
uniqueKey() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
},
},
watch: {
filter: {
Loading
Loading
@@ -72,31 +80,34 @@ export default Vue.extend({
},
},
mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({
const instance = this;
const sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: e => {
onEnd(e) {
sortableEnd();
 
const sortable = this;
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray();
const order = sortable.toArray();
const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10));
 
this.$nextTick(() => {
instance.$nextTick(() => {
boardsStore.moveList(list, order);
});
}
},
});
 
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
Sortable.create(this.$el.parentNode, sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed =
localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
const isCollapsed = localStorage.getItem(`${this.uniqueKey}.expanded`) === 'false';
 
this.list.isExpanded = !isCollapsed;
}
Loading
Loading
@@ -105,16 +116,17 @@ export default Vue.extend({
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
toggleExpanded() {
if (this.list.isExpandable) {
this.list.isExpanded = !this.list.isExpanded;
 
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(
`boards.${this.boardId}.${this.list.type}.expanded`,
this.list.isExpanded,
);
localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
}
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
$('.tooltip').tooltip('hide');
}
},
},
Loading
Loading
Loading
Loading
@@ -20,7 +20,7 @@ export function getBoardSortableDefaultOptions(obj) {
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
 
const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
filter: '.no-drag',
delay: touchEnabled ? 100 : 0,
scrollSensitivity: touchEnabled ? 60 : 100,
scrollSpeed: 20,
Loading
Loading
Loading
Loading
@@ -26,6 +26,12 @@ const TYPES = {
isExpandable: false,
isBlank: true,
},
default: {
// includes label, assignee, and milestone lists
isPreset: false,
isExpandable: true,
isBlank: false,
},
};
 
class List {
Loading
Loading
@@ -249,7 +255,7 @@ class List {
}
 
getTypeInfo(type) {
return TYPES[type] || {};
return TYPES[type] || TYPES.default;
}
 
onNewIssueResponse(issue, data) {
Loading
Loading
Loading
Loading
@@ -92,9 +92,20 @@
width: 400px;
}
 
&.is-expandable {
.board-header {
cursor: pointer;
.board-title-caret {
cursor: pointer;
border-radius: $border-radius-default;
padding: 4px;
&:hover {
background-color: $gray-dark;
transition: background-color 0.1s linear;
}
}
&:not(.is-collapsed) {
.board-title-caret {
margin: 0 $gl-padding-4 0 -10px;
}
}
 
Loading
Loading
@@ -102,20 +113,51 @@
width: 50px;
 
.board-title {
> span {
width: 100%;
margin-top: -12px;
flex-direction: column;
height: 100%;
padding: $gl-padding-8 0;
}
.board-title-caret {
margin-top: 1px;
}
.user-avatar-link,
.milestone-icon {
margin-top: $gl-padding-8;
transform: rotate(90deg);
}
.board-title-text {
flex-grow: 0;
margin: $gl-padding-8 0;
.board-title-main-text {
display: block;
transform: rotate(90deg) translate(35px, 0);
overflow: initial;
}
.board-title-sub-text {
display: none;
}
}
 
.board-title-expandable-toggle {
position: absolute;
top: 50%;
left: 50%;
margin-left: -10px;
.issue-count-badge {
border: 0;
white-space: nowrap;
}
.board-title-text > span,
.issue-count-badge > span {
height: 16px;
// Force the height to be equal to the parent's width while centering the contents.
// The contents *should* be about 16 px.
// We do this because the flow of elements isn't affected by the rotate transform, so we must ensure that a
// rotated element has square dimensions so it won't overlap with its siblings.
margin: calc(50% - 8px) 0;
transform: rotate(90deg);
transform-origin: center;
}
}
}
Loading
Loading
@@ -152,12 +194,14 @@
}
 
.board-title {
align-items: center;
font-size: 1em;
border-bottom: 1px solid $border-color;
padding: $gl-padding-8 $gl-padding;
}
 
.board-title-text {
margin: $gl-vert-padding auto $gl-vert-padding 0;
flex-grow: 1;
}
 
.board-delete {
Loading
Loading
.board.d-inline-block.h-100.px-2.align-top.ws-normal{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
":data-id" => "list.id" }
.board-inner.d-flex.flex-column.position-relative.h-100.rounded
%header.board-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) }", "@click" => "toggleExpanded($event)" }
%h3.board-title.m-0.d-flex.align-items-center.py-2.px-3.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset), "p-0 border-bottom-0 justify-content-center": !list.isExpanded }' }
%i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable",
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }",
"aria-hidden": "true" }
%header.board-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) }" }
%h3.board-title.m-0.d-flex.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset), "border-bottom-0": !list.isExpanded }' }
.board-title-caret.no-drag{ "v-if": "list.isExpandable",
"aria-hidden": "true",
":aria-label": "caretTooltip",
":title": "caretTooltip",
"v-tooltip": "",
data: { placement: "bottom" },
"@click": "toggleExpanded" }
%i.fa.fa-fw{ ":class": '{ "fa-caret-right": list.isExpanded, "fa-caret-down": !list.isExpanded }' }
= render_if_exists "shared/boards/components/list_milestone"
 
%a.user-avatar-link.js-no-trigger{ "v-if": "list.type === \"assignee\"", ":href": "list.assignee.path" }
-# haml-lint:disable AltText
%img.avatar.s20.has-tooltip{ height: "20", width: "20", ":src": "list.assignee.avatar", ":alt": "list.assignee.name" }
 
%span.board-title-text.has-tooltip.block-truncated{ "v-if": "list.type !== \"label\"",
":title" => '((list.label && list.label.description) || list.title || "")', data: { container: "body" } }
{{ list.title }}
.board-title-text
%span.board-title-main-text.has-tooltip.block-truncated{ "v-if": "list.type !== \"label\"",
":title" => '((list.label && list.label.description) || list.title || "")', data: { container: "body" } }
{{ list.title }}
 
%span.board-title-sub-text.prepend-left-5.has-tooltip{ "v-if": "list.type === \"assignee\"",
":title" => '(list.assignee && list.assignee.username || "")' }
@{{ list.assignee.username }}
%span.board-title-sub-text.prepend-left-5.has-tooltip{ "v-if": "list.type === \"assignee\"",
":title" => '(list.assignee && list.assignee.username || "")' }
@{{ list.assignee.username }}
 
%span.has-tooltip{ "v-if": "list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
class: "badge color-label title board-title-text",
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
{{ list.title }}
%span.has-tooltip.badge.color-label.title{ "v-if": "list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
{{ list.title }}
 
- if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
%button.board-delete.p-0.border-0.has-tooltip.float-right{ type: "button", title: _("Delete list"), ":class": "{ 'd-none': !list.isExpanded }", "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
%button.board-delete.no-drag.p-0.border-0.has-tooltip.float-right{ type: "button", title: _("Delete list"), ":class": "{ 'd-none': !list.isExpanded }", "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
.issue-count-badge.text-secondary{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":title": "counterTooltip", ":class": "{ 'd-none': !list.isExpanded }", "v-tooltip": true, data: { placement: "top" } }
%span.issue-count-badge-count
%icon.mr-1{ name: "issues" }
{{ list.issuesSize }}
= render_if_exists "shared/boards/components/list_weight"
 
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button",
.issue-count-badge.no-drag.text-secondary{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":title": "counterTooltip", "v-tooltip": true, data: { placement: "top" } }
%span.d-inline-flex
%span.issue-count-badge-count
%icon.mr-1{ name: "issues" }
{{ list.issuesSize }}
= render_if_exists "shared/boards/components/list_weight"
%button.issue-count-badge-add-button.no-drag.btn.btn-sm.btn-default.ml-1.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => "isNewIssueShown",
":class": "{ 'd-none': !list.isExpanded }",
"aria-label" => _("New issue"),
"title" => _("New issue"),
data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse")
= icon("plus")
 
%board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
":list" => "list",
Loading
Loading
---
title: Labeled issue boards can now collapse
merge_request: 29955
author:
type: added
Loading
Loading
@@ -1606,6 +1606,12 @@ msgstr ""
msgid "Boards"
msgstr ""
 
msgid "Boards|Collapse"
msgstr ""
msgid "Boards|Expand"
msgstr ""
msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
 
Loading
Loading
import Vue from 'vue';
import '~/boards/services/board_service';
import Board from '~/boards/components/board';
import '~/boards/models/list';
import List from '~/boards/models/list';
import { mockBoardService } from '../mock_data';
 
describe('Board component', () => {
Loading
Loading
@@ -27,7 +26,6 @@ describe('Board component', () => {
disabled: false,
issueLinkBase: '/',
rootPath: '/',
// eslint-disable-next-line no-undef
list: new List({
id: 1,
position: 0,
Loading
Loading
@@ -53,57 +51,62 @@ describe('Board component', () => {
expect(vm.$el.classList.contains('is-expandable')).toBe(true);
});
 
it('board is expandable when list type is closed', done => {
vm.list.type = 'closed';
Vue.nextTick(() => {
expect(vm.$el.classList.contains('is-expandable')).toBe(true);
done();
});
it('board is expandable when list type is closed', () => {
expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true);
});
 
it('board is not expandable when list type is label', done => {
vm.list.type = 'label';
vm.list.isExpandable = false;
Vue.nextTick(() => {
expect(vm.$el.classList.contains('is-expandable')).toBe(false);
it('board is expandable when list type is label', () => {
expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true);
});
 
done();
});
it('board is not expandable when list type is blank', () => {
expect(new List({ id: 1, list_type: 'blank' }).isExpandable).toBe(false);
});
 
it('collapses when clicking header', done => {
it('does not collapse when clicking header', done => {
vm.list.isExpanded = true;
vm.$el.querySelector('.board-header').click();
 
Vue.nextTick(() => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
expect(vm.$el.classList.contains('is-collapsed')).toBe(false);
 
done();
});
});
 
it('created sets isExpanded to true from localStorage', done => {
vm.$el.querySelector('.board-header').click();
it('collapses when clicking the collapse icon', done => {
vm.list.isExpanded = true;
 
return Vue.nextTick()
Vue.nextTick()
.then(() => {
vm.$el.querySelector('.board-title-caret').click();
})
.then(() => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
done();
})
.catch(done.fail);
});
 
// call created manually
vm.$options.created[0].call(vm);
it('expands when clicking the expand icon', done => {
vm.list.isExpanded = false;
 
return Vue.nextTick();
Vue.nextTick()
.then(() => {
vm.$el.querySelector('.board-title-caret').click();
})
.then(() => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
expect(vm.$el.classList.contains('is-collapsed')).toBe(false);
done();
})
.catch(done.fail);
});
 
it('is expanded when created', () => {
expect(vm.list.isExpanded).toBe(true);
expect(vm.$el.classList.contains('is-collapsed')).toBe(false);
});
it('does render add issue button', () => {
expect(vm.$el.querySelector('.issue-count-badge-add-button')).not.toBeNull();
});
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment