Commit f452c1aa authored by Phil Hughes's avatar Phil Hughes

Expand/collapse close & backlog lists in issue boards

The closed & backlog lists in issue boards are no collapsible. They can
be collapsed independently of each other & this selection is then saved
to the browser through localStorage. When the page loads, the code gets
the data from localStorage & determines whether to show or hide the list

Closes #23917
parent 8039b9c3
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list';
import boardBlankState from './board_blank_state';
import './board_delete';
......@@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({
disabled: Boolean,
issueLinkBase: String,
rootPath: String,
boardId: {
type: String,
required: true,
},
},
data () {
return {
......@@ -78,7 +83,13 @@ gl.issueBoards.Board = Vue.extend({
methods: {
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')) {
this.list.isExpanded = !this.list.isExpanded;
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
}
},
},
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
......@@ -102,4 +113,11 @@ gl.issueBoards.Board = Vue.extend({
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
},
});
......@@ -12,7 +12,9 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1;
this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1;
this.isExpanded = true;
this.page = 1;
this.loading = true;
this.loadingMore = false;
......
......@@ -31,10 +31,14 @@ gl.issueBoards.BoardsStore = {
},
new (listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
.then(() => {
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position');
})
.catch(() => {
......@@ -47,7 +51,7 @@ gl.issueBoards.BoardsStore = {
},
shouldAddBlankState () {
// Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
......@@ -100,7 +104,7 @@ gl.issueBoards.BoardsStore = {
issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'closed') {
if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
......
......@@ -96,9 +96,51 @@
@media (min-width: $screen-sm-min) {
width: 400px;
}
&.is-expandable {
.board-header {
cursor: pointer;
}
}
&.is-collapsed {
width: 60px;
.board-header {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.board-title {
position: initial;
padding: 0;
border-bottom: 0;
> span {
display: block;
transform: rotate(90deg) translate(25px, 0);
}
}
.board-title-expandable-toggle {
position: absolute;
top: 50%;
left: 50%;
margin-left: -10px;
}
.board-list-component,
.board-issue-count-holder {
display: none;
}
}
}
.board-inner {
position: relative;
height: 100%;
font-size: $issue-boards-font-size;
background: $gray-light;
......
......@@ -5,6 +5,8 @@ module Projects
before_action :authorize_read_list!, only: [:index]
def index
board.lists.create(list_type: :backlog) unless board.lists.backlog.any?
render json: serialize_as_json(board.lists)
end
......
......@@ -5,6 +5,10 @@ class Board < ActiveRecord::Base
validates :project, presence: true
def backlog_list
lists.merge(List.backlog).take
end
def closed_list
lists.merge(List.closed).take
end
......
......@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
enum list_type: { label: 1, closed: 2 }
enum list_type: { backlog: 0, label: 1, closed: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
......
......@@ -12,6 +12,7 @@ module Boards
def create_board!
board = project.boards.create
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :closed)
board
......
......@@ -3,7 +3,7 @@ module Boards
class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless list
issues = without_board_labels(issues) unless movable_list?
issues = with_list_label(issues) if movable_list?
issues.order_by_position_and_priority
end
......
......@@ -26,6 +26,7 @@
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
":board-id" => "boardId",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
%board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
......
.board{ ":class" => '{ "is-draggable": !list.preset }',
.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }',
":data-id" => "list.id" }
.board-inner
%header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
%h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
%i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable",
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-left\": !list.isExpanded }",
"aria-hidden": "true" }
%span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" } }
{{ list.title }}
......@@ -10,13 +13,13 @@
%span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
%button.btn.btn-small.btn-default.pull-right.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
"aria-label" => "New issue",
"title" => "New issue",
data: { placement: "top", container: "body" } }
= icon("plus")
= icon("plus", class: "js-no-trigger-collapse")
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
......
---
title: Expand/collapse backlog & closed lists in issue boards
merge_request:
author:
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