Commit 73ab35bf authored by Simon Knox's avatar Simon Knox

Merge branch...

Merge branch '270583-improve-efficiency-when-creating-additional-boards-within-a-group-or-project' into 'master'

Boards - Remove default labels lists generation [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!49071
parents 920f4430 d619e43c
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import { mapGetters, mapActions, mapState } from 'vuex'; import { mapGetters, mapActions, mapState } from 'vuex';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_new.vue'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_new.vue';
import BoardList from './board_list_new.vue'; import BoardList from './board_list_new.vue';
import { ListType } from '../constants';
export default { export default {
components: { components: {
...@@ -36,16 +35,11 @@ export default { ...@@ -36,16 +35,11 @@ export default {
listIssues() { listIssues() {
return this.getIssuesByList(this.list.id); return this.getIssuesByList(this.list.id);
}, },
shouldFetchIssues() {
return this.list.type !== ListType.blank;
},
}, },
watch: { watch: {
filterParams: { filterParams: {
handler() { handler() {
if (this.shouldFetchIssues) { this.fetchIssuesForList({ listId: this.list.id });
this.fetchIssuesForList({ listId: this.list.id });
}
}, },
deep: true, deep: true,
immediate: true, immediate: true,
......
...@@ -72,9 +72,7 @@ export default { ...@@ -72,9 +72,7 @@ export default {
return this.list?.label?.description || this.list.title || ''; return this.list?.label?.description || this.list.title || '';
}, },
showListHeaderButton() { showListHeaderButton() {
return ( return !this.disabled && this.listType !== ListType.closed;
!this.disabled && this.listType !== ListType.closed && this.listType !== ListType.blank
);
}, },
showMilestoneListDetails() { showMilestoneListDetails() {
return ( return (
...@@ -106,9 +104,6 @@ export default { ...@@ -106,9 +104,6 @@ export default {
this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded
); );
}, },
showBoardListAndBoardInfo() {
return this.listType !== ListType.blank;
},
uniqueKey() { uniqueKey() {
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.listType}.${this.list.id}`; return `boards.${this.boardId}.${this.listType}.${this.list.id}`;
...@@ -286,7 +281,6 @@ export default { ...@@ -286,7 +281,6 @@ export default {
</gl-tooltip> </gl-tooltip>
<div <div
v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary" class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary"
:class="{ :class="{
'gl-display-none!': !list.isExpanded && isSwimlanesHeader, 'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
......
...@@ -75,9 +75,7 @@ export default { ...@@ -75,9 +75,7 @@ export default {
return this.list?.label?.description || this.list.title || ''; return this.list?.label?.description || this.list.title || '';
}, },
showListHeaderButton() { showListHeaderButton() {
return ( return !this.disabled && this.listType !== ListType.closed;
!this.disabled && this.listType !== ListType.closed && this.listType !== ListType.blank
);
}, },
showMilestoneListDetails() { showMilestoneListDetails() {
return ( return (
...@@ -111,9 +109,6 @@ export default { ...@@ -111,9 +109,6 @@ export default {
this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded
); );
}, },
showBoardListAndBoardInfo() {
return this.listType !== ListType.blank;
},
uniqueKey() { uniqueKey() {
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.listType}.${this.list.id}`; return `boards.${this.boardId}.${this.listType}.${this.list.id}`;
...@@ -299,7 +294,6 @@ export default { ...@@ -299,7 +294,6 @@ export default {
<!-- EE end --> <!-- EE end -->
<div <div
v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500" class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
:class="{ :class="{
'gl-display-none!': !list.isExpanded && isSwimlanesHeader, 'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
......
...@@ -9,7 +9,6 @@ export const ListType = { ...@@ -9,7 +9,6 @@ export const ListType = {
backlog: 'backlog', backlog: 'backlog',
closed: 'closed', closed: 'closed',
label: 'label', label: 'label',
blank: 'blank',
}; };
export const inactiveId = 0; export const inactiveId = 0;
...@@ -17,11 +16,7 @@ export const inactiveId = 0; ...@@ -17,11 +16,7 @@ export const inactiveId = 0;
export const ISSUABLE = 'issuable'; export const ISSUABLE = 'issuable';
export const LIST = 'list'; export const LIST = 'list';
/* eslint-disable-next-line @gitlab/require-i18n-strings */
export const DEFAULT_LABELS = ['to do', 'doing'];
export default { export default {
BoardType, BoardType,
ListType, ListType,
DEFAULT_LABELS,
}; };
...@@ -181,7 +181,6 @@ export default () => { ...@@ -181,7 +181,6 @@ export default () => {
.then(res => res.data) .then(res => res.data)
.then(lists => { .then(lists => {
lists.forEach(list => boardsStore.addList(list)); lists.forEach(list => boardsStore.addList(list));
boardsStore.addBlankState();
this.loading = false; this.loading = false;
}) })
.catch(() => { .catch(() => {
......
...@@ -3,7 +3,7 @@ import { pick } from 'lodash'; ...@@ -3,7 +3,7 @@ import { pick } from 'lodash';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { BoardType, ListType, inactiveId, DEFAULT_LABELS } from '~/boards/constants'; import { BoardType, ListType, inactiveId } from '~/boards/constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
formatBoardLists, formatBoardLists,
...@@ -89,7 +89,6 @@ export default { ...@@ -89,7 +89,6 @@ export default {
if (!lists.nodes.find(l => l.listType === ListType.backlog) && !hideBacklogList) { if (!lists.nodes.find(l => l.listType === ListType.backlog) && !hideBacklogList) {
dispatch('createList', { backlog: true }); dispatch('createList', { backlog: true });
} }
dispatch('generateDefaultLists');
}) })
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE)); .catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
}, },
...@@ -150,31 +149,6 @@ export default { ...@@ -150,31 +149,6 @@ export default {
.catch(() => commit(types.RECEIVE_LABELS_FAILURE)); .catch(() => commit(types.RECEIVE_LABELS_FAILURE));
}, },
generateDefaultLists: async ({ state, commit, dispatch }) => {
if (state.disabled) {
return;
}
if (
Object.entries(state.boardLists).find(
([, list]) => list.type !== ListType.backlog && list.type !== ListType.closed,
)
) {
return;
}
const fetchLabelsAndCreateList = label => {
return dispatch('fetchLabels', label)
.then(res => {
if (res.length > 0) {
dispatch('createList', { labelId: res[0].id });
}
})
.catch(() => commit(types.GENERATE_DEFAULT_LISTS_FAILURE));
};
await Promise.all(DEFAULT_LABELS.map(fetchLabelsAndCreateList));
},
moveList: ( moveList: (
{ state, commit, dispatch }, { state, commit, dispatch },
{ listId, replacedListId, newIndex, adjustmentValue }, { listId, replacedListId, newIndex, adjustmentValue },
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
/* global ListIssue */ /* global ListIssue */
import { sortBy, pick } from 'lodash'; import { sortBy, pick } from 'lodash';
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { import {
urlParamsToObject, urlParamsToObject,
...@@ -125,20 +124,6 @@ const boardsStore = { ...@@ -125,20 +124,6 @@ const boardsStore = {
.querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`) .querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
?.classList.remove('is-active'); ?.classList.remove('is-active');
}, },
shouldAddBlankState() {
// Decide whether to add the blank state
return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0];
},
addBlankState() {
if (!this.shouldAddBlankState() || this.welcomeIsHidden()) return;
this.generateDefaultLists()
.then(res => res.data)
.then(data => Promise.all(data.map(list => this.addList(list))))
.catch(() => {
this.removeList(undefined, 'label');
});
},
findIssueLabel(issue, findLabel) { findIssueLabel(issue, findLabel) {
return issue.labels.find(label => label.id === findLabel.id); return issue.labels.find(label => label.id === findLabel.id);
...@@ -202,9 +187,6 @@ const boardsStore = { ...@@ -202,9 +187,6 @@ const boardsStore = {
return list.issues.find(issue => issue.id === id); return list.issues.find(issue => issue.id === id);
}, },
welcomeIsHidden() {
return parseBoolean(Cookies.get('issue_board_welcome_hidden'));
},
removeList(id, type = 'blank') { removeList(id, type = 'blank') {
const list = this.findList('id', id, type); const list = this.findList('id', id, type);
...@@ -562,10 +544,6 @@ const boardsStore = { ...@@ -562,10 +544,6 @@ const boardsStore = {
return axios.get(this.state.endpoints.listsEndpoint); return axios.get(this.state.endpoints.listsEndpoint);
}, },
generateDefaultLists() {
return axios.post(this.state.endpoints.listsEndpointGenerate, {});
},
createList(entityId, entityType) { createList(entityId, entityType) {
const list = { const list = {
[entityType]: entityId, [entityType]: entityId,
......
...@@ -32,11 +32,10 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter ...@@ -32,11 +32,10 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
export default { export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) { [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, showPromotion, ...endpoints } = data; const { boardType, disabled, ...endpoints } = data;
state.endpoints = endpoints; state.endpoints = endpoints;
state.boardType = boardType; state.boardType = boardType;
state.disabled = disabled; state.disabled = disabled;
state.showPromotion = showPromotion;
}, },
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => { [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
......
...@@ -4,7 +4,6 @@ export default () => ({ ...@@ -4,7 +4,6 @@ export default () => ({
endpoints: {}, endpoints: {},
boardType: null, boardType: null,
disabled: false, disabled: false,
showPromotion: false,
isShowingLabels: true, isShowingLabels: true,
activeId: inactiveId, activeId: inactiveId,
sidebarType: '', sidebarType: '',
......
...@@ -71,8 +71,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -71,8 +71,6 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute @project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved? if @project.saved?
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.zone.at(0) }
redirect_to( redirect_to(
project_path(@project, custom_import_params), project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
......
---
title: Boards - Remove default labels lists generation
merge_request: 49071
author:
type: changed
...@@ -394,19 +394,6 @@ status. ...@@ -394,19 +394,6 @@ status.
If you're not able to do some of the things above, make sure you have the right If you're not able to do some of the things above, make sure you have the right
[permissions](#permissions). [permissions](#permissions).
### First time using an issue board
> The automatic creation of the **To Do** and **Doing** lists was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202144) in GitLab 13.5.
The first time you open an issue board, you are presented with the default lists
(**Open**, **To Do**, **Doing**, and **Closed**).
If the **To Do** and **Doing** labels don't exist in the project or group, they are created, and
their lists appear as empty. If any of them already exists, the list is filled with the issues that
have that label.
![issue board default lists](img/issue_board_default_lists_v13_4.png)
### Create a new list ### Create a new list
Create a new list by clicking the **Add list** dropdown button in the upper right corner of the issue board. Create a new list by clicking the **Add list** dropdown button in the upper right corner of the issue board.
...@@ -566,6 +553,22 @@ To select and move multiple cards: ...@@ -566,6 +553,22 @@ To select and move multiple cards:
![Multi-select Issue Cards](img/issue_boards_multi_select_v12_4.png) ![Multi-select Issue Cards](img/issue_boards_multi_select_v12_4.png)
### First time using an issue board
> - The automatic creation of the **To Do** and **Doing** lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202144) in GitLab 13.5.
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/270583) in GitLab 13.7. In GitLab 13.7 and later, the **To Do** and **Doing** columns are not automatically created.
WARNING:
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/270583) in GitLab 13.7.
The **To Do** and **Doing** columns are no longer automatically created.
In GitLab 13.5 and 13.6, the first time you open an issue board, you are presented with the default lists
(**Open**, **To Do**, **Doing**, and **Closed**).
If the **To Do** and **Doing** labels don't exist in the project or group, they are created, and
their lists appear as empty. If any of them already exists, the list is filled with the issues that
have that label.
## Tips ## Tips
A few things to remember: A few things to remember:
......
...@@ -245,7 +245,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -245,7 +245,7 @@ RSpec.describe 'Scoped issue boards', :js do
find('.board-card', match: :first) find('.board-card', match: :first)
expect(page).to have_selector('.board', count: 4) expect(page).to have_selector('.board', count: 2)
expect(all('.board').first).to have_selector('.board-card', count: 2) expect(all('.board').first).to have_selector('.board-card', count: 2)
expect(all('.board').last).to have_selector('.board-card', count: 1) expect(all('.board').last).to have_selector('.board-card', count: 1)
end end
......
...@@ -76,7 +76,7 @@ describe('Board List Header Component', () => { ...@@ -76,7 +76,7 @@ describe('Board List Header Component', () => {
describe('Settings Button', () => { describe('Settings Button', () => {
const hasSettings = [ListType.assignee, ListType.milestone, ListType.label]; const hasSettings = [ListType.assignee, ListType.milestone, ListType.label];
const hasNoSettings = [ListType.backlog, ListType.blank, ListType.closed]; const hasNoSettings = [ListType.backlog, ListType.closed];
it.each(hasSettings)('does render for List Type `%s`', listType => { it.each(hasSettings)('does render for List Type `%s`', listType => {
createComponent({ listType }); createComponent({ listType });
......
...@@ -82,7 +82,7 @@ describe('Board List Header Component', () => { ...@@ -82,7 +82,7 @@ describe('Board List Header Component', () => {
describe('Settings Button', () => { describe('Settings Button', () => {
const hasSettings = [ListType.assignee, ListType.milestone, ListType.label]; const hasSettings = [ListType.assignee, ListType.milestone, ListType.label];
const hasNoSettings = [ListType.backlog, ListType.blank, ListType.closed]; const hasNoSettings = [ListType.backlog, ListType.closed];
it.each(hasSettings)('does render for List Type `%s`', listType => { it.each(hasSettings)('does render for List Type `%s`', listType => {
createComponent({ listType }); createComponent({ listType });
......
...@@ -27,11 +27,11 @@ RSpec.describe 'Issue Boards', :js do ...@@ -27,11 +27,11 @@ RSpec.describe 'Issue Boards', :js do
end end
it 'creates default lists' do it 'creates default lists' do
lists = ['Open', 'To Do', 'Doing', 'Closed'] lists = %w[Open Closed]
wait_for_requests wait_for_requests
expect(page).to have_selector('.board', count: 4) expect(page).to have_selector('.board', count: 2)
page.all('.board').each_with_index do |list, i| page.all('.board').each_with_index do |list, i|
expect(list.find('.board-title')).to have_content(lists[i]) expect(list.find('.board-title')).to have_content(lists[i])
......
...@@ -66,23 +66,6 @@ describe('boardsStore', () => { ...@@ -66,23 +66,6 @@ describe('boardsStore', () => {
}); });
}); });
describe('generateDefaultLists', () => {
const listsEndpointGenerate = `${endpoints.listsEndpoint}/generate.json`;
it('makes a request to generate default lists', () => {
axiosMock.onPost(listsEndpointGenerate).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.generateDefaultLists()).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onPost(listsEndpointGenerate).replyOnce(500);
return expect(boardsStore.generateDefaultLists()).rejects.toThrow();
});
});
describe('createList', () => { describe('createList', () => {
const entityType = 'moorhen'; const entityType = 'moorhen';
const entityId = 'quack'; const entityId = 'quack';
...@@ -727,24 +710,6 @@ describe('boardsStore', () => { ...@@ -727,24 +710,6 @@ describe('boardsStore', () => {
}); });
}); });
it('check for blank state adding', () => {
expect(boardsStore.shouldAddBlankState()).toBe(true);
});
it('check for blank state not adding', () => {
boardsStore.addList(listObj);
expect(boardsStore.shouldAddBlankState()).toBe(false);
});
it('check for blank state adding when closed list exist', () => {
boardsStore.addList({
list_type: 'closed',
});
expect(boardsStore.shouldAddBlankState()).toBe(true);
});
it('removes list from state', () => { it('removes list from state', () => {
boardsStore.addList(listObj); boardsStore.addList(listObj);
......
...@@ -79,7 +79,7 @@ describe('Board List Header Component', () => { ...@@ -79,7 +79,7 @@ describe('Board List Header Component', () => {
const findCaret = () => wrapper.find('.board-title-caret'); const findCaret = () => wrapper.find('.board-title-caret');
describe('Add issue button', () => { describe('Add issue button', () => {
const hasNoAddButton = [ListType.blank, ListType.closed]; const hasNoAddButton = [ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee]; const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => { it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
......
...@@ -73,7 +73,7 @@ describe('Board List Header Component', () => { ...@@ -73,7 +73,7 @@ describe('Board List Header Component', () => {
const findCaret = () => wrapper.find('.board-title-caret'); const findCaret = () => wrapper.find('.board-title-caret');
describe('Add issue button', () => { describe('Add issue button', () => {
const hasNoAddButton = [ListType.blank, ListType.closed]; const hasNoAddButton = [ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee]; const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => { it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
......
...@@ -123,7 +123,7 @@ describe('fetchLists', () => { ...@@ -123,7 +123,7 @@ describe('fetchLists', () => {
payload: formattedLists, payload: formattedLists,
}, },
], ],
[{ type: 'generateDefaultLists' }], [],
done, done,
); );
}); });
...@@ -153,37 +153,12 @@ describe('fetchLists', () => { ...@@ -153,37 +153,12 @@ describe('fetchLists', () => {
payload: formattedLists, payload: formattedLists,
}, },
], ],
[{ type: 'createList', payload: { backlog: true } }, { type: 'generateDefaultLists' }], [{ type: 'createList', payload: { backlog: true } }],
done, done,
); );
}); });
}); });
describe('generateDefaultLists', () => {
let store;
beforeEach(() => {
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
disabled: false,
boardLists: [{ type: 'backlog' }, { type: 'closed' }],
};
store = {
commit: jest.fn(),
dispatch: jest.fn(() => Promise.resolve()),
state,
};
});
it('should dispatch fetchLabels', () => {
return actions.generateDefaultLists(store).then(() => {
expect(store.dispatch.mock.calls[0]).toEqual(['fetchLabels', 'to do']);
expect(store.dispatch.mock.calls[1]).toEqual(['fetchLabels', 'doing']);
});
});
});
describe('createList', () => { describe('createList', () => {
it('should dispatch addList action when creating backlog list', done => { it('should dispatch addList action when creating backlog list', done => {
const backlogList = { const backlogList = {
......
...@@ -33,19 +33,16 @@ describe('Board Store Mutations', () => { ...@@ -33,19 +33,16 @@ describe('Board Store Mutations', () => {
}; };
const boardType = 'group'; const boardType = 'group';
const disabled = false; const disabled = false;
const showPromotion = false;
mutations[types.SET_INITIAL_BOARD_DATA](state, { mutations[types.SET_INITIAL_BOARD_DATA](state, {
...endpoints, ...endpoints,
boardType, boardType,
disabled, disabled,
showPromotion,
}); });
expect(state.endpoints).toEqual(endpoints); expect(state.endpoints).toEqual(endpoints);
expect(state.boardType).toEqual(boardType); expect(state.boardType).toEqual(boardType);
expect(state.disabled).toEqual(disabled); expect(state.disabled).toEqual(disabled);
expect(state.showPromotion).toEqual(showPromotion);
}); });
}); });
......
...@@ -85,7 +85,7 @@ RSpec.shared_examples 'multiple issue boards' do ...@@ -85,7 +85,7 @@ RSpec.shared_examples 'multiple issue boards' do
wait_for_requests wait_for_requests
expect(page).to have_selector('.board', count: 5) expect(page).to have_selector('.board', count: 3)
in_boards_switcher_dropdown do in_boards_switcher_dropdown do
click_link board.name click_link board.name
...@@ -93,7 +93,7 @@ RSpec.shared_examples 'multiple issue boards' do ...@@ -93,7 +93,7 @@ RSpec.shared_examples 'multiple issue boards' do
wait_for_requests wait_for_requests
expect(page).to have_selector('.board', count: 4) expect(page).to have_selector('.board', count: 2)
end end
it 'maintains sidebar state over board switch' do it 'maintains sidebar state over board switch' do
......
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