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';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
import BoardConfigurationOptions from './board_configuration_options.vue';
const boardDefaults = {
id: false,
name: '',
......@@ -13,12 +15,15 @@ const boardDefaults = {
assignee: {},
assignee_id: undefined,
weight: null,
hide_backlog_list: false,
hide_closed_list: false,
};
export default {
components: {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
DeprecatedModal,
BoardConfigurationOptions,
},
props: {
canAdminBoard: {
......@@ -140,7 +145,17 @@ export default {
} else {
boardsStore
.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 => {
visitUrl(data.board_path);
})
......@@ -182,7 +197,7 @@ export default {
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="append-bottom-20">
<label class="form-section-title label-bold" for="board-new-name">{{
__('Board name')
__('Title')
}}</label>
<input
id="board-new-name"
......@@ -196,6 +211,12 @@ export default {
/>
</div>
<board-configuration-options
:is-new-form="isNewForm"
:board="board"
:current-board="currentBoard"
/>
<board-scope
v-if="scopedIssueBoardFeatureEnabled"
: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 @@
/* global List */
/* global ListIssue */
import $ from 'jquery';
import { sortBy } from 'lodash';
import { sortBy, pick } from 'lodash';
import Vue from 'vue';
import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
......@@ -12,6 +12,7 @@ import {
parseBoolean,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
......@@ -23,7 +24,11 @@ import ListLabel from '../models/label';
import ListAssignee from '../models/assignee';
import ListMilestone from '../models/milestone';
import createBoardMutation from '../queries/board.mutation.graphql';
const PER_PAGE = 20;
export const gqlClient = createDefaultClient();
const boardsStore = {
disabled: false,
timeTracking: {
......@@ -542,6 +547,10 @@ const boardsStore = {
this.timeTracking.limitToHours = parseBoolean(limitToHours);
},
generateBoardGid(boardId) {
return `gid://gitlab/Board/${boardId}`;
},
generateBoardsPath(id) {
return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`;
},
......@@ -800,9 +809,33 @@ const boardsStore = {
}
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 }) {
......
......@@ -94,8 +94,8 @@ export default {
<template>
<div data-qa-selector="board_scope_modal">
<div v-if="canAdminBoard" class="media gl-mb-3">
<label class="form-section-title label-bold media-body">{{ __('Board scope') }}</label>
<div v-if="canAdminBoard" class="media">
<label class="form-section-title label-bold media-body">{{ __('Scope') }}</label>
<button v-if="collapseScope" type="button" class="btn" @click="expanded = !expanded">
{{ expandButtonText }}
</button>
......
......@@ -93,7 +93,7 @@
width: 440px;
.block {
padding: $gl-padding 0;
padding: $gl-padding-8 0;
// add a border between all items, but not at the start or end
+ .block {
......
......@@ -10,6 +10,8 @@ module EE
expose :milestone, using: BoardMilestoneEntity
expose :assignee, using: BoardAssigneeEntity
expose :labels, using: BoardLabelEntity
expose :hide_backlog_list
expose :hide_closed_list
end
end
end
---
title: Config to hide Open/Closed list in Boards
merge_request: 42945
author:
type: added
......@@ -4068,9 +4068,6 @@ msgstr ""
msgid "Blog"
msgstr ""
msgid "Board name"
msgstr ""
msgid "Board scope"
msgstr ""
......@@ -6683,6 +6680,9 @@ msgstr ""
msgid "Configure the way a user creates a new account."
msgstr ""
msgid "Configure which lists are shown for anyone who visits this board"
msgstr ""
msgid "Confirm"
msgstr ""
......@@ -15150,6 +15150,9 @@ msgstr ""
msgid "List of all merge commits"
msgstr ""
msgid "List options"
msgstr ""
msgid "List settings"
msgstr ""
......@@ -23518,6 +23521,12 @@ msgstr ""
msgid "Show parent subgroups"
msgstr ""
msgid "Show the Closed list"
msgstr ""
msgid "Show the Open list"
msgstr ""
msgid "Show whitespace changes"
msgstr ""
......
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
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 { listObj, listObjDuplicate } from './mock_data';
......@@ -503,11 +503,15 @@ describe('boardsStore', () => {
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPut(url).replyOnce(config => requestSpy(config));
jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({}));
});
it('makes a request to update the board', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
const expectedResponse = [
expect.objectContaining({ data: dummyResponse }),
expect.objectContaining({}),
];
return expect(
boardsStore.createBoard({
......@@ -555,11 +559,12 @@ describe('boardsStore', () => {
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPost(url).replyOnce(config => requestSpy(config));
jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({}));
});
it('makes a request to create a new board', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
const expectedResponse = dummyResponse;
return expect(boardsStore.createBoard(board))
.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