Commit 43ea3133 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'ss/rm-limit' into 'master'

Add remove limit button to limit

See merge request gitlab-org/gitlab!22552
parents fe5227b3 24b2746e
...@@ -9,7 +9,7 @@ import { ...@@ -9,7 +9,7 @@ import {
GlLink, GlLink,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale'; import { __, n__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import boardsStoreEE from '../stores/boards_store_ee'; import boardsStoreEE from '../stores/boards_store_ee';
import flash from '~/flash'; import flash from '~/flash';
...@@ -17,7 +17,7 @@ import flash from '~/flash'; ...@@ -17,7 +17,7 @@ import flash from '~/flash';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options. // NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default { export default {
headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px', headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List Settings'), listSettingsText: __('List settings'),
assignee: 'assignee', assignee: 'assignee',
milestone: 'milestone', milestone: 'milestone',
label: 'label', label: 'label',
...@@ -26,7 +26,9 @@ export default { ...@@ -26,7 +26,9 @@ export default {
labelAssigneeText: __('Assignee'), labelAssigneeText: __('Assignee'),
editLinkText: __('Edit'), editLinkText: __('Edit'),
noneText: __('None'), noneText: __('None'),
wipLimitText: __('Work in Progress Limit'), wipLimitText: __('Work in progress Limit'),
removeLimitText: __('Remove limit'),
inputPlaceholderText: __('Enter number of issues'),
components: { components: {
GlDrawer, GlDrawer,
GlLabel, GlLabel,
...@@ -42,7 +44,7 @@ export default { ...@@ -42,7 +44,7 @@ export default {
data() { data() {
return { return {
edit: false, edit: false,
currentWipLimit: 0, currentWipLimit: null,
updating: false, updating: false,
}; };
}, },
...@@ -53,7 +55,6 @@ export default { ...@@ -53,7 +55,6 @@ export default {
Warning: Though a computed property it is not reactive because we are Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects referencing a List Model class. Reactivity only applies to plain JS objects
*/ */
return boardsStoreEE.store.state.lists.find(({ id }) => id === this.activeListId); return boardsStoreEE.store.state.lists.find(({ id }) => id === this.activeListId);
}, },
isSidebarOpen() { isSidebarOpen() {
...@@ -68,10 +69,14 @@ export default { ...@@ -68,10 +69,14 @@ export default {
activeListAssignee() { activeListAssignee() {
return this.activeList.assignee; return this.activeList.assignee;
}, },
wipLimitTypeText() {
return n__('%d issue', '%d issues', this.activeList.maxIssueCount);
},
wipLimitIsSet() {
return this.activeList.maxIssueCount !== 0;
},
activeListWipLimit() { activeListWipLimit() {
return this.activeList.maxIssueCount === 0 return this.activeList.maxIssueCount === 0 ? this.$options.noneText : this.wipLimitTypeText;
? this.$options.noneText
: this.activeList.maxIssueCount;
}, },
boardListType() { boardListType() {
return this.activeList.type || null; return this.activeList.type || null;
...@@ -101,22 +106,24 @@ export default { ...@@ -101,22 +106,24 @@ export default {
}, },
showInput() { showInput() {
this.edit = true; this.edit = true;
this.currentWipLimit = this.activeList.maxIssueCount; this.currentWipLimit =
this.activeList.maxIssueCount > 0 ? this.activeList.maxIssueCount : null;
}, },
resetStateAfterUpdate() { resetStateAfterUpdate() {
this.edit = false; this.edit = false;
this.updating = false; this.updating = false;
this.currentWipLimit = 0; this.currentWipLimit = null;
}, },
offFocus() { offFocus() {
if (this.currentWipLimit !== this.activeList.maxIssueCount) { if (this.currentWipLimit !== this.activeList.maxIssueCount && this.currentWipLimit !== null) {
this.updating = true; this.updating = true;
// NOTE: Need a ref to activeListId in case the user closes the drawer. // need to reassign bc were clearing the ref in resetStateAfterUpdate.
const wipLimit = this.currentWipLimit;
const id = this.activeListId; const id = this.activeListId;
this.updateListWipLimit({ maxIssueCount: this.currentWipLimit, id }) this.updateListWipLimit({ maxIssueCount: this.currentWipLimit, id })
.then(({ config }) => { .then(() => {
boardsStoreEE.setMaxIssueCountOnList(id, JSON.parse(config.data).list.max_issue_count); boardsStoreEE.setMaxIssueCountOnList(id, wipLimit);
this.resetStateAfterUpdate(); this.resetStateAfterUpdate();
}) })
.catch(() => { .catch(() => {
...@@ -128,6 +135,25 @@ export default { ...@@ -128,6 +135,25 @@ export default {
this.edit = false; this.edit = false;
} }
}, },
clearWipLimit() {
this.updateListWipLimit({ maxIssueCount: 0, id: this.activeListId })
.then(() => {
boardsStoreEE.setMaxIssueCountOnList(this.activeListId, 0);
this.resetStateAfterUpdate();
})
.catch(() => {
this.resetStateAfterUpdate();
this.setActiveListId(0);
flash(__('Something went wrong while updating your list settings'));
});
},
handleWipLimitChange(wipLimit) {
if (wipLimit === '') {
this.currentWipLimit = null;
} else {
this.currentWipLimit = Number(wipLimit);
}
},
onEnter() { onEnter() {
this.offFocus(); this.offFocus();
}, },
...@@ -169,25 +195,39 @@ export default { ...@@ -169,25 +195,39 @@ export default {
}}</gl-link> }}</gl-link>
</template> </template>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between flex-column">
<div> <div class="d-flex justify-content-between align-items-center mb-2">
<label>{{ $options.wipLimitText }}</label> <label class="m-0">{{ $options.wipLimitText }}</label>
<gl-form-input <gl-button
v-if="edit" class="js-edit-button h-100 border-0 gl-line-height-14 text-dark"
v-model.number="currentWipLimit" variant="link"
v-autofocusonshow @click="showInput"
:disabled="updating" >{{ $options.editLinkText }}</gl-button
type="number" >
min="0" </div>
trim <gl-form-input
@keydown.enter.native="onEnter" v-if="edit"
@blur="offFocus" v-autofocusonshow
/> :value="currentWipLimit"
<p v-else class="js-wip-limit bold">{{ activeListWipLimit }}</p> :disabled="updating"
:placeholder="$options.inputPlaceholderText"
trim
@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 gl-line-height-14 text-secondary"
variant="link"
@click="clearWipLimit"
>{{ $options.removeLimitText }}</gl-button
>
</template>
</div> </div>
<gl-button class="h-100 border-0 gl-line-height-14" variant="link" @click="showInput">
{{ $options.editLinkText }}
</gl-button>
</div> </div>
</template> </template>
</gl-drawer> </gl-drawer>
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
%gl-button.no-drag.rounded-right{ type: "button", %gl-button.no-drag.rounded-right{ type: "button",
"@click" => "openSidebarSettings", "@click" => "openSidebarSettings",
"v-if" => "isSettingsShown", "v-if" => "isSettingsShown",
"aria-label" => _("List Settings"), "aria-label" => _("List settings"),
"ref" => "settingsBtn", "ref" => "settingsBtn",
"title" => _("List Settings") } "title" => _("List settings") }
= sprite_icon("settings") = sprite_icon("settings")
%gl-tooltip{ ":target" => "() => $refs.settingsBtn" } %gl-tooltip{ ":target" => "() => $refs.settingsBtn" }
= _("List Settings") = _("List settings")
...@@ -204,14 +204,14 @@ describe 'issue boards', :js do ...@@ -204,14 +204,14 @@ describe 'issue boards', :js do
end end
it 'shows the list settings button' do it 'shows the list settings button' do
expect(page).to have_selector(:button, "List Settings") expect(page).to have_selector(:button, "List settings")
expect(page).not_to have_selector(".js-board-settings-sidebar") expect(page).not_to have_selector(".js-board-settings-sidebar")
end end
context 'when settings button is clicked' do context 'when settings button is clicked' do
it 'shows the board list settings sidebar' do it 'shows the board list settings sidebar' do
page.within(find(".board:nth-child(2)")) do page.within(find(".board:nth-child(2)")) do
click_button('List Settings') click_button('List settings')
end end
expect(page.find('.js-board-settings-sidebar').find('.gl-label-text')).to have_text("Brount") expect(page.find('.js-board-settings-sidebar').find('.gl-label-text')).to have_text("Brount")
...@@ -221,7 +221,31 @@ describe 'issue boards', :js do ...@@ -221,7 +221,31 @@ describe 'issue boards', :js do
context 'when boards setting sidebar is open' do context 'when boards setting sidebar is open' do
before do before do
page.within(find(".board:nth-child(2)")) do page.within(find(".board:nth-child(2)")) do
click_button('List Settings') click_button('List settings')
end
end
context "when user clicks Remove Limit" do
before do
max_issue_count = 2
page.within(find('.js-board-settings-sidebar')) do
click_button("Edit")
find('input').set(max_issue_count)
end
# Off click
find('body').click
wait_for_requests
end
it "sets max issue count to zero" do
page.find('.js-remove-limit').click
wait_for_requests
expect(page.find('.js-wip-limit')).to have_text("None")
end end
end end
...@@ -242,7 +266,7 @@ describe 'issue boards', :js do ...@@ -242,7 +266,7 @@ describe 'issue boards', :js do
wait_for_requests wait_for_requests
page.within(find(".board:nth-child(2)")) do page.within(find(".board:nth-child(2)")) do
click_button('List Settings') click_button('List settings')
end end
expect(page.find('.js-wip-limit')).to have_text(max_issue_count) expect(page.find('.js-wip-limit')).to have_text(max_issue_count)
...@@ -308,7 +332,7 @@ describe 'issue boards', :js do ...@@ -308,7 +332,7 @@ describe 'issue boards', :js do
end end
it 'does not show the list settings button' do it 'does not show the list settings button' do
expect(page).to have_no_selector(:button, "List Settings") expect(page).to have_no_selector(:button, "List settings")
expect(page).not_to have_selector(".js-board-settings-sidebar") expect(page).not_to have_selector(".js-board-settings-sidebar")
end end
end end
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
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 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"`; 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"`; 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 "assignee" renders the correct list type text 1`] = `"Assignee"`;
......
...@@ -3,14 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -3,14 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from 'axios'; import axios from 'axios';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { import { GlDrawer, GlLabel, GlFormInput, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
GlDrawer,
GlLabel,
GlButton,
GlFormInput,
GlAvatarLink,
GlAvatarLabeled,
} from '@gitlab/ui';
import BoardSettingsSidebar from 'ee/boards/components/board_settings_sidebar.vue'; import BoardSettingsSidebar from 'ee/boards/components/board_settings_sidebar.vue';
import boardsStore from 'ee_else_ce/boards/stores/boards_store_ee'; import boardsStore from 'ee_else_ce/boards/stores/boards_store_ee';
import getters from 'ee_else_ce/boards/stores/getters'; import getters from 'ee_else_ce/boards/stores/getters';
...@@ -32,8 +25,9 @@ describe('BoardSettingsSideBar', () => { ...@@ -32,8 +25,9 @@ describe('BoardSettingsSideBar', () => {
const labelTitle = 'test'; const labelTitle = 'test';
const labelColor = '#FFFF'; const labelColor = '#FFFF';
const listId = 1; const listId = 1;
const currentWipLimit = 1; // Needs to be other than null to trigger requests.
const createComponent = (state = {}, actions = {}, localState = {}) => { const createComponent = (state = { activeListId: 0 }, actions = {}, localState = {}) => {
storeActions = actions; storeActions = actions;
const store = new Vuex.Store({ const store = new Vuex.Store({
...@@ -88,23 +82,19 @@ describe('BoardSettingsSideBar', () => { ...@@ -88,23 +82,19 @@ describe('BoardSettingsSideBar', () => {
}); });
describe('on close', () => { describe('on close', () => {
it('calls closeSidebar', done => { it('calls closeSidebar', () => {
const spy = jest.fn(); const spy = jest.fn();
createComponent({}, { setActiveListId: spy }); createComponent({ activeListId: 0 }, { setActiveListId: spy });
wrapper.find(GlDrawer).vm.$emit('close'); wrapper.find(GlDrawer).vm.$emit('close');
return wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() expect(storeActions.setActiveListId).toHaveBeenCalledWith(
.then(() => { expect.anything(),
expect(storeActions.setActiveListId).toHaveBeenCalledWith( 0,
expect.anything(), undefined,
0, );
undefined, });
);
})
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -307,45 +297,80 @@ describe('BoardSettingsSideBar', () => { ...@@ -307,45 +297,80 @@ describe('BoardSettingsSideBar', () => {
list_type: 'label', list_type: 'label',
}); });
createComponent({ activeListId: listId }); createComponent({ activeListId: listId }, { updateListWipLimit: () => {} });
}); });
it('renders an input', done => { it('renders an input', () => {
wrapper.find(GlButton).vm.$emit('click'); wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() expect(wrapper.find(GlFormInput).exists()).toBe(true);
.then(() => { });
expect(wrapper.find(GlFormInput).exists()).toBe(true);
})
.then(done)
.catch(done.fail);
}); });
it('does not render current wipLimit text', done => { it('does not render current wipLimit text', () => {
wrapper.find(GlButton).vm.$emit('click'); wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() expect(wrapper.find('.js-wip-limit').exists()).toBe(false);
.then(() => { });
expect(wrapper.find('.js-wip-limit').exists()).toBe(false);
})
.then(done)
.catch(done.fail);
}); });
it('sets wipLimit to be the value of list.maxIssueCount', done => { it('sets wipLimit to be the value of list.maxIssueCount', () => {
expect(wrapper.vm.currentWipLimit).toEqual(0); expect(wrapper.vm.currentWipLimit).toEqual(null);
wrapper.find(GlButton).vm.$emit('click'); wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() expect(wrapper.vm.currentWipLimit).toBe(4);
.then(() => { });
expect(wrapper.vm.currentWipLimit).toBe(4); });
}) });
.then(done)
.catch(done.fail); 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({ activeListId: 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({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: 0,
list_type: 'label',
});
createComponent({ activeListId: listId }, { updateListWipLimit: () => {} });
});
it('does not render the remove limit button', () => {
expect(wrapper.find('.js-remove-limit').exists()).toBe(false);
});
}); });
}); });
...@@ -374,9 +399,13 @@ describe('BoardSettingsSideBar', () => { ...@@ -374,9 +399,13 @@ describe('BoardSettingsSideBar', () => {
describe(`when blur is triggered by ${blurMethod}`, () => { describe(`when blur is triggered by ${blurMethod}`, () => {
it('calls updateListWipLimit', () => { it('calls updateListWipLimit', () => {
const spy = jest.fn().mockResolvedValue({ const spy = jest.fn().mockResolvedValue({
config: { data: JSON.stringify({ list: { max_issue_count: 'hello' } }) }, config: { data: JSON.stringify({ list: { max_issue_count: '4' } }) },
}); });
createComponent({ activeListId: 1 }, { updateListWipLimit: spy }, { edit: true }); createComponent(
{ activeListId: 1 },
{ updateListWipLimit: spy },
{ edit: true, currentWipLimit },
);
triggerBlur(blurMethod); triggerBlur(blurMethod);
...@@ -387,11 +416,7 @@ describe('BoardSettingsSideBar', () => { ...@@ -387,11 +416,7 @@ describe('BoardSettingsSideBar', () => {
describe('when component wipLimit and List.maxIssueCount are equal', () => { describe('when component wipLimit and List.maxIssueCount are equal', () => {
it('doesnt call updateListWipLimit', () => { it('doesnt call updateListWipLimit', () => {
const spy = jest.fn(() => const spy = jest.fn().mockResolvedValue({});
Promise.resolve({
config: { data: JSON.stringify({ list: { max_issue_count: 0 } }) },
}),
);
createComponent( createComponent(
{ activeListId: 1 }, { activeListId: 1 },
{ updateListWipLimit: spy }, { updateListWipLimit: spy },
...@@ -406,16 +431,33 @@ describe('BoardSettingsSideBar', () => { ...@@ -406,16 +431,33 @@ describe('BoardSettingsSideBar', () => {
}); });
}); });
describe('when currentWipLimit is null', () => {
it('doesnt call updateListWipLimit', () => {
const spy = jest.fn().mockResolvedValue({});
createComponent(
{ activeListId: 1 },
{ updateListWipLimit: spy },
{ edit: true, currentWipLimit: null },
);
triggerBlur(blurMethod);
return wrapper.vm.$nextTick().then(() => {
expect(spy).toHaveBeenCalledTimes(0);
});
});
});
describe('when response is successful', () => { describe('when response is successful', () => {
const maxIssueCount = 11; const maxIssueCount = 11;
beforeEach(() => { beforeEach(() => {
const spy = jest.fn(() => const spy = jest.fn().mockResolvedValue({});
Promise.resolve({ createComponent(
config: { data: JSON.stringify({ list: { max_issue_count: maxIssueCount } }) }, { activeListId: 1 },
}), { updateListWipLimit: spy },
{ edit: true, currentWipLimit: maxIssueCount },
); );
createComponent({ activeListId: 1 }, { updateListWipLimit: spy }, { edit: true });
triggerBlur(blurMethod); triggerBlur(blurMethod);
...@@ -445,7 +487,7 @@ describe('BoardSettingsSideBar', () => { ...@@ -445,7 +487,7 @@ describe('BoardSettingsSideBar', () => {
createComponent( createComponent(
{ activeListId: 1 }, { activeListId: 1 },
{ updateListWipLimit: spy, setActiveListId: () => {} }, { updateListWipLimit: spy, setActiveListId: () => {} },
{ edit: true }, { edit: true, currentWipLimit },
); );
triggerBlur(blurMethod); triggerBlur(blurMethod);
......
...@@ -7164,6 +7164,9 @@ msgstr "" ...@@ -7164,6 +7164,9 @@ msgstr ""
msgid "Enter new AWS Secret Access Key" msgid "Enter new AWS Secret Access Key"
msgstr "" msgstr ""
msgid "Enter number of issues"
msgstr ""
msgid "Enter the issue description" msgid "Enter the issue description"
msgstr "" msgstr ""
...@@ -11292,9 +11295,6 @@ msgstr "" ...@@ -11292,9 +11295,6 @@ msgstr ""
msgid "List" msgid "List"
msgstr "" msgstr ""
msgid "List Settings"
msgstr ""
msgid "List Your Gitea Repositories" msgid "List Your Gitea Repositories"
msgstr "" msgstr ""
...@@ -11304,6 +11304,9 @@ msgstr "" ...@@ -11304,6 +11304,9 @@ msgstr ""
msgid "List of IPs and CIDRs of allowed secondary nodes. Comma-separated, e.g. \"1.1.1.1, 2.2.2.0/24\"" msgid "List of IPs and CIDRs of allowed secondary nodes. Comma-separated, e.g. \"1.1.1.1, 2.2.2.0/24\""
msgstr "" msgstr ""
msgid "List settings"
msgstr ""
msgid "List the merge requests that must be merged before this one." msgid "List the merge requests that must be merged before this one."
msgstr "" msgstr ""
...@@ -15610,6 +15613,9 @@ msgstr "" ...@@ -15610,6 +15613,9 @@ msgstr ""
msgid "Remove group" msgid "Remove group"
msgstr "" msgstr ""
msgid "Remove limit"
msgstr ""
msgid "Remove milestone" msgid "Remove milestone"
msgstr "" msgstr ""
...@@ -21439,7 +21445,7 @@ msgstr "" ...@@ -21439,7 +21445,7 @@ msgstr ""
msgid "Withdraw Access Request" msgid "Withdraw Access Request"
msgstr "" msgstr ""
msgid "Work in Progress Limit" msgid "Work in progress Limit"
msgstr "" msgstr ""
msgid "Workflow Help" msgid "Workflow Help"
......
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