Commit d4b601d9 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'boards-config' into 'master'

Config to hide Open/Closed list in Boards

See merge request gitlab-org/gitlab!42945
parents 074128b8 d1b9f1b0
<script>
import { GlFormCheckbox } from '@gitlab/ui';
export default {
components: {
GlFormCheckbox,
},
props: {
currentBoard: {
type: Object,
required: true,
},
board: {
type: Object,
required: true,
},
isNewForm: {
type: Boolean,
required: false,
default: false,
},
},
data() {
const { hide_backlog_list: hideBacklogList, hide_closed_list: hideClosedList } = this.isNewForm
? this.board
: this.currentBoard;
return {
hideClosedList,
hideBacklogList,
};
},
methods: {
changeClosedList(checked) {
this.board.hideClosedList = !checked;
},
changeBacklogList(checked) {
this.board.hideBacklogList = !checked;
},
},
};
</script>
<template>
<div class="append-bottom-20">
<label class="form-section-title label-bold" for="board-new-name">
{{ __('List options') }}
</label>
<p class="text-secondary gl-mb-3">
{{ __('Configure which lists are shown for anyone who visits this board') }}
</p>
<gl-form-checkbox
:checked="!hideBacklogList"
data-testid="backlog-list-checkbox"
@change="changeBacklogList"
>{{ __('Show the Open list') }}
</gl-form-checkbox>
<gl-form-checkbox
:checked="!hideClosedList"
data-testid="closed-list-checkbox"
@change="changeClosedList"
>{{ __('Show the Closed list') }}
</gl-form-checkbox>
</div>
</template>
...@@ -5,6 +5,8 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; ...@@ -5,6 +5,8 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import BoardConfigurationOptions from './board_configuration_options.vue';
const boardDefaults = { const boardDefaults = {
id: false, id: false,
name: '', name: '',
...@@ -13,12 +15,15 @@ const boardDefaults = { ...@@ -13,12 +15,15 @@ const boardDefaults = {
assignee: {}, assignee: {},
assignee_id: undefined, assignee_id: undefined,
weight: null, weight: null,
hide_backlog_list: false,
hide_closed_list: false,
}; };
export default { export default {
components: { components: {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'), BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
DeprecatedModal, DeprecatedModal,
BoardConfigurationOptions,
}, },
props: { props: {
canAdminBoard: { canAdminBoard: {
...@@ -140,7 +145,17 @@ export default { ...@@ -140,7 +145,17 @@ export default {
} else { } else {
boardsStore boardsStore
.createBoard(this.board) .createBoard(this.board)
.then(resp => resp.data) .then(resp => {
// This handles 2 use cases
// - In create call we only get one parameter, the new board
// - In update call, due to Promise.all, we get REST response in
// array index 0
if (Array.isArray(resp)) {
return resp[0].data;
}
return resp.data ? resp.data : resp;
})
.then(data => { .then(data => {
visitUrl(data.board_path); visitUrl(data.board_path);
}) })
...@@ -182,7 +197,7 @@ export default { ...@@ -182,7 +197,7 @@ export default {
<form v-else class="js-board-config-modal" @submit.prevent> <form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="append-bottom-20"> <div v-if="!readonly" class="append-bottom-20">
<label class="form-section-title label-bold" for="board-new-name">{{ <label class="form-section-title label-bold" for="board-new-name">{{
__('Board name') __('Title')
}}</label> }}</label>
<input <input
id="board-new-name" id="board-new-name"
...@@ -196,6 +211,12 @@ export default { ...@@ -196,6 +211,12 @@ export default {
/> />
</div> </div>
<board-configuration-options
:is-new-form="isNewForm"
:board="board"
:current-board="currentBoard"
/>
<board-scope <board-scope
v-if="scopedIssueBoardFeatureEnabled" v-if="scopedIssueBoardFeatureEnabled"
:collapse-scope="isNewForm" :collapse-scope="isNewForm"
......
mutation UpdateBoard($id: ID!, $hideClosedList: Boolean, $hideBacklogList: Boolean) {
updateBoard(
input: { id: $id, hideClosedList: $hideClosedList, hideBacklogList: $hideBacklogList }
) {
board {
id
hideClosedList
hideBacklogList
}
}
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global List */ /* global List */
/* global ListIssue */ /* global ListIssue */
import $ from 'jquery'; import $ from 'jquery';
import { sortBy } from 'lodash'; import { sortBy, pick } from 'lodash';
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
parseBoolean, parseBoolean,
convertObjectPropsToCamelCase, convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import { __ } from '~/locale'; import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
...@@ -23,7 +24,11 @@ import ListLabel from '../models/label'; ...@@ -23,7 +24,11 @@ import ListLabel from '../models/label';
import ListAssignee from '../models/assignee'; import ListAssignee from '../models/assignee';
import ListMilestone from '../models/milestone'; import ListMilestone from '../models/milestone';
import createBoardMutation from '../queries/board.mutation.graphql';
const PER_PAGE = 20; const PER_PAGE = 20;
export const gqlClient = createDefaultClient();
const boardsStore = { const boardsStore = {
disabled: false, disabled: false,
timeTracking: { timeTracking: {
...@@ -542,6 +547,10 @@ const boardsStore = { ...@@ -542,6 +547,10 @@ const boardsStore = {
this.timeTracking.limitToHours = parseBoolean(limitToHours); this.timeTracking.limitToHours = parseBoolean(limitToHours);
}, },
generateBoardGid(boardId) {
return `gid://gitlab/Board/${boardId}`;
},
generateBoardsPath(id) { generateBoardsPath(id) {
return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`; return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`;
}, },
...@@ -800,9 +809,33 @@ const boardsStore = { ...@@ -800,9 +809,33 @@ const boardsStore = {
} }
if (boardPayload.id) { if (boardPayload.id) {
return axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload }); const input = {
...pick(boardPayload, ['hideClosedList', 'hideBacklogList']),
id: this.generateBoardGid(boardPayload.id),
};
return Promise.all([
axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload }),
gqlClient.mutate({
mutation: createBoardMutation,
variables: input,
}),
]);
} }
return axios.post(this.generateBoardsPath(), { board: boardPayload });
return axios
.post(this.generateBoardsPath(), { board: boardPayload })
.then(resp => resp.data)
.then(data => {
gqlClient.mutate({
mutation: createBoardMutation,
variables: {
...pick(boardPayload, ['hideClosedList', 'hideBacklogList']),
id: this.generateBoardGid(data.id),
},
});
return data;
});
}, },
deleteBoard({ id }) { deleteBoard({ id }) {
......
...@@ -94,8 +94,8 @@ export default { ...@@ -94,8 +94,8 @@ export default {
<template> <template>
<div data-qa-selector="board_scope_modal"> <div data-qa-selector="board_scope_modal">
<div v-if="canAdminBoard" class="media gl-mb-3"> <div v-if="canAdminBoard" class="media">
<label class="form-section-title label-bold media-body">{{ __('Board scope') }}</label> <label class="form-section-title label-bold media-body">{{ __('Scope') }}</label>
<button v-if="collapseScope" type="button" class="btn" @click="expanded = !expanded"> <button v-if="collapseScope" type="button" class="btn" @click="expanded = !expanded">
{{ expandButtonText }} {{ expandButtonText }}
</button> </button>
......
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
width: 440px; width: 440px;
.block { .block {
padding: $gl-padding 0; padding: $gl-padding-8 0;
// add a border between all items, but not at the start or end // add a border between all items, but not at the start or end
+ .block { + .block {
......
...@@ -10,6 +10,8 @@ module EE ...@@ -10,6 +10,8 @@ module EE
expose :milestone, using: BoardMilestoneEntity expose :milestone, using: BoardMilestoneEntity
expose :assignee, using: BoardAssigneeEntity expose :assignee, using: BoardAssigneeEntity
expose :labels, using: BoardLabelEntity expose :labels, using: BoardLabelEntity
expose :hide_backlog_list
expose :hide_closed_list
end end
end end
end end
---
title: Config to hide Open/Closed list in Boards
merge_request: 42945
author:
type: added
...@@ -4068,9 +4068,6 @@ msgstr "" ...@@ -4068,9 +4068,6 @@ msgstr ""
msgid "Blog" msgid "Blog"
msgstr "" msgstr ""
msgid "Board name"
msgstr ""
msgid "Board scope" msgid "Board scope"
msgstr "" msgstr ""
...@@ -6683,6 +6680,9 @@ msgstr "" ...@@ -6683,6 +6680,9 @@ msgstr ""
msgid "Configure the way a user creates a new account." msgid "Configure the way a user creates a new account."
msgstr "" msgstr ""
msgid "Configure which lists are shown for anyone who visits this board"
msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "" msgstr ""
...@@ -15150,6 +15150,9 @@ msgstr "" ...@@ -15150,6 +15150,9 @@ msgstr ""
msgid "List of all merge commits" msgid "List of all merge commits"
msgstr "" msgstr ""
msgid "List options"
msgstr ""
msgid "List settings" msgid "List settings"
msgstr "" msgstr ""
...@@ -23518,6 +23521,12 @@ msgstr "" ...@@ -23518,6 +23521,12 @@ msgstr ""
msgid "Show parent subgroups" msgid "Show parent subgroups"
msgstr "" msgstr ""
msgid "Show the Closed list"
msgstr ""
msgid "Show the Open list"
msgstr ""
msgid "Show whitespace changes" msgid "Show whitespace changes"
msgstr "" msgstr ""
......
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore, { gqlClient } from '~/boards/stores/boards_store';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate } from './mock_data'; import { listObj, listObjDuplicate } from './mock_data';
...@@ -503,11 +503,15 @@ describe('boardsStore', () => { ...@@ -503,11 +503,15 @@ describe('boardsStore', () => {
beforeEach(() => { beforeEach(() => {
requestSpy = jest.fn(); requestSpy = jest.fn();
axiosMock.onPut(url).replyOnce(config => requestSpy(config)); axiosMock.onPut(url).replyOnce(config => requestSpy(config));
jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({}));
}); });
it('makes a request to update the board', () => { it('makes a request to update the board', () => {
requestSpy.mockReturnValue([200, dummyResponse]); requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse }); const expectedResponse = [
expect.objectContaining({ data: dummyResponse }),
expect.objectContaining({}),
];
return expect( return expect(
boardsStore.createBoard({ boardsStore.createBoard({
...@@ -555,11 +559,12 @@ describe('boardsStore', () => { ...@@ -555,11 +559,12 @@ describe('boardsStore', () => {
beforeEach(() => { beforeEach(() => {
requestSpy = jest.fn(); requestSpy = jest.fn();
axiosMock.onPost(url).replyOnce(config => requestSpy(config)); axiosMock.onPost(url).replyOnce(config => requestSpy(config));
jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({}));
}); });
it('makes a request to create a new board', () => { it('makes a request to create a new board', () => {
requestSpy.mockReturnValue([200, dummyResponse]); requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse }); const expectedResponse = dummyResponse;
return expect(boardsStore.createBoard(board)) return expect(boardsStore.createBoard(board))
.resolves.toEqual(expectedResponse) .resolves.toEqual(expectedResponse)
......
import { shallowMount } from '@vue/test-utils';
import BoardConfigurationOptions from '~/boards/components/board_configuration_options.vue';
describe('BoardConfigurationOptions', () => {
let wrapper;
const board = { hide_backlog_list: false, hide_closed_list: false };
const defaultProps = {
currentBoard: board,
board,
isNewForm: false,
};
const createComponent = () => {
wrapper = shallowMount(BoardConfigurationOptions, {
propsData: { ...defaultProps },
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const backlogListCheckbox = el => el.find('[data-testid="backlog-list-checkbox"]');
const closedListCheckbox = el => el.find('[data-testid="closed-list-checkbox"]');
const checkboxAssert = (backlogCheckbox, closedCheckbox) => {
expect(backlogListCheckbox(wrapper).attributes('checked')).toEqual(
backlogCheckbox ? undefined : 'true',
);
expect(closedListCheckbox(wrapper).attributes('checked')).toEqual(
closedCheckbox ? undefined : 'true',
);
};
it.each`
backlogCheckboxValue | closedCheckboxValue
${true} | ${true}
${true} | ${false}
${false} | ${true}
${false} | ${false}
`(
'renders two checkbox when one is $backlogCheckboxValue and other is $closedCheckboxValue',
async ({ backlogCheckboxValue, closedCheckboxValue }) => {
await wrapper.setData({
hideBacklogList: backlogCheckboxValue,
hideClosedList: closedCheckboxValue,
});
return wrapper.vm.$nextTick().then(() => {
checkboxAssert(backlogCheckboxValue, closedCheckboxValue);
});
},
);
});
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