Commit 4f838cf8 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '233434-epic-boards-create-an-epic-board' into 'master'

Create Epic Board [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!54642
parents 5b012457 ac49ac1d
......@@ -161,42 +161,49 @@ export default {
currentMutation() {
return this.board.id ? updateBoardMutation : createBoardMutation;
},
mutationVariables() {
baseMutationVariables() {
const { board } = this;
/* eslint-disable @gitlab/require-i18n-strings */
let baseMutationVariables = {
const variables = {
name: board.name,
hideBacklogList: board.hide_backlog_list,
hideClosedList: board.hide_closed_list,
};
if (this.scopedIssueBoardFeatureEnabled) {
baseMutationVariables = {
...baseMutationVariables,
weight: board.weight,
assigneeId: board.assignee?.id ? convertToGraphQLId('User', board.assignee.id) : null,
milestoneId:
board.milestone?.id || board.milestone?.id === 0
? convertToGraphQLId('Milestone', board.milestone.id)
: null,
labelIds: board.labels.map(fullLabelId),
iterationId: board.iteration_id
? convertToGraphQLId('Iteration', board.iteration_id)
: null,
};
}
/* eslint-enable @gitlab/require-i18n-strings */
return board.id
? {
...baseMutationVariables,
...variables,
id: fullBoardId(board.id),
}
: {
...baseMutationVariables,
projectPath: this.projectId ? this.fullPath : null,
groupPath: this.groupId ? this.fullPath : null,
...variables,
projectPath: this.projectId ? this.fullPath : undefined,
groupPath: this.groupId ? this.fullPath : undefined,
};
},
boardScopeMutationVariables() {
/* eslint-disable @gitlab/require-i18n-strings */
return {
weight: this.board.weight,
assigneeId: this.board.assignee?.id
? convertToGraphQLId('User', this.board.assignee.id)
: null,
milestoneId:
this.board.milestone?.id || this.board.milestone?.id === 0
? convertToGraphQLId('Milestone', this.board.milestone.id)
: null,
labelIds: this.board.labels.map(fullLabelId),
iterationId: this.board.iteration_id
? convertToGraphQLId('Iteration', this.board.iteration_id)
: null,
};
/* eslint-enable @gitlab/require-i18n-strings */
},
mutationVariables() {
return {
...this.baseMutationVariables,
...(this.scopedIssueBoardFeatureEnabled ? this.boardScopeMutationVariables : {}),
};
},
},
mounted() {
this.resetFormState();
......@@ -208,6 +215,16 @@ export default {
setIteration(iterationId) {
this.board.iteration_id = iterationId;
},
boardCreateResponse(data) {
return data.createBoard.board.webPath;
},
boardUpdateResponse(data) {
const path = data.updateBoard.board.webPath;
const param = getParameterByName('group_by')
? `?group_by=${getParameterByName('group_by')}`
: '';
return `${path}${param}`;
},
async createOrUpdateBoard() {
const response = await this.$apollo.mutate({
mutation: this.currentMutation,
......@@ -215,14 +232,10 @@ export default {
});
if (!this.board.id) {
return response.data.createBoard.board.webPath;
return this.boardCreateResponse(response.data);
}
const path = response.data.updateBoard.board.webPath;
const param = getParameterByName('group_by')
? `?group_by=${getParameterByName('group_by')}`
: '';
return `${path}${param}`;
return this.boardUpdateResponse(response.data);
},
async submit() {
if (this.board.name.length === 0) return;
......
......@@ -10,6 +10,8 @@ import {
} from '@gitlab/ui';
import { throttle } from 'lodash';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
......@@ -18,8 +20,6 @@ import eventHub from '../eventhub';
import groupQuery from '../graphql/group_boards.query.graphql';
import projectQuery from '../graphql/project_boards.query.graphql';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
export default {
......@@ -123,6 +123,9 @@ export default {
board() {
return this.currentBoard;
},
showCreate() {
return this.multipleIssueBoardsAvailable;
},
showDelete() {
return this.boards.length > 1;
},
......@@ -327,7 +330,7 @@ export default {
<gl-dropdown-divider />
<gl-dropdown-item
v-if="multipleIssueBoardsAvailable"
v-if="showCreate"
v-gl-modal-directive="'board-config-modal'"
data-qa-selector="create_new_board_button"
@click.prevent="showPage('new')"
......
<script>
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
// extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */
import { mapState } from 'vuex';
import BoardFormFoss from '~/boards/components/board_form.vue';
import createEpicBoardMutation from '../graphql/epic_board_create.mutation.graphql';
export default {
extends: BoardFormFoss,
computed: {
...mapState(['isEpicBoard']),
epicBoardCreateQuery() {
return createEpicBoardMutation;
},
mutationVariables() {
// TODO: Epic board scope will be added in a future iteration: https://gitlab.com/gitlab-org/gitlab/-/issues/231389
return {
...this.baseMutationVariables,
...(this.scopedIssueBoardFeatureEnabled && !this.isEpicBoard
? this.boardScopeMutationVariables
: {}),
};
},
},
methods: {
epicBoardCreateResponse(data) {
return data.epicBoardCreate.epicBoard.webPath;
},
async createOrUpdateBoard() {
const response = await this.$apollo.mutate({
mutation: this.isEpicBoard ? this.epicBoardCreateQuery : this.currentMutation,
variables: { input: this.mutationVariables },
});
if (!this.board.id) {
return this.isEpicBoard
? this.epicBoardCreateResponse(response.data)
: this.boardCreateResponse(response.data);
}
return this.boardUpdateResponse(response.data);
},
},
};
</script>
......@@ -11,6 +11,9 @@ export default {
extends: BoardsSelectorFoss,
computed: {
...mapState(['isEpicBoard']),
showCreate() {
return this.isEpicBoard || this.multipleIssueBoardsAvailable;
},
},
methods: {
epicBoardUpdate(data) {
......
mutation createEpicBoard($input: EpicBoardCreateInput!) {
epicBoardCreate(input: $input) {
epicBoard {
id
webPath
}
errors
}
}
......@@ -12,6 +12,8 @@ RSpec.describe 'epic boards', :js do
context 'multiple epic boards' do
before do
stub_licensed_features(epics: true)
group.add_maintainer(user)
sign_in(user)
visit_epic_boards_page
end
......@@ -40,6 +42,18 @@ RSpec.describe 'epic boards', :js do
expect(page).to have_content(epic_board2.name)
end
end
it 'creates new epic board without detailed configuration' do
in_boards_switcher_dropdown do
click_button 'Create new board'
end
fill_in 'board-new-name', with: 'This is a new board'
click_button 'Create board'
wait_for_requests
expect(page).to have_button('This is a new board')
end
end
def visit_epic_boards_page
......
import { GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import BoardForm from 'ee/boards/components/board_form.vue';
import createEpicBoardMutation from 'ee/boards/graphql/epic_board_create.mutation.graphql';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { formType } from '~/boards/constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
stripFinalUrlSegment: jest.requireActual('~/lib/utils/url_utility').stripFinalUrlSegment,
}));
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(Vuex);
const currentBoard = {
id: 1,
name: 'test',
labels: [],
milestone_id: undefined,
assignee: {},
assignee_id: undefined,
weight: null,
hide_backlog_list: false,
hide_closed_list: false,
};
const defaultProps = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard,
currentPage: '',
};
describe('BoardForm', () => {
let wrapper;
let mutate;
let location;
const findModal = () => wrapper.findComponent(GlModal);
const findModalActionPrimary = () => findModal().props('actionPrimary');
const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]');
const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]');
const findInput = () => wrapper.find('#board-new-name');
const createStore = () => {
return new Vuex.Store({
state: {
isEpicBoard: true,
},
});
};
const store = createStore();
const createComponent = (props) => {
wrapper = shallowMount(BoardForm, {
localVue,
propsData: { ...defaultProps, ...props },
provide: {
rootPath: 'root',
},
mocks: {
$apollo: {
mutate,
},
},
attachTo: document.body,
store,
});
};
beforeAll(() => {
location = window.location;
delete window.location;
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mutate = null;
window.location = location;
});
describe('when creating a new epic board', () => {
describe('on non-scoped-board', () => {
beforeEach(() => {
createComponent({ canAdminBoard: true, currentPage: formType.new });
});
it('clears the form', () => {
expect(findInput().element.value).toBe('');
});
it('shows a correct title about creating a board', () => {
expect(findModal().attributes('title')).toBe('Create new board');
});
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Create board');
expect(findModalActionPrimary().attributes[0].variant).toBe('success');
});
it('does not render delete confirmation message', () => {
expect(findDeleteConfirmation().exists()).toBe(false);
});
it('renders form wrapper', () => {
expect(findFormWrapper().exists()).toBe(true);
});
});
describe('when submitting a create event', () => {
const fillForm = () => {
findInput().value = 'Test name';
findInput().trigger('input');
findInput().trigger('keyup.enter', { metaKey: true });
};
beforeEach(() => {
mutate = jest.fn().mockResolvedValue({
data: {
epicBoardCreate: {
epicBoard: { id: 'gid://gitlab/Boards::EpicBoard/123', webPath: 'test-path' },
},
},
});
});
it('does not call API if board name is empty', async () => {
createComponent({ canAdminBoard: true, currentPage: formType.new });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
expect(mutate).not.toHaveBeenCalled();
});
it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
createComponent({ canAdminBoard: true, currentPage: formType.new });
fillForm();
await waitForPromises();
expect(mutate).toHaveBeenCalledWith({
mutation: createEpicBoardMutation,
variables: {
input: expect.objectContaining({
name: 'test',
}),
},
});
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('test-path');
});
it('shows an error flash if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.new });
fillForm();
await waitForPromises();
expect(mutate).toHaveBeenCalled();
await waitForPromises();
expect(visitUrl).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalled();
});
});
});
});
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