Commit 462506f8 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch 'jestodous-refactor-board-to-sfc' into 'master'

Refactor board.js to Vue single file component

See merge request gitlab-org/gitlab!27691
parents 9974d366 88eadb00
...@@ -16,6 +16,14 @@ import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_ ...@@ -16,6 +16,14 @@ import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_
import { ListType } from '../constants'; import { ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
/**
* Please don't edit this file, have a look at:
* ./board_column.vue
* https://gitlab.com/gitlab-org/gitlab/-/issues/212300
*
* This file here will be deleted soon
* @deprecated
*/
export default Vue.extend({ export default Vue.extend({
components: { components: {
BoardBlankState, BoardBlankState,
...@@ -54,6 +62,13 @@ export default Vue.extend({ ...@@ -54,6 +62,13 @@ export default Vue.extend({
type: String, type: String,
required: true, required: true,
}, },
// Does not do anything but is used
// to support the API of the new board_column.vue
canAdminList: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
......
This diff is collapsed.
...@@ -3,7 +3,6 @@ import Vue from 'vue'; ...@@ -3,7 +3,6 @@ import Vue from 'vue';
import 'ee_else_ce/boards/models/issue'; import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list'; import 'ee_else_ce/boards/models/list';
import Board from 'ee_else_ce/boards/components/board';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar'; import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle'; import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
...@@ -65,7 +64,15 @@ export default () => { ...@@ -65,7 +64,15 @@ export default () => {
issueBoardsApp = new Vue({ issueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
Board, Board: () =>
window?.gon?.features?.sfcIssueBoards
? import('ee_else_ce/boards/components/board_column.vue')
: /**
* Please have a look at, we are moving to the SFC soon:
* https://gitlab.com/gitlab-org/gitlab/-/issues/212300
* @deprecated
*/
import('ee_else_ce/boards/components/board'),
BoardSidebar, BoardSidebar,
BoardAddIssuesModal, BoardAddIssuesModal,
BoardSettingsSidebar: () => BoardSettingsSidebar: () =>
......
...@@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController ...@@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true) push_frontend_feature_flag(:multi_select_board, default_enabled: true)
push_frontend_feature_flag(:sfc_issue_boards, default_enabled: true)
end end
private private
......
...@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true) push_frontend_feature_flag(:multi_select_board, default_enabled: true)
push_frontend_feature_flag(:sfc_issue_boards, default_enabled: true)
end end
private private
......
- board = local_assigns.fetch(:board, nil) - board = local_assigns.fetch(:board, nil)
- group = local_assigns.fetch(:group, false) - group = local_assigns.fetch(:group, false)
-# TODO: Move group_id and can_admin_list to the board store
See: https://gitlab.com/gitlab-org/gitlab/-/issues/213082
- group_id = @group&.id || "null"
- can_admin_list = can?(current_user, :admin_list, current_board_parent) == true
- @no_breadcrumb_container = true - @no_breadcrumb_container = true
- @no_container = true - @no_container = true
- @content_class = "issue-boards-content js-focus-mode-board" - @content_class = "issue-boards-content js-focus-mode-board"
...@@ -22,6 +26,8 @@ ...@@ -22,6 +26,8 @@
%board{ "v-cloak" => "true", %board{ "v-cloak" => "true",
"v-for" => "list in state.lists", "v-for" => "list in state.lists",
"ref" => "board", "ref" => "board",
":can-admin-list" => can_admin_list,
":group-id" => group_id,
":list" => "list", ":list" => "list",
":disabled" => "disabled", ":disabled" => "disabled",
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
......
-# Please have a look at app/assets/javascripts/boards/components/board_column.vue
This haml file is deprecated and will be deleted soon, please change the Vue app
https://gitlab.com/gitlab-org/gitlab/-/issues/212300
.board.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" }', .board.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", data: { qa_selector: "board_list" } } ":data-id" => "list.id", data: { qa_selector: "board_list" } }
.board-inner.d-flex.flex-column.position-relative.h-100.rounded .board-inner.d-flex.flex-column.position-relative.h-100.rounded
......
...@@ -5,6 +5,12 @@ import Board from '~/boards/components/board'; ...@@ -5,6 +5,12 @@ import Board from '~/boards/components/board';
import { __, sprintf, s__ } from '~/locale'; import { __, sprintf, s__ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
/**
* Please have a look at:
* ./board_column.vue
* https://gitlab.com/gitlab-org/gitlab/-/issues/212300
* @deprecated
*/
export default Board.extend({ export default Board.extend({
data() { data() {
return { return {
......
<script>
import { mapActions } from 'vuex';
import BoardColumnFoss from '~/boards/components/board_column.vue';
import { __, sprintf, s__ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import BoardPromotionState from 'ee/boards/components/board_promotion_state';
export default {
components: {
BoardPromotionState,
},
extends: BoardColumnFoss,
data() {
return {
weightFeatureAvailable: boardsStore.weightFeatureAvailable,
};
},
computed: {
issuesTooltip() {
const { issuesSize, maxIssueCount } = this.list;
if (maxIssueCount > 0) {
return sprintf(__('%{issuesSize} issues with a limit of %{maxIssueCount}'), {
issuesSize,
maxIssueCount,
});
}
// TODO: Remove this pattern.
return BoardColumnFoss.computed.issuesTooltip.call(this);
},
weightCountToolTip() {
const { totalWeight } = this.list;
if (this.weightFeatureAvailable) {
return sprintf(s__('%{totalWeight} total weight'), { totalWeight });
}
return null;
},
},
methods: {
...mapActions(['setActiveListId']),
openSidebarSettings() {
this.setActiveListId(this.list.id);
},
},
};
</script>
export default { export default {
computed: { computed: {
isWipLimitsOn() { isWipLimitsOn() {
return gon.features.wipLimits; return Boolean(gon?.features?.wipLimits);
}, },
}, },
}; };
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Board from 'ee/boards/components/board_column.vue';
import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data';
// board_promotion_state tries to mount on the real DOM,
// so we are mocking it in this test
jest.mock('ee/boards/components/board_promotion_state', () => ({}));
describe('Board Column Component', () => {
let wrapper;
let axiosMock;
beforeEach(() => {
window.gon = {};
axiosMock = new AxiosMockAdapter(axios);
axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
});
afterEach(() => {
axiosMock.restore();
wrapper.destroy();
localStorage.clear();
});
const createComponent = ({
listType = ListType.backlog,
collapsed = false,
withLocalStorage = true,
} = {}) => {
const boardId = '1';
const listMock = {
...listObj,
list_type: listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
listMock.user = {};
}
// Making List reactive
const list = Vue.observable(new List(listMock));
if (withLocalStorage) {
localStorage.setItem(
`boards.${boardId}.${list.type}.${list.id}.expanded`,
(!collapsed).toString(),
);
}
wrapper = shallowMount(Board, {
propsData: {
boardId,
disabled: false,
issueLinkBase: '/',
rootPath: '/',
list,
},
});
};
const findSettingsButton = () => wrapper.find({ ref: 'settingsBtn' });
describe('Settings Button', () => {
it.each(Object.values(ListType))(
'when feature flag is off: does not render for List Type `%s`',
listType => {
window.gon = {
features: {
wipLimits: false,
},
};
createComponent({ listType });
expect(findSettingsButton().exists()).toBe(false);
},
);
describe('when feature flag is on', () => {
const hasSettings = [ListType.assignee, ListType.milestone, ListType.label];
const hasNoSettings = [ListType.backlog, ListType.blank, ListType.closed, ListType.promotion];
beforeEach(() => {
window.gon = {
features: {
wipLimits: true,
},
};
});
it.each(hasSettings)('does render for List Type `%s`', listType => {
createComponent({ listType });
expect(findSettingsButton().exists()).toBe(true);
});
it.each(hasNoSettings)('does not render for List Type `%s`', listType => {
createComponent({ listType });
expect(findSettingsButton().exists()).toBe(false);
});
it('has a test for each list type', () => {
Object.values(ListType).forEach(value => {
expect([...hasSettings, ...hasNoSettings]).toContain(value);
});
});
});
});
});
import Sortablejs from 'sortablejs'; const Sortablejs = jest.genMockFromModule('sortablejs');
export default Sortablejs; export default Sortablejs;
export const Sortable = Sortablejs; export const Sortable = Sortablejs;
......
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Board from '~/boards/components/board_column.vue';
import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data';
describe('Board Column Component', () => {
let wrapper;
let axiosMock;
beforeEach(() => {
window.gon = {};
axiosMock = new AxiosMockAdapter(axios);
axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
});
afterEach(() => {
axiosMock.restore();
wrapper.destroy();
localStorage.clear();
});
const createComponent = ({
listType = ListType.backlog,
collapsed = false,
withLocalStorage = true,
} = {}) => {
const boardId = '1';
const listMock = {
...listObj,
list_type: listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
listMock.user = {};
}
// Making List reactive
const list = Vue.observable(new List(listMock));
if (withLocalStorage) {
localStorage.setItem(
`boards.${boardId}.${list.type}.${list.id}.expanded`,
(!collapsed).toString(),
);
}
wrapper = shallowMount(Board, {
propsData: {
boardId,
disabled: false,
issueLinkBase: '/',
rootPath: '/',
list,
},
});
};
const isExpandable = () => wrapper.classes('is-expandable');
const isCollapsed = () => wrapper.classes('is-collapsed');
const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
describe('Add issue button', () => {
const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
createComponent({ listType });
expect(findAddIssueButton().exists()).toBe(false);
});
it.each(hasAddButton)('does render when List Type is `%s`', listType => {
createComponent({ listType });
expect(findAddIssueButton().exists()).toBe(true);
});
it('has a test for each list type', () => {
Object.values(ListType).forEach(value => {
expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
});
});
it('does render when logged out', () => {
createComponent();
expect(findAddIssueButton().exists()).toBe(true);
});
});
describe('Given different list types', () => {
it('is expandable when List Type is `backlog`', () => {
createComponent({ listType: ListType.backlog });
expect(isExpandable()).toBe(true);
});
});
describe('expanding / collapsing the column', () => {
it('does not collapse when clicking the header', () => {
createComponent();
expect(isCollapsed()).toBe(false);
wrapper.find('.board-header').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isCollapsed()).toBe(false);
});
});
it('collapses expanded Column when clicking the collapse icon', () => {
createComponent();
expect(wrapper.vm.list.isExpanded).toBe(true);
wrapper.find('.board-title-caret').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isCollapsed()).toBe(true);
});
});
it('expands collapsed Column when clicking the expand icon', () => {
createComponent({ collapsed: true });
expect(isCollapsed()).toBe(true);
wrapper.find('.board-title-caret').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isCollapsed()).toBe(false);
});
});
it("when logged in it calls list update and doesn't set localStorage", () => {
jest.spyOn(List.prototype, 'update');
window.gon.current_user_id = 1;
createComponent({ withLocalStorage: false });
wrapper.find('.board-title-caret').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.list.update).toHaveBeenCalledTimes(1);
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null);
});
});
it("when logged out it doesn't call list update and sets localStorage", () => {
jest.spyOn(List.prototype, 'update');
createComponent();
wrapper.find('.board-title-caret').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.list.update).toHaveBeenCalledTimes(0);
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(
String(wrapper.vm.list.isExpanded),
);
});
});
});
});
...@@ -56,7 +56,7 @@ describe('List model', () => { ...@@ -56,7 +56,7 @@ describe('List model', () => {
label: { label: {
id: 1, id: 1,
title: 'test', title: 'test',
color: 'red', color: '#ff0000',
text_color: 'white', text_color: 'white',
}, },
}); });
...@@ -64,8 +64,7 @@ describe('List model', () => { ...@@ -64,8 +64,7 @@ describe('List model', () => {
expect(list.id).toBe(listObj.id); expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label'); expect(list.type).toBe('label');
expect(list.position).toBe(0); expect(list.position).toBe(0);
expect(list.label.color).toBe('red'); expect(list.label).toEqual(listObj.label);
expect(list.label.textColor).toBe('white');
}); });
}); });
......
...@@ -15,7 +15,7 @@ export const listObj = { ...@@ -15,7 +15,7 @@ export const listObj = {
label: { label: {
id: 5000, id: 5000,
title: 'Test', title: 'Test',
color: 'red', color: '#ff0000',
description: 'testing;', description: 'testing;',
textColor: 'white', textColor: 'white',
}, },
...@@ -30,7 +30,7 @@ export const listObjDuplicate = { ...@@ -30,7 +30,7 @@ export const listObjDuplicate = {
label: { label: {
id: listObj.label.id, id: listObj.label.id,
title: 'Test', title: 'Test',
color: 'red', color: '#ff0000',
description: 'testing;', description: 'testing;',
}, },
}; };
......
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