Commit 37dcb167 authored by Scott Stern's avatar Scott Stern Committed by Natalia Tepluhina

Move board settings sidebar to ce

parent d7400d4e
<script>
import {
GlDrawer,
GlLabel,
GlButton,
GlFormInput,
GlAvatarLink,
GlAvatarLabeled,
GlLink,
} from '@gitlab/ui';
import { GlDrawer, GlLabel, GlAvatarLink, GlAvatarLabeled, GlLink } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { __, n__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import boardsStoreEE from '../stores/boards_store_ee';
import { __ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/sidebar/event_hub';
import flash from '~/flash';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { inactiveId } from '~/boards/constants';
......@@ -27,29 +17,14 @@ export default {
labelListText: __('Label'),
labelMilestoneText: __('Milestone'),
labelAssigneeText: __('Assignee'),
editLinkText: __('Edit'),
noneText: __('None'),
wipLimitText: __('Work in progress Limit'),
removeLimitText: __('Remove limit'),
inputPlaceholderText: __('Enter number of issues'),
components: {
GlDrawer,
GlLabel,
GlButton,
GlFormInput,
GlAvatarLink,
GlAvatarLabeled,
GlLink,
},
directives: {
autofocusonshow,
},
data() {
return {
edit: false,
currentWipLimit: null,
updating: false,
};
BoardSettingsSidebarWipLimit: () =>
import('ee_component/boards/components/board_settings_wip_limit.vue'),
},
computed: {
...mapState(['activeId']),
......@@ -58,7 +33,7 @@ export default {
Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects
*/
return boardsStoreEE.store.state.lists.find(({ id }) => id === this.activeId);
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
},
isSidebarOpen() {
return this.activeId !== inactiveId;
......@@ -72,15 +47,6 @@ export default {
activeListAssignee() {
return this.activeList.assignee;
},
wipLimitTypeText() {
return n__('%d issue', '%d issues', this.activeList.maxIssueCount);
},
wipLimitIsSet() {
return this.activeList.maxIssueCount !== 0;
},
activeListWipLimit() {
return this.activeList.maxIssueCount === 0 ? this.$options.noneText : this.wipLimitTypeText;
},
boardListType() {
return this.activeList.type || null;
},
......@@ -108,66 +74,12 @@ export default {
eventHub.$off('sidebar.closeAll', this.closeSidebar);
},
methods: {
...mapActions(['setActiveId', 'updateListWipLimit']),
...mapActions(['setActiveId']),
closeSidebar() {
this.edit = false;
this.setActiveId(inactiveId);
},
showInput() {
this.edit = true;
this.currentWipLimit =
this.activeList.maxIssueCount > 0 ? this.activeList.maxIssueCount : null;
},
resetStateAfterUpdate() {
this.edit = false;
this.updating = false;
this.currentWipLimit = null;
},
offFocus() {
if (this.currentWipLimit !== this.activeList.maxIssueCount && this.currentWipLimit !== null) {
this.updating = true;
// need to reassign bc were clearing the ref in resetStateAfterUpdate.
const wipLimit = this.currentWipLimit;
const id = this.activeId;
this.updateListWipLimit({ maxIssueCount: this.currentWipLimit, id })
.then(() => {
boardsStoreEE.setMaxIssueCountOnList(id, wipLimit);
this.resetStateAfterUpdate();
})
.catch(() => {
this.resetStateAfterUpdate();
this.setActiveId(inactiveId);
flash(__('Something went wrong while updating your list settings'));
});
} else {
this.edit = false;
}
},
clearWipLimit() {
this.updateListWipLimit({ maxIssueCount: 0, id: this.activeId })
.then(() => {
boardsStoreEE.setMaxIssueCountOnList(this.activeId, 0);
this.resetStateAfterUpdate();
})
.catch(() => {
this.resetStateAfterUpdate();
this.setActiveId(inactiveId);
flash(__('Something went wrong while updating your list settings'));
});
},
handleWipLimitChange(wipLimit) {
if (wipLimit === '') {
this.currentWipLimit = null;
} else {
this.currentWipLimit = Number(wipLimit);
}
},
onEnter() {
this.offFocus();
},
showScopedLabels(label) {
return boardsStoreEE.store.scopedLabels.enabled && isScopedLabel(label);
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
},
};
......@@ -207,43 +119,8 @@ export default {
</gl-link>
</template>
</div>
<div class="d-flex justify-content-between flex-column">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="m-0">{{ $options.wipLimitText }}</label>
<gl-button
class="js-edit-button h-100 border-0 text-dark"
variant="link"
@click="showInput"
>{{ $options.editLinkText }}</gl-button
>
</div>
<gl-form-input
v-if="edit"
v-autofocusonshow
:value="currentWipLimit"
:disabled="updating"
:placeholder="$options.inputPlaceholderText"
trim
number
type="number"
min="0"
@input="handleWipLimitChange"
@keydown.enter.native="onEnter"
@blur="offFocus"
/>
<div v-else class="d-flex align-items-center">
<p class="js-wip-limit bold m-0 text-secondary">{{ activeListWipLimit }}</p>
<template v-if="wipLimitIsSet">
<span class="m-1">-</span>
<gl-button
class="js-remove-limit h-100 border-0 text-secondary"
variant="link"
@click="clearWipLimit"
>{{ $options.removeLimitText }}</gl-button
>
</template>
</div>
</div>
<board-settings-sidebar-wip-limit :max-issue-count="activeList.maxIssueCount" />
</template>
</gl-drawer>
</template>
......@@ -83,8 +83,7 @@ export default () => {
Board: () => import('ee_else_ce/boards/components/board_column.vue'),
BoardSidebar,
BoardAddIssuesModal,
BoardSettingsSidebar: () =>
import('ee_component/boards/components/board_settings_sidebar.vue'),
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
store,
apolloProvider,
......
......@@ -10,6 +10,10 @@ export default {
commit(types.SET_ENDPOINTS, endpoints);
},
setActiveId({ commit }, id) {
commit(types.SET_ACTIVE_ID, id);
},
fetchLists: () => {
notImplemented();
},
......
......@@ -19,3 +19,4 @@ export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
......@@ -10,6 +10,10 @@ export default {
state.endpoints = endpoints;
},
[mutationTypes.SET_ACTIVE_ID](state, id) {
state.activeId = id;
},
[mutationTypes.REQUEST_ADD_LIST]: () => {
notImplemented();
},
......
<script>
import { mapActions, mapState } from 'vuex';
import { GlButton, GlFormInput } from '@gitlab/ui';
import flash from '~/flash';
import { __, n__ } from '~/locale';
import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { inactiveId } from '~/boards/constants';
export default {
i18n: {
wipLimitText: __('Work in progress Limit'),
editLinkText: __('Edit'),
noneText: __('None'),
inputPlaceholderText: __('Enter number of issues'),
removeLimitText: __('Remove limit'),
},
components: {
GlButton,
GlFormInput,
},
directives: {
autofocusonshow,
},
props: {
maxIssueCount: {
type: Number,
required: true,
},
},
data() {
return {
currentWipLimit: null,
edit: false,
updating: false,
};
},
computed: {
...mapState(['activeId']),
wipLimitTypeText() {
return n__('%d issue', '%d issues', this.maxIssueCount);
},
wipLimitIsSet() {
return this.maxIssueCount !== 0;
},
activeListWipLimit() {
return this.wipLimitIsSet ? this.wipLimitTypeText : this.$options.i18n.noneText;
},
},
methods: {
...mapActions(['setActiveId', 'updateListWipLimit']),
showInput() {
this.edit = true;
this.currentWipLimit = this.maxIssueCount > 0 ? this.maxIssueCount : null;
},
handleWipLimitChange(wipLimit) {
if (wipLimit === '') {
this.currentWipLimit = null;
} else {
this.currentWipLimit = Number(wipLimit);
}
},
onEnter() {
this.offFocus();
},
resetStateAfterUpdate() {
this.edit = false;
this.updating = false;
this.currentWipLimit = null;
},
offFocus() {
if (this.currentWipLimit !== this.maxIssueCount && this.currentWipLimit !== null) {
this.updating = true;
// need to reassign bc were clearing the ref in resetStateAfterUpdate.
const wipLimit = this.currentWipLimit;
const id = this.activeId;
this.updateListWipLimit({ maxIssueCount: this.currentWipLimit, id })
.then(() => {
boardsStoreEE.setMaxIssueCountOnList(id, wipLimit);
})
.catch(() => {
this.setActiveId(0);
flash(__('Something went wrong while updating your list settings'));
})
.finally(() => {
this.resetStateAfterUpdate();
});
} else {
this.edit = false;
}
},
clearWipLimit() {
this.updateListWipLimit({ maxIssueCount: 0, id: this.activeId })
.then(() => {
boardsStoreEE.setMaxIssueCountOnList(this.activeId, inactiveId);
})
.catch(() => {
this.setActiveId(inactiveId);
flash(__('Something went wrong while updating your list settings'));
})
.finally(() => {
this.resetStateAfterUpdate();
});
},
},
};
</script>
<template>
<div class="gl-display-flex gl-justify-content-space-between gl-flex-direction-column">
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-mb-2">
<label class="m-0">{{ $options.i18n.wipLimitText }}</label>
<gl-button
class="js-edit-button gl-h-full gl-border-0 text-dark"
variant="link"
@click="showInput"
>{{ $options.i18n.editLinkText }}</gl-button
>
</div>
<gl-form-input
v-if="edit"
v-autofocusonshow
:value="currentWipLimit"
:disabled="updating"
:placeholder="$options.i18n.inputPlaceholderText"
trim
number
type="number"
min="0"
@input="handleWipLimitChange"
@keydown.enter.native="onEnter"
@blur="offFocus"
/>
<div v-else class="gl-display-flex gl-align-items-center">
<p class="js-wip-limit bold gl-m-0 text-secondary">{{ activeListWipLimit }}</p>
<template v-if="wipLimitIsSet">
<span class="m-1">-</span>
<gl-button
class="js-remove-limit gl-h-full gl-border-0 text-secondary"
variant="link"
@click="clearWipLimit"
>{{ $options.i18n.removeLimitText }}</gl-button
>
</template>
</div>
</div>
</template>
......@@ -67,9 +67,6 @@ export default {
commit(types.SET_SHOW_LABELS, val);
},
setActiveId({ commit }, listId) {
commit(types.SET_ACTIVE_LIST_ID, listId);
},
updateListWipLimit({ state }, { maxIssueCount }) {
const { activeId } = state;
......
......@@ -15,5 +15,4 @@ export const TOGGLE_EPICS_SWIMLANES = 'TOGGLE_EPICS_SWIMLANES';
export const RECEIVE_SWIMLANES_SUCCESS = 'RECEIVE_SWIMLANES_SUCCESS';
export const RECEIVE_SWIMLANES_FAILURE = 'RECEIVE_SWIMLANES_FAILURE';
export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
export const SET_ACTIVE_LIST_ID = 'SET_ACTIVE_LIST_ID';
export const SET_SHOW_LABELS = 'SET_SHOW_LABELS';
......@@ -11,9 +11,6 @@ export default {
[mutationTypes.SET_SHOW_LABELS]: (state, val) => {
state.isShowingLabels = val;
},
[mutationTypes.SET_ACTIVE_LIST_ID]: (state, id) => {
state.activeId = id;
},
[mutationTypes.REQUEST_AVAILABLE_BOARDS]: () => {
notImplemented();
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BoardSettingsSideBar when activeList is present when activeListWipLimit is 0 renders "None" in the block 1`] = `"None"`;
exports[`BoardSettingsSideBar when activeList is present when activeListWipLimit is greater than 0 it renders 1 1`] = `"1 issue"`;
exports[`BoardSettingsSideBar when activeList is present when activeListWipLimit is greater than 0 it renders 11 1`] = `"11 issues"`;
exports[`BoardSettingsSideBar when activeList is present when activeListWipLimit is greater than 0 when list type is "assignee" renders the correct list type text 1`] = `"Assignee"`;
exports[`BoardSettingsSideBar when activeList is present when activeListWipLimit is greater than 0 when list type is "milestone" renders the correct list type text 1`] = `"Milestone"`;
exports[`BoardSettingsSideBar when activeList is present when activeListWipLimit is greater than 0 when list type is "milestone" renders the correct milestone text 1`] = `"Backlog"`;
......@@ -3,536 +3,60 @@ import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDrawer, GlLabel, GlFormInput, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import BoardSettingsSidebar from 'ee/boards/components/board_settings_sidebar.vue';
import boardsStore from 'ee_else_ce/boards/stores/boards_store_ee';
import getters from 'ee_else_ce/boards/stores/getters';
import bs from '~/boards/stores/boards_store';
import sidebarEventHub from '~/sidebar/event_hub';
import flash from '~/flash';
import waitForPromises from 'helpers/wait_for_promises';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import BoardSettingsWipLimit from 'ee_component/boards/components/board_settings_wip_limit.vue';
import boardsStore from '~/boards/stores/boards_store';
import { inactiveId } from '~/boards/constants';
jest.mock('~/flash');
// NOTE: needed for calling boardsStore.addList
const localVue = createLocalVue();
localVue.use(Vuex);
describe('BoardSettingsSideBar', () => {
describe('ee/BoardSettingsSidebar', () => {
let wrapper;
let mock;
let storeActions;
const labelTitle = 'test';
const labelColor = '#FFFF';
const listId = 1;
const currentWipLimit = 1; // Needs to be other than null to trigger requests.
let mock;
const createComponent = (state = { activeId: inactiveId }, actions = {}, localState = {}) => {
const createComponent = (state = { activeId: inactiveId }, actions = {}) => {
storeActions = actions;
const store = new Vuex.Store({
state,
actions: storeActions,
getters,
});
wrapper = shallowMount(BoardSettingsSidebar, {
store,
localVue,
data() {
return localState;
stubs: {
'board-settings-sidebar-wip-limit': BoardSettingsWipLimit,
},
});
};
const triggerBlur = type => {
if (type === 'blur') {
wrapper.find(GlFormInput).vm.$emit('blur');
}
if (type === 'enter') {
wrapper.find(GlFormInput).trigger('keydown.enter');
}
};
beforeEach(() => {
// mock CE store
const storeMock = {
state: { lists: [] },
create() {},
setCurrentBoard: jest.fn(),
findList: bs.findList,
addList: bs.addList,
removeList: bs.removeList,
scopedLabels: {
enabled: false,
},
};
boardsStore.initEESpecific(storeMock);
});
afterEach(() => {
jest.restoreAllMocks();
wrapper.destroy();
});
describe('GlDrawer', () => {
it('finds a GlDrawer component', () => {
createComponent();
expect(wrapper.find(GlDrawer).exists()).toBe(true);
});
describe('on close', () => {
it('calls closeSidebar', () => {
const spy = jest.fn();
createComponent({ activeId: inactiveId }, { setActiveId: spy });
wrapper.find(GlDrawer).vm.$emit('close');
return wrapper.vm.$nextTick().then(() => {
expect(storeActions.setActiveId).toHaveBeenCalledWith(
expect.anything(),
inactiveId,
undefined,
);
});
});
it('calls closeSidebar on sidebar.closeAll event', () => {
createComponent({ activeId: inactiveId }, { setActiveId: jest.fn() });
sidebarEventHub.$emit('sidebar.closeAll');
return wrapper.vm.$nextTick().then(() => {
expect(storeActions.setActiveId).toHaveBeenCalledWith(
expect.anything(),
inactiveId,
undefined,
);
});
});
});
describe('when activeId is zero', () => {
it('renders GlDrawer with open false', () => {
createComponent();
expect(wrapper.find(GlDrawer).props('open')).toBe(false);
});
});
describe('when activeId is greater than zero', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.store.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
});
afterEach(() => {
boardsStore.store.removeList(listId);
});
it('renders GlDrawer with open false', () => {
createComponent({ activeId: 1 });
expect(wrapper.find(GlDrawer).props('open')).toBe(true);
});
});
describe('when activeId is in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.store.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
createComponent({ activeId: listId });
});
afterEach(() => {
mock.restore();
});
it('renders label title', () => {
expect(wrapper.find(GlLabel).props('title')).toEqual(labelTitle);
});
it('renders label background color', () => {
expect(wrapper.find(GlLabel).props('backgroundColor')).toEqual(labelColor);
});
});
describe('when activeId is not in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.store.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
createComponent({ activeId: inactiveId });
boardsStore.create();
});
afterEach(() => {
mock.restore();
});
it('does not render GlLabel', () => {
expect(wrapper.find(GlLabel).exists()).toBe(false);
});
});
});
describe('when activeList is present', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
boardsStore.store.removeList(listId);
});
describe('when activeListWipLimit is 0', () => {
beforeEach(() => {
boardsStore.store.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: 0,
list_type: 'label',
});
});
it('renders "None" in the block', () => {
createComponent({ activeId: listId });
expect(wrapper.find('.js-wip-limit').text()).toMatchSnapshot();
});
});
describe('when activeListWipLimit is greater than 0', () => {
describe('when list type is "milestone"', () => {
beforeEach(() => {
boardsStore.store.addList({
id: 1,
milestone: {
webUrl: 'https://gitlab.com/h5bp/html5-boilerplate/-/milestones/1',
title: 'Backlog',
},
max_issue_count: 1,
list_type: 'milestone',
});
});
afterEach(() => {
boardsStore.store.removeList(1, 'milestone');
wrapper.destroy();
});
it('renders the correct milestone text', () => {
createComponent({ activeId: 1 });
expect(wrapper.find('.js-milestone').text()).toMatchSnapshot();
});
it('renders the correct list type text', () => {
createComponent({ activeId: 1 });
expect(wrapper.find('.js-list-label').text()).toMatchSnapshot();
});
});
describe('when list type is "assignee"', () => {
beforeEach(() => {
boardsStore.store.addList({
id: 1,
user: { username: 'root', avatar: '', name: 'Test', webUrl: 'https://gitlab.com/root' },
max_issue_count: 1,
list_type: 'assignee',
});
});
afterEach(() => {
boardsStore.store.removeList(1, 'assignee');
wrapper.destroy();
});
it('renders gl-avatar-link with correct href', () => {
createComponent({ activeId: 1 });
expect(wrapper.find(GlAvatarLink).exists()).toBe(true);
expect(wrapper.find(GlAvatarLink).attributes('href')).toEqual('https://gitlab.com/root');
});
it('renders gl-avatar-labeled with "root" as username and name as "Test"', () => {
createComponent({ activeId: 1 });
expect(wrapper.find(GlAvatarLabeled).exists()).toBe(true);
expect(wrapper.find(GlAvatarLabeled).attributes('label')).toEqual('Test');
expect(wrapper.find(GlAvatarLabeled).attributes('sublabel')).toEqual('@root');
});
it('renders the correct list type text', () => {
createComponent({ activeId: 1 });
expect(wrapper.find('.js-list-label').text()).toMatchSnapshot();
});
});
it.each`
num
${1}
${11}
`('it renders $num', ({ num }) => {
boardsStore.store.addList({
id: num,
label: { title: labelTitle, color: labelColor },
max_issue_count: num,
list_type: 'label',
});
createComponent({ activeId: num });
expect(wrapper.find('.js-wip-limit').text()).toMatchSnapshot();
});
});
});
describe('when clicking edit', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.store.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: 4,
list_type: 'label',
});
createComponent({ activeId: listId }, { updateListWipLimit: () => {} });
});
it('renders an input', () => {
wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlFormInput).exists()).toBe(true);
});
});
it('does not render current wipLimit text', () => {
wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-wip-limit').exists()).toBe(false);
});
});
it('sets wipLimit to be the value of list.maxIssueCount', () => {
expect(wrapper.vm.currentWipLimit).toEqual(null);
wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.currentWipLimit).toBe(4);
});
});
});
describe('remove limit', () => {
describe('when wipLimit is set', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.store.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: 4,
list_type: 'label',
});
const spy = jest.fn().mockResolvedValue({
config: { data: JSON.stringify({ list: { max_issue_count: 0 } }) },
});
createComponent({ activeId: listId }, { updateListWipLimit: spy });
});
it('resets wipLimit to 0', () => {
expect(wrapper.vm.activeList.maxIssueCount).toEqual(4);
wrapper.find('.js-remove-limit').vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.activeList.maxIssueCount).toEqual(0);
});
});
});
describe('when wipLimit is not set', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.store.addList({
it('confirms we render BoardSettingsSidebarWipLimit', () => {
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: 0,
list_type: 'label',
});
createComponent({ activeId: listId }, { updateListWipLimit: () => {} });
});
it('does not render the remove limit button', () => {
expect(wrapper.find('.js-remove-limit').exists()).toBe(false);
});
});
});
describe('when edit is true', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.store.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: 2,
list_type: 'label',
});
});
afterEach(() => {
flash.mockReset();
boardsStore.store.removeList(listId, 'label');
});
describe.each`
blurMethod
${'enter'}
${'blur'}
`('$blurMethod', ({ blurMethod }) => {
describe(`when blur is triggered by ${blurMethod}`, () => {
it('calls updateListWipLimit', () => {
const spy = jest.fn().mockResolvedValue({
config: { data: JSON.stringify({ list: { max_issue_count: '4' } }) },
});
createComponent(
{ activeId: 1 },
{ updateListWipLimit: spy },
{ edit: true, currentWipLimit },
);
triggerBlur(blurMethod);
return wrapper.vm.$nextTick().then(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('when component wipLimit and List.maxIssueCount are equal', () => {
it('doesnt call updateListWipLimit', () => {
const spy = jest.fn().mockResolvedValue({});
createComponent(
{ activeId: 1 },
{ updateListWipLimit: spy },
{ edit: true, currentWipLimit: 2 },
);
triggerBlur(blurMethod);
return wrapper.vm.$nextTick().then(() => {
expect(spy).toHaveBeenCalledTimes(0);
});
});
});
describe('when currentWipLimit is null', () => {
it('doesnt call updateListWipLimit', () => {
const spy = jest.fn().mockResolvedValue({});
createComponent(
{ activeId: 1 },
{ updateListWipLimit: spy },
{ edit: true, currentWipLimit: null },
);
triggerBlur(blurMethod);
return wrapper.vm.$nextTick().then(() => {
expect(spy).toHaveBeenCalledTimes(0);
});
});
});
describe('when response is successful', () => {
const maxIssueCount = 11;
beforeEach(() => {
const spy = jest.fn().mockResolvedValue({});
createComponent(
{ activeId: 1 },
{ updateListWipLimit: spy },
{ edit: true, currentWipLimit: maxIssueCount },
);
triggerBlur(blurMethod);
return waitForPromises();
});
it('sets activeWipLimit to new maxIssueCount value', () => {
/*
* DANGER: bad coupling to the computed prop of the component because the
* computed prop relys on the list from boardStore, for now this is the way around
* stale values from boardsStore being updated, when we move List and BoardsStore to Vuex
* or Graphql we will be able to query the DOM for the new value.
*/
expect(wrapper.vm.activeList.maxIssueCount).toEqual(maxIssueCount);
});
it('toggles GlFormInput on blur', () => {
expect(wrapper.find(GlFormInput).exists()).toBe(false);
expect(wrapper.find('.js-wip-limit').exists()).toBe(true);
expect(wrapper.vm.updating).toBe(false);
});
});
describe('when response fails', () => {
beforeEach(() => {
const spy = jest.fn().mockRejectedValue();
createComponent(
{ activeId: 1 },
{ updateListWipLimit: spy, setActiveId: () => {} },
{ edit: true, currentWipLimit },
);
triggerBlur(blurMethod);
return waitForPromises();
});
it('calls flash with expected error', () => {
expect(flash).toHaveBeenCalledTimes(1);
});
});
});
});
describe('passing of props to gl-form-input', () => {
beforeEach(() => {
createComponent({ activeId: listId }, { updateListWipLimit: () => {} }, { edit: true });
});
it('passes `trim`', () => {
expect(wrapper.find(GlFormInput).attributes().trim).toBeDefined();
});
createComponent({ activeId: listId });
it('passes `number`', () => {
expect(wrapper.find(GlFormInput).attributes().number).toBeDefined();
});
});
expect(wrapper.find(BoardSettingsWipLimit).exists()).toBe(true);
});
});
import '~/boards/models/list';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
import { noop } from 'lodash';
import BoardSettingsWipLimit from 'ee_component/boards/components/board_settings_wip_limit.vue';
import boardsStore from '~/boards/stores/boards_store';
import flash from '~/flash';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(Vuex);
describe('BoardSettingsWipLimit', () => {
let wrapper;
let storeActions;
const labelTitle = 'test';
const labelColor = '#FFFF';
const listId = 1;
const currentWipLimit = 1; // Needs to be other than null to trigger requests
let mock;
const addList = (maxIssueCount = 0) => {
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: maxIssueCount,
list_type: 'label',
});
};
const clickEdit = () => wrapper.find('.js-edit-button').vm.$emit('click');
const findRemoveWipLimit = () => wrapper.find('.js-remove-limit');
const findWipLimit = () => wrapper.find('.js-wip-limit');
const findInput = () => wrapper.find(GlFormInput);
const createComponent = ({
vuexState = { activeId: listId },
actions = {},
localState = {},
props = { maxIssueCount: 0 },
}) => {
storeActions = actions;
const store = new Vuex.Store({
state: vuexState,
actions: storeActions,
});
wrapper = shallowMount(BoardSettingsWipLimit, {
propsData: props,
store,
localVue,
data() {
return localState;
},
});
};
const triggerBlur = type => {
if (type === 'blur') {
findInput().vm.$emit('blur');
}
if (type === 'enter') {
findInput().trigger('keydown.enter');
}
};
beforeEach(() => {
boardsStore.create();
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
jest.restoreAllMocks();
wrapper.destroy();
});
describe('when activeList is present', () => {
describe('when activeListWipLimit is 0', () => {
it('renders "None" in the block', () => {
createComponent({ vuexState: { activeId: listId } });
expect(findWipLimit().text()).toBe('None');
});
});
describe('when activeId is greater than 0', () => {
afterEach(() => {
boardsStore.removeList(listId);
});
it.each`
num | expected
${1} | ${'1 issue'}
${11} | ${'11 issues'}
`('it renders $num', ({ num, expected }) => {
addList(4);
createComponent({ vuexState: { activeId: num }, props: { maxIssueCount: num } });
expect(findWipLimit().text()).toBe(expected);
});
});
});
describe('when clicking edit', () => {
const maxIssueCount = 4;
beforeEach(async () => {
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: noop },
props: { maxIssueCount },
});
clickEdit();
await wrapper.vm.$nextTick();
});
it('renders an input', () => {
expect(findInput().exists()).toBe(true);
});
it('does not render current wipLimit text', () => {
expect(findWipLimit().exists()).toBe(false);
});
it('sets wipLimit to be the value of list.maxIssueCount', () => {
expect(findInput().attributes('value')).toBe(String(maxIssueCount));
});
});
describe('remove limit', () => {
describe('when wipLimit is set', () => {
beforeEach(() => {
addList(4);
const spy = jest.fn().mockResolvedValue({
config: { data: JSON.stringify({ list: { max_issue_count: 0 } }) },
});
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: spy },
props: { maxIssueCount: 4 },
});
});
it('resets wipLimit to 0', async () => {
expect(findWipLimit().text()).toContain(4);
findRemoveWipLimit().vm.$emit('click');
await wrapper.vm.$nextTick();
// WARNING: https://gitlab.com/gitlab-org/gitlab/-/issues/232573
expect(boardsStore.findList('id', listId).maxIssueCount).toBe(0);
});
});
describe('when wipLimit is not set', () => {
beforeEach(() => {
addList();
createComponent({ vuexState: { activeId: listId }, actions: { updateListWipLimit: noop } });
});
it('does not render the remove limit button', () => {
expect(findRemoveWipLimit().exists()).toBe(false);
});
});
});
describe('when edit is true', () => {
beforeEach(() => {
addList(2);
});
afterEach(() => {
flash.mockReset();
boardsStore.removeList(listId, 'label');
});
describe.each`
blurMethod
${'enter'}
${'blur'}
`('$blurMethod', ({ blurMethod }) => {
describe(`when blur is triggered by ${blurMethod}`, () => {
it('calls updateListWipLimit', async () => {
const spy = jest.fn().mockResolvedValue({
config: { data: JSON.stringify({ list: { max_issue_count: '4' } }) },
});
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: spy },
localState: { edit: true, currentWipLimit },
});
triggerBlur(blurMethod);
await wrapper.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(1);
});
describe('when component wipLimit and List.maxIssueCount are equal', () => {
it('doesnt call updateListWipLimit', async () => {
const spy = jest.fn().mockResolvedValue({});
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: spy },
localState: { edit: true, currentWipLimit: 2 },
props: { maxIssueCount: 2 },
});
triggerBlur(blurMethod);
await wrapper.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(0);
});
});
describe('when currentWipLimit is null', () => {
it('doesnt call updateListWipLimit', async () => {
const spy = jest.fn().mockResolvedValue({});
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: spy },
localState: { edit: true, currentWipLimit: null },
});
triggerBlur(blurMethod);
await wrapper.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(0);
});
});
describe('when response is successful', () => {
const maxIssueCount = 11;
beforeEach(() => {
const spy = jest.fn().mockResolvedValue({});
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: spy },
localState: { edit: true, currentWipLimit: maxIssueCount },
});
triggerBlur(blurMethod);
return waitForPromises();
});
it('sets activeWipLimit to new maxIssueCount value', () => {
/*
* DANGER: bad coupling to the computed prop of the component because the
* computed prop relys on the list from boardStore, for now this is the way around
* stale values from boardsStore being updated, when we move List and BoardsStore to Vuex
* or Graphql we will be able to query the DOM for the new value.
*/
expect(boardsStore.findList('id', 1).maxIssueCount).toBe(maxIssueCount);
});
it('toggles GlFormInput on blur', () => {
expect(findInput().exists()).toBe(false);
expect(findWipLimit().exists()).toBe(true);
expect(wrapper.vm.updating).toBe(false);
});
});
describe('when response fails', () => {
beforeEach(() => {
const spy = jest.fn().mockRejectedValue();
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: spy, setActiveId: noop },
localState: { edit: true, currentWipLimit },
});
triggerBlur(blurMethod);
return waitForPromises();
});
it('calls flash with expected error', () => {
expect(flash).toHaveBeenCalledTimes(1);
});
});
});
});
describe('passing of props to gl-form-input', () => {
beforeEach(() => {
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: noop },
localState: { edit: true },
});
});
it('passes `trim`', () => {
expect(findInput().attributes().trim).toBeDefined();
});
it('passes `number`', () => {
expect(findInput().attributes().number).toBeDefined();
});
});
});
});
......@@ -3,7 +3,6 @@ import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
import actions from 'ee/boards/stores/actions';
import * as types from 'ee/boards/stores/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import { inactiveId } from '~/boards/constants';
jest.mock('axios');
......@@ -30,23 +29,6 @@ describe('setShowLabels', () => {
});
});
describe('setActiveId', () => {
it('should commit mutation SET_ACTIVE_LIST_ID', done => {
const state = {
activeId: inactiveId,
};
testAction(
actions.setActiveId,
1,
state,
[{ type: types.SET_ACTIVE_LIST_ID, payload: 1 }],
[],
done,
);
});
});
describe('updateListWipLimit', () => {
let storeMock;
......
import mutations from 'ee/boards/stores/mutations';
import { inactiveId } from '~/boards/constants';
import { mockLists, mockEpics } from '../mock_data';
const expectNotImplemented = action => {
......@@ -20,19 +19,6 @@ describe('SET_SHOW_LABELS', () => {
});
});
describe('SET_ACTIVE_LIST_ID', () => {
it('updates aciveListId to be the value that is passed', () => {
const expectedId = 1;
const state = {
activeId: inactiveId,
};
mutations.SET_ACTIVE_LIST_ID(state, expectedId);
expect(state.activeId).toBe(expectedId);
});
});
describe('REQUEST_AVAILABLE_BOARDS', () => {
expectNotImplemented(mutations.REQUEST_AVAILABLE_BOARDS);
});
......
import '~/boards/models/list';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDrawer, GlLabel, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import boardsStore from '~/boards/stores/boards_store';
import sidebarEventHub from '~/sidebar/event_hub';
import { inactiveId } from '~/boards/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('BoardSettingsSidebar', () => {
let wrapper;
let mock;
let storeActions;
const labelTitle = 'test';
const labelColor = '#FFFF';
const listId = 1;
const createComponent = (state = { activeId: inactiveId }, actions = {}) => {
storeActions = actions;
const store = new Vuex.Store({
state,
actions: storeActions,
});
wrapper = shallowMount(BoardSettingsSidebar, {
store,
localVue,
});
};
const findLabel = () => wrapper.find(GlLabel);
const findDrawer = () => wrapper.find(GlDrawer);
beforeEach(() => {
boardsStore.create();
});
afterEach(() => {
jest.restoreAllMocks();
wrapper.destroy();
});
it('finds a GlDrawer component', () => {
createComponent();
expect(findDrawer().exists()).toBe(true);
});
describe('on close', () => {
it('calls closeSidebar', async () => {
const spy = jest.fn();
createComponent({ activeId: inactiveId }, { setActiveId: spy });
findDrawer().vm.$emit('close');
await wrapper.vm.$nextTick();
expect(storeActions.setActiveId).toHaveBeenCalledWith(
expect.anything(),
inactiveId,
undefined,
);
});
it('calls closeSidebar on sidebar.closeAll event', async () => {
createComponent({ activeId: inactiveId }, { setActiveId: jest.fn() });
sidebarEventHub.$emit('sidebar.closeAll');
await wrapper.vm.$nextTick();
expect(storeActions.setActiveId).toHaveBeenCalledWith(
expect.anything(),
inactiveId,
undefined,
);
});
});
describe('when activeId is zero', () => {
it('renders GlDrawer with open false', () => {
createComponent();
expect(findDrawer().props('open')).toBe(false);
});
});
describe('when activeId is greater than zero', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
});
afterEach(() => {
boardsStore.removeList(listId);
});
it('renders GlDrawer with open false', () => {
createComponent({ activeId: 1 });
expect(findDrawer().props('open')).toBe(true);
});
});
describe('when activeId is in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
createComponent({ activeId: listId });
});
afterEach(() => {
mock.restore();
});
it('renders label title', () => {
expect(findLabel().props('title')).toBe(labelTitle);
});
it('renders label background color', () => {
expect(findLabel().props('backgroundColor')).toBe(labelColor);
});
});
describe('when activeId is not in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
createComponent({ activeId: inactiveId });
});
afterEach(() => {
mock.restore();
});
it('does not render GlLabel', () => {
expect(findLabel().exists()).toBe(false);
});
});
describe('when activeList is present', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
boardsStore.removeList(listId);
});
describe('when list type is "milestone"', () => {
beforeEach(() => {
boardsStore.addList({
id: 1,
milestone: {
webUrl: 'https://gitlab.com/h5bp/html5-boilerplate/-/milestones/1',
title: 'Backlog',
},
max_issue_count: 1,
list_type: 'milestone',
});
});
afterEach(() => {
boardsStore.removeList(1, 'milestone');
wrapper.destroy();
});
it('renders the correct milestone text', () => {
createComponent({ activeId: 1 });
expect(wrapper.find('.js-milestone').text()).toBe('Backlog');
});
it('renders the correct list type text', () => {
createComponent({ activeId: 1 });
expect(wrapper.find('.js-list-label').text()).toBe('Milestone');
});
});
describe('when list type is "assignee"', () => {
beforeEach(() => {
boardsStore.addList({
id: 1,
user: { username: 'root', avatar: '', name: 'Test', webUrl: 'https://gitlab.com/root' },
max_issue_count: 1,
list_type: 'assignee',
});
});
afterEach(() => {
boardsStore.removeList(1, 'assignee');
wrapper.destroy();
});
it('renders gl-avatar-link with correct href', () => {
createComponent({ activeId: 1 });
expect(wrapper.find(GlAvatarLink).exists()).toBe(true);
expect(wrapper.find(GlAvatarLink).attributes('href')).toBe('https://gitlab.com/root');
});
it('renders gl-avatar-labeled with "root" as username and name as "Test"', () => {
createComponent({ activeId: 1 });
expect(wrapper.find(GlAvatarLabeled).exists()).toBe(true);
expect(wrapper.find(GlAvatarLabeled).attributes('label')).toBe('Test');
expect(wrapper.find(GlAvatarLabeled).attributes('sublabel')).toBe('@root');
});
it('renders the correct list type text', () => {
createComponent({ activeId: 1 });
expect(wrapper.find('.js-list-label').text()).toBe('Assignee');
});
});
});
});
import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import { inactiveId } from '~/boards/constants';
const expectNotImplemented = action => {
it('is not implemented', () => {
......@@ -25,6 +26,23 @@ describe('setEndpoints', () => {
});
});
describe('setActiveId', () => {
it('should commit mutation SET_ACTIVE_ID', done => {
const state = {
activeId: inactiveId,
};
testAction(
actions.setActiveId,
1,
state,
[{ type: types.SET_ACTIVE_ID, payload: 1 }],
[],
done,
);
});
});
describe('fetchLists', () => {
expectNotImplemented(actions.fetchLists);
});
......
......@@ -32,6 +32,16 @@ describe('Board Store Mutations', () => {
});
});
describe('SET_ACTIVE_ID', () => {
it('updates aciveListId to be the value that is passed', () => {
const expectedId = 1;
mutations.SET_ACTIVE_ID(state, expectedId);
expect(state.activeId).toBe(expectedId);
});
});
describe('REQUEST_ADD_LIST', () => {
expectNotImplemented(mutations.REQUEST_ADD_LIST);
});
......
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