Commit cb85dab0 authored by Scott Stern's avatar Scott Stern Committed by Kushal Pandya

Add foundation for saving collapsed state based on current user

Add boilerplate and update backend to
support saving board list state to backend
parent 5bd93d1e
...@@ -50,6 +50,9 @@ export default Vue.extend({ ...@@ -50,6 +50,9 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
isLoggedIn() {
return Boolean(gon.current_user_id);
},
counterTooltip() { counterTooltip() {
const { issuesSize } = this.list; const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`; return `${n__('%d issue', '%d issues', issuesSize)}`;
...@@ -106,7 +109,11 @@ export default Vue.extend({ ...@@ -106,7 +109,11 @@ export default Vue.extend({
Sortable.create(this.$el.parentNode, sortableOptions); Sortable.create(this.$el.parentNode, sortableOptions);
}, },
created() { created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { if (
this.list.isExpandable &&
AccessorUtilities.isLocalStorageAccessSafe() &&
!this.isLoggedIn
) {
const isCollapsed = localStorage.getItem(`${this.uniqueKey}.expanded`) === 'false'; const isCollapsed = localStorage.getItem(`${this.uniqueKey}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed; this.list.isExpanded = !isCollapsed;
...@@ -120,10 +127,14 @@ export default Vue.extend({ ...@@ -120,10 +127,14 @@ export default Vue.extend({
if (this.list.isExpandable) { if (this.list.isExpandable) {
this.list.isExpanded = !this.list.isExpanded; this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) { if (AccessorUtilities.isLocalStorageAccessSafe() && !this.isLoggedIn) {
localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); 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. // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips. // Close all tooltips manually to prevent dangling tooltips.
$('.tooltip').tooltip('hide'); $('.tooltip').tooltip('hide');
......
...@@ -45,7 +45,7 @@ class List { ...@@ -45,7 +45,7 @@ class List {
const typeInfo = this.getTypeInfo(this.type); const typeInfo = this.getTypeInfo(this.type);
this.preset = Boolean(typeInfo.isPreset); this.preset = Boolean(typeInfo.isPreset);
this.isExpandable = Boolean(typeInfo.isExpandable); this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = true; this.isExpanded = !obj.collapsed;
this.page = 1; this.page = 1;
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
...@@ -113,7 +113,8 @@ class List { ...@@ -113,7 +113,8 @@ class List {
} }
update() { update() {
gl.boardService.updateList(this.id, this.position).catch(() => { const collapsed = !this.isExpanded;
return gl.boardService.updateList(this.id, this.position, collapsed).catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
} }
......
...@@ -32,8 +32,8 @@ export default class BoardService { ...@@ -32,8 +32,8 @@ export default class BoardService {
return boardsStore.createList(entityId, entityType); return boardsStore.createList(entityId, entityType);
} }
updateList(id, position) { updateList(id, position, collapsed) {
return boardsStore.updateList(id, position); return boardsStore.updateList(id, position, collapsed);
} }
destroyList(id) { destroyList(id) {
......
...@@ -278,10 +278,11 @@ const boardsStore = { ...@@ -278,10 +278,11 @@ const boardsStore = {
}); });
}, },
updateList(id, position) { updateList(id, position, collapsed) {
return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, { return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, {
list: { list: {
position, position,
collapsed,
}, },
}); });
}, },
......
...@@ -25,7 +25,7 @@ module Boards ...@@ -25,7 +25,7 @@ module Boards
end end
def update def update
list = board.lists.movable.find(params[:id]) list = board.lists.find(params[:id])
service = Boards::Lists::UpdateService.new(board_parent, current_user, update_list_params) service = Boards::Lists::UpdateService.new(board_parent, current_user, update_list_params)
result = service.execute(list) result = service.execute(list)
......
---
title: Frontend support for saving issue board preferences on the current user
merge_request: 16421
author:
type: changed
...@@ -185,6 +185,24 @@ describe Boards::ListsController do ...@@ -185,6 +185,24 @@ describe Boards::ListsController do
end end
end end
context 'with a list_type other than :label' do
let!(:closed) { create(:closed_list, board: board, position: 2) }
it 'saves collapsed preference for user' do
save_setting user: user, board: board, list: closed, setting: { collapsed: true }
expect(closed.preferences_for(user).collapsed).to eq(true)
expect(response).to have_gitlab_http_status(200)
end
it 'saves not collapsed preference for user' do
save_setting user: user, board: board, list: closed, setting: { collapsed: false }
expect(closed.preferences_for(user).collapsed).to eq(false)
expect(response).to have_gitlab_http_status(200)
end
end
def move(user:, board:, list:, position:) def move(user:, board:, list:, position:)
sign_in(user) sign_in(user)
......
...@@ -97,8 +97,9 @@ describe('BoardService', () => { ...@@ -97,8 +97,9 @@ describe('BoardService', () => {
describe('updateList', () => { describe('updateList', () => {
const id = 'David Webb'; const id = 'David Webb';
const position = 'unknown'; const position = 'unknown';
const collapsed = false;
const expectedRequest = expect.objectContaining({ const expectedRequest = expect.objectContaining({
data: JSON.stringify({ list: { position } }), data: JSON.stringify({ list: { position, collapsed } }),
}); });
let requestSpy; let requestSpy;
...@@ -112,7 +113,7 @@ describe('BoardService', () => { ...@@ -112,7 +113,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([200, dummyResponse]); requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse }); const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(service.updateList(id, position)) return expect(service.updateList(id, position, collapsed))
.resolves.toEqual(expectedResponse) .resolves.toEqual(expectedResponse)
.then(() => { .then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest); expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
...@@ -122,7 +123,7 @@ describe('BoardService', () => { ...@@ -122,7 +123,7 @@ describe('BoardService', () => {
it('fails for error response', () => { it('fails for error response', () => {
requestSpy.mockReturnValue([500]); requestSpy.mockReturnValue([500]);
return expect(service.updateList(id, position)) return expect(service.updateList(id, position, collapsed))
.rejects.toThrow() .rejects.toThrow()
.then(() => { .then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest); expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
......
...@@ -5,21 +5,16 @@ import { mockBoardService } from '../mock_data'; ...@@ -5,21 +5,16 @@ import { mockBoardService } from '../mock_data';
describe('Board component', () => { describe('Board component', () => {
let vm; let vm;
let el;
beforeEach(done => { const createComponent = ({ gon = {}, collapsed = false, listType = 'backlog' } = {}) => {
loadFixtures('boards/show.html'); if (Object.prototype.hasOwnProperty.call(gon, 'current_user_id')) {
window.gon = gon;
el = document.createElement('div'); } else {
window.gon = {};
}
const el = document.createElement('div');
document.body.appendChild(el); document.body.appendChild(el);
gl.boardService = mockBoardService({
boardsEndpoint: '/',
listsEndpoint: '/',
bulkUpdatePath: '/',
boardId: 1,
});
vm = new Board({ vm = new Board({
propsData: { propsData: {
boardId: '1', boardId: '1',
...@@ -30,94 +25,244 @@ describe('Board component', () => { ...@@ -30,94 +25,244 @@ describe('Board component', () => {
id: 1, id: 1,
position: 0, position: 0,
title: 'test', title: 'test',
list_type: 'backlog', list_type: listType,
collapsed,
}), }),
}, },
}).$mount(el); }).$mount(el);
};
const setUpTests = (done, opts = {}) => {
loadFixtures('boards/show.html');
gl.boardService = mockBoardService({
boardsEndpoint: '/',
listsEndpoint: '/',
bulkUpdatePath: '/',
boardId: 1,
});
createComponent(opts);
Vue.nextTick(done); Vue.nextTick(done);
}); };
const cleanUpTests = spy => {
if (spy) {
spy.calls.reset();
}
afterEach(() => {
vm.$destroy(); vm.$destroy();
// remove the component from the DOM // remove the component from the DOM
document.querySelector('.board').remove(); document.querySelector('.board').remove();
localStorage.removeItem(`boards.${vm.boardId}.${vm.list.type}.expanded`); localStorage.removeItem(`${vm.uniqueKey}.expanded`);
}); };
it('board is expandable when list type is backlog', () => { describe('List', () => {
expect(vm.$el.classList.contains('is-expandable')).toBe(true); beforeEach(() => {
}); gl.boardService = mockBoardService({
boardsEndpoint: '/',
listsEndpoint: '/',
bulkUpdatePath: '/',
boardId: 1,
});
});
it('board is expandable when list type is closed', () => {
expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true);
});
it('board is expandable when list type is label', () => {
expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true);
});
it('board is expandable when list type is closed', () => { it('board is not expandable when list type is blank', () => {
expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true); expect(new List({ id: 1, list_type: 'blank' }).isExpandable).toBe(false);
});
}); });
it('board is expandable when list type is label', () => { describe('when clicking the header', () => {
expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true); beforeEach(done => {
setUpTests(done);
});
afterEach(() => {
cleanUpTests();
});
it('does not collapse', done => {
vm.list.isExpanded = true;
vm.$el.querySelector('.board-header').click();
Vue.nextTick()
.then(() => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(false);
})
.then(done)
.catch(done.fail);
});
}); });
it('board is not expandable when list type is blank', () => { describe('when clicking the collapse icon', () => {
expect(new List({ id: 1, list_type: 'blank' }).isExpandable).toBe(false); beforeEach(done => {
setUpTests(done);
});
afterEach(() => {
cleanUpTests();
});
it('collapses', done => {
Vue.nextTick()
.then(() => {
vm.$el.querySelector('.board-title-caret').click();
})
.then(() => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
})
.then(done)
.catch(done.fail);
});
}); });
it('does not collapse when clicking header', done => { describe('when clicking the expand icon', () => {
vm.list.isExpanded = true; beforeEach(done => {
vm.$el.querySelector('.board-header').click(); setUpTests(done);
});
Vue.nextTick(() => { afterEach(() => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(false); cleanUpTests();
});
it('expands', done => {
vm.list.isExpanded = false;
done(); Vue.nextTick()
.then(() => {
vm.$el.querySelector('.board-title-caret').click();
})
.then(() => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(false);
})
.then(done)
.catch(done.fail);
}); });
}); });
it('collapses when clicking the collapse icon', done => { describe('when collapsed is false', () => {
vm.list.isExpanded = true; beforeEach(done => {
setUpTests(done);
});
Vue.nextTick() afterEach(() => {
.then(() => { cleanUpTests();
vm.$el.querySelector('.board-title-caret').click(); });
})
.then(() => { it('is expanded when collapsed is false', () => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(true); expect(vm.list.isExpanded).toBe(true);
done(); expect(vm.$el.classList.contains('is-collapsed')).toBe(false);
}) });
.catch(done.fail);
}); });
it('expands when clicking the expand icon', done => { describe('when list type is blank', () => {
vm.list.isExpanded = false; beforeEach(done => {
setUpTests(done, { listType: 'blank' });
});
afterEach(() => {
cleanUpTests();
});
it('does not render add issue button when list type is blank', done => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.issue-count-badge-add-button')).toBeNull();
Vue.nextTick()
.then(() => {
vm.$el.querySelector('.board-title-caret').click();
})
.then(() => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(false);
done(); done();
}) });
.catch(done.fail); });
}); });
it('is expanded when created', () => { describe('when list type is backlog', () => {
expect(vm.list.isExpanded).toBe(true); beforeEach(done => {
expect(vm.$el.classList.contains('is-collapsed')).toBe(false); setUpTests(done);
});
afterEach(() => {
cleanUpTests();
});
it('board is expandable', () => {
expect(vm.$el.classList.contains('is-expandable')).toBe(true);
});
}); });
it('does render add issue button', () => { describe('when logged in', () => {
expect(vm.$el.querySelector('.issue-count-badge-add-button')).not.toBeNull(); let spy;
beforeEach(done => {
spy = spyOn(List.prototype, 'update');
setUpTests(done, { gon: { current_user_id: 1 } });
});
afterEach(() => {
cleanUpTests(spy);
});
it('calls list update', done => {
Vue.nextTick()
.then(() => {
vm.$el.querySelector('.board-title-caret').click();
})
.then(() => {
expect(vm.list.update).toHaveBeenCalledTimes(1);
})
.then(done)
.catch(done.fail);
});
}); });
it('does not render add issue button when list type is blank', done => { describe('when logged out', () => {
vm.list.type = 'blank'; let spy;
beforeEach(done => {
spy = spyOn(List.prototype, 'update');
setUpTests(done, { collapsed: false });
});
afterEach(() => {
cleanUpTests(spy);
});
Vue.nextTick(() => { // can only be one or the other cant toggle window.gon.current_user_id states.
expect(vm.$el.querySelector('.issue-count-badge-add-button')).toBeNull(); it('clicking on the caret does not call list update', done => {
Vue.nextTick()
.then(() => {
vm.$el.querySelector('.board-title-caret').click();
})
.then(() => {
expect(vm.list.update).toHaveBeenCalledTimes(0);
})
.then(done)
.catch(done.fail);
});
it('sets expanded to be the opposite of its value when toggleExpanded is called', done => {
const expanded = true;
vm.list.isExpanded = expanded;
vm.toggleExpanded();
Vue.nextTick()
.then(() => {
expect(vm.list.isExpanded).toBe(!expanded);
expect(localStorage.getItem(`${vm.uniqueKey}.expanded`)).toBe(String(!expanded));
})
.then(done)
.catch(done.fail);
});
done(); it('does render add issue button', () => {
expect(vm.$el.querySelector('.issue-count-badge-add-button')).not.toBeNull();
}); });
}); });
}); });
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