summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Greiling <mike@pixelcog.com>2019-06-28 21:20:06 +0000
committerMike Greiling <mike@pixelcog.com>2019-06-28 21:20:06 +0000
commitdf717efaabd9ab11806495244e1e5fa515c01f1f (patch)
tree8627d217c8caa9e01dbb8bb76836d93f8d5b1b4e
parent388a496443cf8b86e72c275ca63b14e8f059c8f6 (diff)
parent5c078128351f4100b916eb8b70a6954afc6d926b (diff)
downloadgitlab-ce-df717efaabd9ab11806495244e1e5fa515c01f1f.tar.gz
Merge branch 'mh/collapsible-boards' into 'master'
Allow collapsing all issue boards Closes #40630 See merge request gitlab-org/gitlab-ce!29955
-rw-r--r--app/assets/javascripts/boards/components/board.js40
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js8
-rw-r--r--app/assets/stylesheets/pages/boards.scss72
-rw-r--r--app/views/shared/boards/components/_board.html.haml58
-rw-r--r--changelogs/unreleased/mh-collapsible-boards.yml5
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/javascripts/boards/components/board_spec.js61
8 files changed, 168 insertions, 84 deletions
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 45b9e57f9ab..c6122fbc686 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,6 +1,7 @@
+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';
@@ -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: {
@@ -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;
}
@@ -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');
}
},
},
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index 636ca99952c..68ea28e68d9 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -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,
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index a9d88f19146..cd553d0c4af 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -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 {
@@ -249,7 +255,7 @@ class List {
}
getTypeInfo(type) {
- return TYPES[type] || {};
+ return TYPES[type] || TYPES.default;
}
onNewIssueResponse(issue, data) {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 5e3652db48f..343cca96851 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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 {
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index f9cfcabc015..fdb2a0a1843 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -1,52 +1,60 @@
.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",
diff --git a/changelogs/unreleased/mh-collapsible-boards.yml b/changelogs/unreleased/mh-collapsible-boards.yml
new file mode 100644
index 00000000000..b69d6e81cc4
--- /dev/null
+++ b/changelogs/unreleased/mh-collapsible-boards.yml
@@ -0,0 +1,5 @@
+---
+title: Labeled issue boards can now collapse
+merge_request: 29955
+author:
+type: added
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 840664298d3..a04a0acd98e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js
index d08ee41802b..683783334c6 100644
--- a/spec/javascripts/boards/components/board_spec.js
+++ b/spec/javascripts/boards/components/board_spec.js
@@ -1,7 +1,6 @@
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', () => {
@@ -27,7 +26,6 @@ describe('Board component', () => {
disabled: false,
issueLinkBase: '/',
rootPath: '/',
- // eslint-disable-next-line no-undef
list: new List({
id: 1,
position: 0,
@@ -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();
});