Commit 24b2746e authored by Scott Stern's avatar Scott Stern Committed by Phil Hughes

Add remove limit button to wip limit

Added remove limit button to board sidebar
so user can remove wip limit
parent fe5227b3
......@@ -9,7 +9,7 @@ import {
GlLink,
} from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import { __, n__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import boardsStoreEE from '../stores/boards_store_ee';
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.
export default {
headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List Settings'),
listSettingsText: __('List settings'),
assignee: 'assignee',
milestone: 'milestone',
label: 'label',
......@@ -26,7 +26,9 @@ export default {
labelAssigneeText: __('Assignee'),
editLinkText: __('Edit'),
noneText: __('None'),
wipLimitText: __('Work in Progress Limit'),
wipLimitText: __('Work in progress Limit'),
removeLimitText: __('Remove limit'),
inputPlaceholderText: __('Enter number of issues'),
components: {
GlDrawer,
GlLabel,
......@@ -42,7 +44,7 @@ export default {
data() {
return {
edit: false,
currentWipLimit: 0,
currentWipLimit: null,
updating: false,
};
},
......@@ -53,7 +55,6 @@ 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.activeListId);
},
isSidebarOpen() {
......@@ -68,10 +69,14 @@ 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.activeList.maxIssueCount;
return this.activeList.maxIssueCount === 0 ? this.$options.noneText : this.wipLimitTypeText;
},
boardListType() {
return this.activeList.type || null;
......@@ -101,22 +106,24 @@ export default {
},
showInput() {
this.edit = true;
this.currentWipLimit = this.activeList.maxIssueCount;
this.currentWipLimit =
this.activeList.maxIssueCount > 0 ? this.activeList.maxIssueCount : null;
},
resetStateAfterUpdate() {
this.edit = false;
this.updating = false;
this.currentWipLimit = 0;
this.currentWipLimit = null;
},
offFocus() {
if (this.currentWipLimit !== this.activeList.maxIssueCount) {
if (this.currentWipLimit !== this.activeList.maxIssueCount && this.currentWipLimit !== null) {
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;
this.updateListWipLimit({ maxIssueCount: this.currentWipLimit, id })
.then(({ config }) => {
boardsStoreEE.setMaxIssueCountOnList(id, JSON.parse(config.data).list.max_issue_count);
.then(() => {
boardsStoreEE.setMaxIssueCountOnList(id, wipLimit);
this.resetStateAfterUpdate();
})
.catch(() => {
......@@ -128,6 +135,25 @@ export default {
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() {
this.offFocus();
},
......@@ -169,25 +195,39 @@ export default {
}}</gl-link>
</template>
</div>
<div class="d-flex justify-content-between">
<div>
<label>{{ $options.wipLimitText }}</label>
<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 gl-line-height-14 text-dark"
variant="link"
@click="showInput"
>{{ $options.editLinkText }}</gl-button
>
</div>
<gl-form-input
v-if="edit"
v-model.number="currentWipLimit"
v-autofocusonshow
:value="currentWipLimit"
:disabled="updating"
type="number"
min="0"
:placeholder="$options.inputPlaceholderText"
trim
@input="handleWipLimitChange"
@keydown.enter.native="onEnter"
@blur="offFocus"
/>
<p v-else class="js-wip-limit bold">{{ activeListWipLimit }}</p>
<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>
<gl-button class="h-100 border-0 gl-line-height-14" variant="link" @click="showInput">
{{ $options.editLinkText }}
</gl-button>
</div>
</template>
</gl-drawer>
......
......@@ -2,9 +2,9 @@
%gl-button.no-drag.rounded-right{ type: "button",
"@click" => "openSidebarSettings",
"v-if" => "isSettingsShown",
"aria-label" => _("List Settings"),
"aria-label" => _("List settings"),
"ref" => "settingsBtn",
"title" => _("List Settings") }
"title" => _("List settings") }
= sprite_icon("settings")
%gl-tooltip{ ":target" => "() => $refs.settingsBtn" }
= _("List Settings")
= _("List settings")
......@@ -204,14 +204,14 @@ describe 'issue boards', :js do
end
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")
end
context 'when settings button is clicked' do
it 'shows the board list settings sidebar' do
page.within(find(".board:nth-child(2)")) do
click_button('List Settings')
click_button('List settings')
end
expect(page.find('.js-board-settings-sidebar').find('.gl-label-text')).to have_text("Brount")
......@@ -221,7 +221,31 @@ describe 'issue boards', :js do
context 'when boards setting sidebar is open' do
before 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
......@@ -242,7 +266,7 @@ describe 'issue boards', :js do
wait_for_requests
page.within(find(".board:nth-child(2)")) do
click_button('List Settings')
click_button('List settings')
end
expect(page.find('.js-wip-limit')).to have_text(max_issue_count)
......@@ -308,7 +332,7 @@ describe 'issue boards', :js do
end
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")
end
end
......
......@@ -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 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"`;
......
......@@ -3,14 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import {
GlDrawer,
GlLabel,
GlButton,
GlFormInput,
GlAvatarLink,
GlAvatarLabeled,
} from '@gitlab/ui';
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';
......@@ -32,8 +25,9 @@ describe('BoardSettingsSideBar', () => {
const labelTitle = 'test';
const labelColor = '#FFFF';
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;
const store = new Vuex.Store({
......@@ -88,23 +82,19 @@ describe('BoardSettingsSideBar', () => {
});
describe('on close', () => {
it('calls closeSidebar', done => {
it('calls closeSidebar', () => {
const spy = jest.fn();
createComponent({}, { setActiveListId: spy });
createComponent({ activeListId: 0 }, { setActiveListId: spy });
wrapper.find(GlDrawer).vm.$emit('close');
return wrapper.vm
.$nextTick()
.then(() => {
return wrapper.vm.$nextTick().then(() => {
expect(storeActions.setActiveListId).toHaveBeenCalledWith(
expect.anything(),
0,
undefined,
);
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -307,45 +297,80 @@ describe('BoardSettingsSideBar', () => {
list_type: 'label',
});
createComponent({ activeListId: listId });
createComponent({ activeListId: listId }, { updateListWipLimit: () => {} });
});
it('renders an input', done => {
wrapper.find(GlButton).vm.$emit('click');
it('renders an input', () => {
wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm
.$nextTick()
.then(() => {
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlFormInput).exists()).toBe(true);
})
.then(done)
.catch(done.fail);
});
});
it('does not render current wipLimit text', done => {
wrapper.find(GlButton).vm.$emit('click');
it('does not render current wipLimit text', () => {
wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm
.$nextTick()
.then(() => {
return wrapper.vm.$nextTick().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 => {
expect(wrapper.vm.currentWipLimit).toEqual(0);
it('sets wipLimit to be the value of list.maxIssueCount', () => {
expect(wrapper.vm.currentWipLimit).toEqual(null);
wrapper.find(GlButton).vm.$emit('click');
wrapper.find('.js-edit-button').vm.$emit('click');
return wrapper.vm
.$nextTick()
.then(() => {
return wrapper.vm.$nextTick().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', () => {
describe(`when blur is triggered by ${blurMethod}`, () => {
it('calls updateListWipLimit', () => {
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);
......@@ -387,11 +416,7 @@ describe('BoardSettingsSideBar', () => {
describe('when component wipLimit and List.maxIssueCount are equal', () => {
it('doesnt call updateListWipLimit', () => {
const spy = jest.fn(() =>
Promise.resolve({
config: { data: JSON.stringify({ list: { max_issue_count: 0 } }) },
}),
);
const spy = jest.fn().mockResolvedValue({});
createComponent(
{ activeListId: 1 },
{ updateListWipLimit: spy },
......@@ -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', () => {
const maxIssueCount = 11;
beforeEach(() => {
const spy = jest.fn(() =>
Promise.resolve({
config: { data: JSON.stringify({ list: { max_issue_count: maxIssueCount } }) },
}),
const spy = jest.fn().mockResolvedValue({});
createComponent(
{ activeListId: 1 },
{ updateListWipLimit: spy },
{ edit: true, currentWipLimit: maxIssueCount },
);
createComponent({ activeListId: 1 }, { updateListWipLimit: spy }, { edit: true });
triggerBlur(blurMethod);
......@@ -445,7 +487,7 @@ describe('BoardSettingsSideBar', () => {
createComponent(
{ activeListId: 1 },
{ updateListWipLimit: spy, setActiveListId: () => {} },
{ edit: true },
{ edit: true, currentWipLimit },
);
triggerBlur(blurMethod);
......
......@@ -7164,6 +7164,9 @@ msgstr ""
msgid "Enter new AWS Secret Access Key"
msgstr ""
msgid "Enter number of issues"
msgstr ""
msgid "Enter the issue description"
msgstr ""
......@@ -11292,9 +11295,6 @@ msgstr ""
msgid "List"
msgstr ""
msgid "List Settings"
msgstr ""
msgid "List Your Gitea Repositories"
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\""
msgstr ""
msgid "List settings"
msgstr ""
msgid "List the merge requests that must be merged before this one."
msgstr ""
......@@ -15610,6 +15613,9 @@ msgstr ""
msgid "Remove group"
msgstr ""
msgid "Remove limit"
msgstr ""
msgid "Remove milestone"
msgstr ""
......@@ -21439,7 +21445,7 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
msgid "Work in Progress Limit"
msgid "Work in progress Limit"
msgstr ""
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