Commit 0000d668 authored by Simon Knox's avatar Simon Knox

Merge branch '341331-refactor-board-scope-to-graphql' into 'master'

Refactor fetching board scope to GraphQL [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!73604
parents bf668226 703f89df
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
import { GlModal, GlAlert } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex'; import { mapGetters, mapActions, mapState } from 'vuex';
import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants'; import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId, getZeroBasedIdFromGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility'; import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { fullLabelId, fullBoardId } from '../boards_util'; import { fullLabelId } from '../boards_util';
import { formType } from '../constants'; import { formType } from '../constants';
import createBoardMutation from '../graphql/board_create.mutation.graphql'; import createBoardMutation from '../graphql/board_create.mutation.graphql';
...@@ -18,11 +18,11 @@ const boardDefaults = { ...@@ -18,11 +18,11 @@ const boardDefaults = {
name: '', name: '',
labels: [], labels: [],
milestone: {}, milestone: {},
iteration_id: undefined, iteration: {},
assignee: {}, assignee: {},
weight: null, weight: null,
hide_backlog_list: false, hideBacklogList: false,
hide_closed_list: false, hideClosedList: false,
}; };
export default { export default {
...@@ -144,17 +144,16 @@ export default { ...@@ -144,17 +144,16 @@ export default {
return destroyBoardMutation; return destroyBoardMutation;
}, },
baseMutationVariables() { baseMutationVariables() {
const { board } = this; const {
const variables = { board: { name, hideBacklogList, hideClosedList, id },
name: board.name, } = this;
hideBacklogList: board.hide_backlog_list,
hideClosedList: board.hide_closed_list, const variables = { name, hideBacklogList, hideClosedList };
};
return board.id return id
? { ? {
...variables, ...variables,
id: fullBoardId(board.id), id,
} }
: { : {
...variables, ...variables,
...@@ -168,11 +167,13 @@ export default { ...@@ -168,11 +167,13 @@ export default {
assigneeId: this.board.assignee?.id assigneeId: this.board.assignee?.id
? convertToGraphQLId(TYPE_USER, this.board.assignee.id) ? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null, : null,
// Temporarily converting to milestone ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
milestoneId: this.board.milestone?.id milestoneId: this.board.milestone?.id
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id) ? convertToGraphQLId(TYPE_MILESTONE, getZeroBasedIdFromGraphQLId(this.board.milestone.id))
: null, : null,
iterationId: this.board.iteration_id // Temporarily converting to iteration ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id) iterationId: this.board.iteration?.id
? convertToGraphQLId(TYPE_ITERATION, getZeroBasedIdFromGraphQLId(this.board.iteration.id))
: null, : null,
}; };
}, },
...@@ -226,7 +227,7 @@ export default { ...@@ -226,7 +227,7 @@ export default {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: this.deleteMutation, mutation: this.deleteMutation,
variables: { variables: {
id: fullBoardId(this.board.id), id: this.board.id,
}, },
}); });
}, },
...@@ -262,7 +263,9 @@ export default { ...@@ -262,7 +263,9 @@ export default {
} }
}, },
setIteration(iterationId) { setIteration(iterationId) {
this.board.iteration_id = iterationId; this.$set(this.board, 'iteration', {
id: iterationId,
});
}, },
setBoardLabels(labels) { setBoardLabels(labels) {
this.board.labels = labels; this.board.labels = labels;
...@@ -329,8 +332,8 @@ export default { ...@@ -329,8 +332,8 @@ export default {
</div> </div>
<board-configuration-options <board-configuration-options
:hide-backlog-list.sync="board.hide_backlog_list" :hide-backlog-list.sync="board.hideBacklogList"
:hide-closed-list.sync="board.hide_closed_list" :hide-closed-list.sync="board.hideClosedList"
:readonly="readonly" :readonly="readonly"
/> />
......
...@@ -9,17 +9,20 @@ import { ...@@ -9,17 +9,20 @@ import {
GlModalDirective, GlModalDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue'; import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { s__ } from '~/locale';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import groupQuery from '../graphql/group_boards.query.graphql'; import groupBoardsQuery from '../graphql/group_boards.query.graphql';
import projectQuery from '../graphql/project_boards.query.graphql'; import projectBoardsQuery from '../graphql/project_boards.query.graphql';
import groupBoardQuery from '../graphql/group_board.query.graphql';
import projectBoardQuery from '../graphql/project_board.query.graphql';
const MIN_BOARDS_TO_VIEW_RECENT = 10; const MIN_BOARDS_TO_VIEW_RECENT = 10;
...@@ -39,10 +42,6 @@ export default { ...@@ -39,10 +42,6 @@ export default {
}, },
inject: ['fullPath', 'recentBoardsEndpoint'], inject: ['fullPath', 'recentBoardsEndpoint'],
props: { props: {
currentBoard: {
type: Object,
required: true,
},
throttleDuration: { throttleDuration: {
type: Number, type: Number,
default: 200, default: 200,
...@@ -86,14 +85,47 @@ export default { ...@@ -86,14 +85,47 @@ export default {
maxPosition: 0, maxPosition: 0,
filterTerm: '', filterTerm: '',
currentPage: '', currentPage: '',
board: {},
}; };
}, },
apollo: {
board: {
query() {
return this.currentBoardQuery;
},
variables() {
return {
fullPath: this.fullPath,
boardId: this.fullBoardId,
};
},
update(data) {
const board = data.workspace?.board;
return {
...board,
labels: board?.labels?.nodes,
};
},
error() {
this.setError({ message: this.$options.i18n.errorFetchingBoard });
},
},
},
computed: { computed: {
...mapState(['boardType']), ...mapState(['boardType', 'fullBoardId']),
...mapGetters(['isGroupBoard']), ...mapGetters(['isGroupBoard', 'isProjectBoard']),
parentType() { parentType() {
return this.boardType; return this.boardType;
}, },
currentBoardQueryCE() {
return this.isGroupBoard ? groupBoardQuery : projectBoardQuery;
},
currentBoardQuery() {
return this.currentBoardQueryCE;
},
isBoardLoading() {
return this.$apollo.queries.board.loading;
},
loading() { loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards); return this.loadingRecentBoards || Boolean(this.loadingBoards);
}, },
...@@ -102,9 +134,6 @@ export default { ...@@ -102,9 +134,6 @@ export default {
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
); );
}, },
board() {
return this.currentBoard;
},
showCreate() { showCreate() {
return this.multipleIssueBoardsAvailable; return this.multipleIssueBoardsAvailable;
}, },
...@@ -137,6 +166,7 @@ export default { ...@@ -137,6 +166,7 @@ export default {
eventHub.$off('showBoardModal', this.showPage); eventHub.$off('showBoardModal', this.showPage);
}, },
methods: { methods: {
...mapActions(['setError']),
showPage(page) { showPage(page) {
this.currentPage = page; this.currentPage = page;
}, },
...@@ -153,7 +183,7 @@ export default { ...@@ -153,7 +183,7 @@ export default {
})); }));
}, },
boardQuery() { boardQuery() {
return this.isGroupBoard ? groupQuery : projectQuery; return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
}, },
loadBoards(toggleDropdown = true) { loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) { if (toggleDropdown && this.boards.length > 0) {
...@@ -229,13 +259,18 @@ export default { ...@@ -229,13 +259,18 @@ export default {
this.hasScrollFade = this.isScrolledUp(); this.hasScrollFade = this.isScrolledUp();
}, },
}, },
i18n: {
errorFetchingBoard: s__('Board|An error occurred while fetching the board, please try again.'),
},
}; };
</script> </script>
<template> <template>
<div class="boards-switcher js-boards-selector gl-mr-3"> <div class="boards-switcher js-boards-selector gl-mr-3">
<span class="boards-selector-wrapper js-boards-selector-wrapper"> <span class="boards-selector-wrapper js-boards-selector-wrapper">
<gl-loading-icon v-if="isBoardLoading" size="md" class="gl-mt-2" />
<gl-dropdown <gl-dropdown
v-else
data-qa-selector="boards_dropdown" data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle js-dropdown-toggle" toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height" menu-class="flex-column dropdown-extended-height"
...@@ -336,7 +371,7 @@ export default { ...@@ -336,7 +371,7 @@ export default {
:can-admin-board="canAdminBoard" :can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights" :weights="weights"
:current-board="currentBoard" :current-board="board"
:current-page="currentPage" :current-page="currentPage"
@cancel="cancel" @cancel="cancel"
/> />
......
fragment BoardScopeFragment on Board {
id
name
hideBacklogList
hideClosedList
}
#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
query GroupBoard($fullPath: ID!, $boardId: ID!) {
workspace: group(fullPath: $fullPath) {
board(id: $boardId) {
...BoardScopeFragment
}
}
}
#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
query ProjectBoard($fullPath: ID!, $boardId: ID!) {
workspace: project(fullPath: $fullPath) {
board(id: $boardId) {
...BoardScopeFragment
}
}
}
...@@ -32,7 +32,6 @@ export default (params = {}) => { ...@@ -32,7 +32,6 @@ export default (params = {}) => {
data() { data() {
const boardsSelectorProps = { const boardsSelectorProps = {
...dataset, ...dataset,
currentBoard: JSON.parse(dataset.currentBoard),
hasMissingBoards: parseBoolean(dataset.hasMissingBoards), hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard), canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
......
...@@ -15,6 +15,8 @@ export const isGid = (id) => { ...@@ -15,6 +15,8 @@ export const isGid = (id) => {
return false; return false;
}; };
const parseGid = (gid) => parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10);
/** /**
* Ids generated by GraphQL endpoints are usually in the format * Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method extracts Id number * gid://gitlab/Environments/123. This method extracts Id number
...@@ -23,8 +25,12 @@ export const isGid = (id) => { ...@@ -23,8 +25,12 @@ export const isGid = (id) => {
* @param {String} gid GraphQL global ID * @param {String} gid GraphQL global ID
* @returns {Number} * @returns {Number}
*/ */
export const getIdFromGraphQLId = (gid = '') => export const getIdFromGraphQLId = (gid = '') => parseGid(gid) || null;
parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null;
export const getZeroBasedIdFromGraphQLId = (gid = '') => {
const parsedGid = parseGid(gid);
return Number.isInteger(parsedGid) ? parsedGid : null;
};
export const MutationOperationMode = { export const MutationOperationMode = {
Append: 'APPEND', Append: 'APPEND',
......
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
- milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board? - milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board?
- weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : [] - weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : []
#js-multiple-boards-switcher.inline.boards-switcher{ data: { current_board: current_board_json.to_json, #js-multiple-boards-switcher.inline.boards-switcher{ data: { milestone_path: milestones_filter_path(milestone_filter_opts),
milestone_path: milestones_filter_path(milestone_filter_opts),
board_base_url: board_base_url, board_base_url: board_base_url,
has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s, has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s,
can_admin_board: can?(current_user, :admin_issue_board, parent).to_s, can_admin_board: can?(current_user, :admin_issue_board, parent).to_s,
......
...@@ -3,9 +3,7 @@ ...@@ -3,9 +3,7 @@
// extends a valid Vue single file component. // extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */ /* eslint-disable @gitlab/no-runtime-template-compiler */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { fullBoardId } from '~/boards/boards_util';
import BoardFormFoss from '~/boards/components/board_form.vue'; import BoardFormFoss from '~/boards/components/board_form.vue';
import { fullEpicBoardId } from '../boards_util';
import createEpicBoardMutation from '../graphql/epic_board_create.mutation.graphql'; import createEpicBoardMutation from '../graphql/epic_board_create.mutation.graphql';
import destroyEpicBoardMutation from '../graphql/epic_board_destroy.mutation.graphql'; import destroyEpicBoardMutation from '../graphql/epic_board_destroy.mutation.graphql';
import updateEpicBoardMutation from '../graphql/epic_board_update.mutation.graphql'; import updateEpicBoardMutation from '../graphql/epic_board_update.mutation.graphql';
...@@ -18,11 +16,8 @@ export default { ...@@ -18,11 +16,8 @@ export default {
return this.board.id ? updateEpicBoardMutation : createEpicBoardMutation; return this.board.id ? updateEpicBoardMutation : createEpicBoardMutation;
}, },
mutationVariables() { mutationVariables() {
const { board } = this;
return { return {
...this.baseMutationVariables, ...this.baseMutationVariables,
...(board.id && this.isEpicBoard && { id: fullEpicBoardId(board.id) }),
...(this.scopedIssueBoardFeatureEnabled || this.isEpicBoard ...(this.scopedIssueBoardFeatureEnabled || this.isEpicBoard
? this.boardScopeMutationVariables ? this.boardScopeMutationVariables
: {}), : {}),
...@@ -56,7 +51,7 @@ export default { ...@@ -56,7 +51,7 @@ export default {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: this.isEpicBoard ? destroyEpicBoardMutation : this.deleteMutation, mutation: this.isEpicBoard ? destroyEpicBoardMutation : this.deleteMutation,
variables: { variables: {
id: this.isEpicBoard ? fullEpicBoardId(this.board.id) : fullBoardId(this.board.id), id: this.board.id,
}, },
}); });
}, },
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import AssigneeSelect from './assignee_select.vue'; import AssigneeSelect from './assignee_select.vue';
import BoardScopeCurrentIteration from './board_scope_current_iteration.vue'; import BoardScopeCurrentIteration from './board_scope_current_iteration.vue';
import BoardLabelsSelect from './labels_select.vue'; import BoardLabelsSelect from './labels_select.vue';
...@@ -51,6 +52,9 @@ export default { ...@@ -51,6 +52,9 @@ export default {
? __('Board scope affects which issues are displayed for anyone who visits this board') ? __('Board scope affects which issues are displayed for anyone who visits this board')
: __('Board scope affects which epics are displayed for anyone who visits this board'); : __('Board scope affects which epics are displayed for anyone who visits this board');
}, },
iterationId() {
return getIdFromGraphQLId(this.board.iteration?.id) || null;
},
}, },
methods: { methods: {
...@@ -87,7 +91,7 @@ export default { ...@@ -87,7 +91,7 @@ export default {
<board-scope-current-iteration <board-scope-current-iteration
v-if="isIssueBoard" v-if="isIssueBoard"
:can-admin-board="canAdminBoard" :can-admin-board="canAdminBoard"
:iteration-id="board.iteration_id" :iteration-id="iterationId"
@set-iteration="$emit('set-iteration', $event)" @set-iteration="$emit('set-iteration', $event)"
/> />
......
...@@ -7,6 +7,7 @@ import BoardsSelectorFoss from '~/boards/components/boards_selector.vue'; ...@@ -7,6 +7,7 @@ import BoardsSelectorFoss from '~/boards/components/boards_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import epicBoardsQuery from '../graphql/epic_boards.query.graphql'; import epicBoardsQuery from '../graphql/epic_boards.query.graphql';
import epicBoardQuery from '../graphql/epic_board.query.graphql';
export default { export default {
extends: BoardsSelectorFoss, extends: BoardsSelectorFoss,
...@@ -19,6 +20,9 @@ export default { ...@@ -19,6 +20,9 @@ export default {
showDelete() { showDelete() {
return this.boards.length > 1; return this.boards.length > 1;
}, },
currentBoardQuery() {
return this.isEpicBoard ? epicBoardQuery : this.currentBoardQueryCE;
},
}, },
methods: { methods: {
epicBoardUpdate(data) { epicBoardUpdate(data) {
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
import searchGroupLabels from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql'; import searchGroupLabels from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
import searchProjectLabels from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql'; import searchProjectLabels from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
...@@ -60,10 +59,7 @@ export default { ...@@ -60,10 +59,7 @@ export default {
return !this.isEditing; return !this.isEditing;
}, },
update(data) { update(data) {
return data.workspace?.labels?.nodes.map((label) => ({ return data.workspace?.labels?.nodes;
...label,
id: getIdFromGraphQLId(label.id),
}));
}, },
error() { error() {
this.setError({ message: this.$options.i18n.errorSearchingLabels }); this.setError({ message: this.$options.i18n.errorSearchingLabels });
...@@ -123,7 +119,7 @@ export default { ...@@ -123,7 +119,7 @@ export default {
this.$emit('set-labels', labels); this.$emit('set-labels', labels);
}, },
onLabelRemove(labelId) { onLabelRemove(labelId) {
const labels = this.selected.filter(({ id }) => getIdFromGraphQLId(id) !== labelId); const labels = this.selected.filter(({ id }) => id !== labelId);
this.selected = labels; this.selected = labels;
this.$emit('set-labels', labels); this.$emit('set-labels', labels);
}, },
......
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/iteration.fragment.graphql"
fragment BoardScopeFragment on Board {
id
name
hideBacklogList
hideClosedList
assignee {
...User
}
milestone {
id
title
}
labels {
nodes {
...Label
}
}
iteration {
...Iteration
}
weight
}
#import "~/graphql_shared/fragments/label.fragment.graphql"
query EpicBoard($fullPath: ID!, $boardId: BoardsEpicBoardID!) {
workspace: group(fullPath: $fullPath) {
board: epicBoard(id: $boardId) {
id
name
hideBacklogList
hideClosedList
labels {
nodes {
...Label
}
}
}
}
}
...@@ -225,6 +225,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -225,6 +225,7 @@ RSpec.describe 'Scoped issue boards', :js do
it 'prefills fields' do it 'prefills fields' do
visit project_boards_path(project_2) visit project_boards_path(project_2)
wait_for_all_requests
edit_board.click edit_board.click
...@@ -332,6 +333,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -332,6 +333,7 @@ RSpec.describe 'Scoped issue boards', :js do
it 'adds label to board' do it 'adds label to board' do
label_title = issue.labels.first.title label_title = issue.labels.first.title
visit project_boards_path(project) visit project_boards_path(project)
wait_for_all_requests
update_board_label(label_title) update_board_label(label_title)
...@@ -346,6 +348,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -346,6 +348,7 @@ RSpec.describe 'Scoped issue boards', :js do
label_2_title = issue_2.labels.first.title label_2_title = issue_2.labels.first.title
visit project_boards_path(project) visit project_boards_path(project)
wait_for_all_requests
update_board_label(label_title) update_board_label(label_title)
...@@ -365,6 +368,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -365,6 +368,7 @@ RSpec.describe 'Scoped issue boards', :js do
label_2_title = issue_2.labels.first.title label_2_title = issue_2.labels.first.title
visit project_boards_path(project) visit project_boards_path(project)
wait_for_all_requests
update_board_label(label_title) update_board_label(label_title)
...@@ -378,6 +382,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -378,6 +382,7 @@ RSpec.describe 'Scoped issue boards', :js do
stub_licensed_features(multiple_group_issue_boards: true) stub_licensed_features(multiple_group_issue_boards: true)
visit group_boards_path(group) visit group_boards_path(group)
wait_for_all_requests
edit_board.click edit_board.click
page.within('.labels') do page.within('.labels') do
......
...@@ -23,15 +23,19 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -23,15 +23,19 @@ jest.mock('~/lib/utils/url_utility', () => ({
Vue.use(Vuex); Vue.use(Vuex);
const currentBoard = { const currentBoard = {
id: 1, id: 'gid://gitlab/Board/1',
name: 'test', name: 'test',
labels: [], labels: [],
milestone_id: undefined, milestone: {},
assignee: {}, assignee: {},
assignee_id: undefined,
weight: null, weight: null,
hide_backlog_list: false, hideBacklogList: false,
hide_closed_list: false, hideClosedList: false,
};
const currentEpicBoard = {
...currentBoard,
id: 'gid://gitlab/Boards::EpicBoard/321',
}; };
const defaultProps = { const defaultProps = {
...@@ -206,7 +210,9 @@ describe('BoardForm', () => { ...@@ -206,7 +210,9 @@ describe('BoardForm', () => {
milestone: { milestone: {
id: 2, id: 2,
}, },
iteration_id: 3, iteration: {
id: 'gid://gitlab/Iteration/3',
},
}, },
canAdminBoard: true, canAdminBoard: true,
currentPage: formType.edit, currentPage: formType.edit,
...@@ -221,7 +227,7 @@ describe('BoardForm', () => { ...@@ -221,7 +227,7 @@ describe('BoardForm', () => {
mutation: updateBoardMutation, mutation: updateBoardMutation,
variables: { variables: {
input: expect.objectContaining({ input: expect.objectContaining({
id: `gid://gitlab/Board/${currentBoard.id}`, id: currentBoard.id,
assigneeId: 'gid://gitlab/User/1', assigneeId: 'gid://gitlab/User/1',
milestoneId: 'gid://gitlab/Milestone/2', milestoneId: 'gid://gitlab/Milestone/2',
iterationId: 'gid://gitlab/Iteration/3', iterationId: 'gid://gitlab/Iteration/3',
...@@ -240,11 +246,15 @@ describe('BoardForm', () => { ...@@ -240,11 +246,15 @@ describe('BoardForm', () => {
mutate = jest.fn().mockResolvedValue({ mutate = jest.fn().mockResolvedValue({
data: { data: {
epicBoardUpdate: { epicBoardUpdate: {
epicBoard: { id: 'gid://gitlab/Boards::EpicBoard/321', webPath: 'test-path' }, epicBoard: { id: currentEpicBoard.id, webPath: 'test-path' },
}, },
}, },
}); });
createComponent({ canAdminBoard: true, currentPage: formType.edit }); createComponent({
canAdminBoard: true,
currentPage: formType.edit,
currentBoard: currentEpicBoard,
});
findInput().trigger('keyup.enter', { metaKey: true }); findInput().trigger('keyup.enter', { metaKey: true });
...@@ -254,7 +264,7 @@ describe('BoardForm', () => { ...@@ -254,7 +264,7 @@ describe('BoardForm', () => {
mutation: updateEpicBoardMutation, mutation: updateEpicBoardMutation,
variables: { variables: {
input: expect.objectContaining({ input: expect.objectContaining({
id: `gid://gitlab/Boards::EpicBoard/${currentBoard.id}`, id: currentEpicBoard.id,
}), }),
}, },
}); });
...@@ -265,7 +275,11 @@ describe('BoardForm', () => { ...@@ -265,7 +275,11 @@ describe('BoardForm', () => {
it('shows a GlAlert if GraphQL mutation fails', async () => { it('shows a GlAlert if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem'); mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.edit }); createComponent({
canAdminBoard: true,
currentPage: formType.edit,
currentBoard: currentEpicBoard,
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {}); jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findInput().trigger('keyup.enter', { metaKey: true }); findInput().trigger('keyup.enter', { metaKey: true });
...@@ -285,19 +299,31 @@ describe('BoardForm', () => { ...@@ -285,19 +299,31 @@ describe('BoardForm', () => {
}); });
it('passes correct primary action text and variant', () => { it('passes correct primary action text and variant', () => {
createComponent({ canAdminBoard: true, currentPage: formType.delete }); createComponent({
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
});
expect(findModalActionPrimary().text).toBe('Delete'); expect(findModalActionPrimary().text).toBe('Delete');
expect(findModalActionPrimary().attributes[0].variant).toBe('danger'); expect(findModalActionPrimary().attributes[0].variant).toBe('danger');
}); });
it('renders delete confirmation message', () => { it('renders delete confirmation message', () => {
createComponent({ canAdminBoard: true, currentPage: formType.delete }); createComponent({
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
});
expect(findDeleteConfirmation().exists()).toBe(true); expect(findDeleteConfirmation().exists()).toBe(true);
}); });
it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => { it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => {
mutate = jest.fn().mockResolvedValue({}); mutate = jest.fn().mockResolvedValue({});
createComponent({ canAdminBoard: true, currentPage: formType.delete }); createComponent({
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
});
findModal().vm.$emit('primary'); findModal().vm.$emit('primary');
await waitForPromises(); await waitForPromises();
...@@ -305,7 +331,7 @@ describe('BoardForm', () => { ...@@ -305,7 +331,7 @@ describe('BoardForm', () => {
expect(mutate).toHaveBeenCalledWith({ expect(mutate).toHaveBeenCalledWith({
mutation: destroyEpicBoardMutation, mutation: destroyEpicBoardMutation,
variables: { variables: {
id: 'gid://gitlab/Boards::EpicBoard/1', id: currentEpicBoard.id,
}, },
}); });
...@@ -315,7 +341,11 @@ describe('BoardForm', () => { ...@@ -315,7 +341,11 @@ describe('BoardForm', () => {
it('shows a GlAlert if GraphQL mutation fails', async () => { it('shows a GlAlert if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem'); mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.delete }); createComponent({
canAdminBoard: true,
currentPage: formType.delete,
currentBoard: currentEpicBoard,
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {}); jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findModal().vm.$emit('primary'); findModal().vm.$emit('primary');
......
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui'; import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex'; import Vuex from 'vuex';
import BoardsSelector from 'ee/boards/components/boards_selector.vue'; import BoardsSelector from 'ee/boards/components/boards_selector.vue';
import { BoardType } from '~/boards/constants';
import epicBoardQuery from 'ee/boards/graphql/epic_board.query.graphql';
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
import defaultStore from '~/boards/stores';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockGroupBoardResponse, mockProjectBoardResponse } from 'jest/boards/mock_data';
import { mockEpicBoardResponse } from '../mock_data';
const throttleDuration = 1; const throttleDuration = 1;
const localVue = createLocalVue(); Vue.use(VueApollo);
localVue.use(Vuex);
function boardGenerator(n) { function boardGenerator(n) {
return new Array(n).fill().map((board, index) => { return new Array(n).fill().map((board, index) => {
...@@ -29,13 +37,28 @@ describe('BoardsSelector', () => { ...@@ -29,13 +37,28 @@ describe('BoardsSelector', () => {
let allBoardsResponse; let allBoardsResponse;
let recentBoardsResponse; let recentBoardsResponse;
let mock; let mock;
let fakeApollo;
let store;
const boards = boardGenerator(20); const boards = boardGenerator(20);
const recentBoards = boardGenerator(5); const recentBoards = boardGenerator(5);
const createStore = () => { const createStore = ({
return new Vuex.Store({ isGroupBoard = false,
isProjectBoard = false,
isEpicBoard = false,
} = {}) => {
store = new Vuex.Store({
...defaultStore,
actions: {
setError: jest.fn(),
},
getters: { getters: {
isEpicBoard: () => false, isEpicBoard: () => isEpicBoard,
isGroupBoard: () => isGroupBoard,
isProjectBoard: () => isProjectBoard,
},
state: {
boardType: isGroupBoard ? BoardType.group : BoardType.project,
}, },
}); });
}; };
...@@ -45,43 +68,22 @@ describe('BoardsSelector', () => { ...@@ -45,43 +68,22 @@ describe('BoardsSelector', () => {
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
beforeEach(() => { const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
mock = new MockAdapter(axios); const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
const $apollo = { const epicBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockEpicBoardResponse);
queries: {
boards: {
loading: false,
},
},
};
allBoardsResponse = Promise.resolve({
data: {
group: {
boards: {
edges: boards.map((board) => ({ node: board })),
},
},
},
});
recentBoardsResponse = Promise.resolve({
data: recentBoards,
});
const store = createStore(); const createComponent = () => {
fakeApollo = createMockApollo([
[projectBoardQuery, projectBoardQueryHandlerSuccess],
[groupBoardQuery, groupBoardQueryHandlerSuccess],
[epicBoardQuery, epicBoardQueryHandlerSuccess],
]);
wrapper = mount(BoardsSelector, { wrapper = mount(BoardsSelector, {
localVue, store,
apolloProvider: fakeApollo,
propsData: { propsData: {
throttleDuration, throttleDuration,
currentBoard: {
id: 1,
name: 'Development',
milestone_id: null,
weight: null,
assignee_id: null,
labels: [],
},
boardBaseUrl: `${TEST_HOST}/board/base/url`, boardBaseUrl: `${TEST_HOST}/board/base/url`,
hasMissingBoards: false, hasMissingBoards: false,
canAdminBoard: true, canAdminBoard: true,
...@@ -89,13 +91,11 @@ describe('BoardsSelector', () => { ...@@ -89,13 +91,11 @@ describe('BoardsSelector', () => {
scopedIssueBoardFeatureEnabled: true, scopedIssueBoardFeatureEnabled: true,
weights: [], weights: [],
}, },
mocks: { $apollo },
attachTo: document.body, attachTo: document.body,
provide: { provide: {
fullPath: '', fullPath: '',
recentBoardsEndpoint: `${TEST_HOST}/recent`, recentBoardsEndpoint: `${TEST_HOST}/recent`,
}, },
store,
}); });
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => { wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
...@@ -103,48 +103,103 @@ describe('BoardsSelector', () => { ...@@ -103,48 +103,103 @@ describe('BoardsSelector', () => {
[options.loadingKey]: true, [options.loadingKey]: true,
}); });
}); });
};
mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
fakeApollo = null;
store = null;
mock.restore(); mock.restore();
}); });
describe('loading', () => { describe('fetching all board', () => {
// we are testing loading state, so don't resolve responses until after the tests beforeEach(() => {
afterEach(async () => { mock = new MockAdapter(axios);
await Promise.all([allBoardsResponse, recentBoardsResponse]);
return nextTick();
});
it('shows loading spinner', () => { allBoardsResponse = Promise.resolve({
expect(getDropdownHeaders()).toHaveLength(0); data: {
expect(getDropdownItems()).toHaveLength(0); group: {
expect(getLoadingIcon().exists()).toBe(true); boards: {
edges: boards.map((board) => ({ node: board })),
},
},
},
});
recentBoardsResponse = Promise.resolve({
data: recentBoards,
});
createStore();
createComponent();
mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
}); });
});
describe('loaded', () => { describe('loading', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.setData({ // Wait for current board to be loaded
loadingBoards: false, await nextTick();
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
});
// we are testing loading state, so don't resolve responses until after the tests
afterEach(async () => {
await Promise.all([allBoardsResponse, recentBoardsResponse]);
await nextTick();
});
it('shows loading spinner', () => {
expect(getDropdownHeaders()).toHaveLength(0);
expect(getDropdownItems()).toHaveLength(0);
expect(getLoadingIcon().exists()).toBe(true);
}); });
// NOTE: Due to timing issues, this `return` of `Promise.all` is required because
// `app/assets/javascripts/boards/components/boards_selector.vue` does a `$nextTick()` in
// loadRecentBoards. For some unknown reason it doesn't work with `await`, the `return`
// is required.
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
}); });
it('hides loading spinner', async () => { describe('loaded', () => {
await wrapper.vm.$nextTick(); beforeEach(async () => {
expect(getLoadingIcon().exists()).toBe(false); // Wait for current board to be loaded
await nextTick();
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
await wrapper.setData({
loadingBoards: false,
loadingRecentBoards: false,
});
});
it('hides loading spinner', async () => {
await nextTick();
expect(getLoadingIcon().exists()).toBe(false);
});
}); });
}); });
describe('fetching current board', () => {
it.each`
boardType | isEpicBoard | queryHandler | notCalledHandler
${BoardType.group} | ${false} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
${BoardType.project} | ${false} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
${BoardType.group} | ${true} | ${epicBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`(
'fetches $boardType board when isEpicBoard is $isEpicBoard',
async ({ boardType, isEpicBoard, queryHandler, notCalledHandler }) => {
createStore({
isProjectBoard: boardType === BoardType.project,
isGroupBoard: boardType === BoardType.group,
isEpicBoard,
});
createComponent();
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
},
);
});
}); });
...@@ -7,6 +7,18 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label ...@@ -7,6 +7,18 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
export const mockEpicBoardResponse = {
data: {
workspace: {
epicBoard: {
id: 'gid://gitlab/Boards::EpicBoard/1',
name: 'Development',
},
__typename: 'Group',
},
},
};
export const mockLabel = { export const mockLabel = {
id: 'gid://gitlab/GroupLabel/121', id: 'gid://gitlab/GroupLabel/121',
title: 'To Do', title: 'To Do',
......
...@@ -5638,6 +5638,9 @@ msgstr "" ...@@ -5638,6 +5638,9 @@ msgstr ""
msgid "Boards|View scope" msgid "Boards|View scope"
msgstr "" msgstr ""
msgid "Board|An error occurred while fetching the board, please try again."
msgstr ""
msgid "Board|Are you sure you want to delete this board?" msgid "Board|Are you sure you want to delete this board?"
msgstr "" msgstr ""
......
...@@ -17,15 +17,14 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -17,15 +17,14 @@ jest.mock('~/lib/utils/url_utility', () => ({
})); }));
const currentBoard = { const currentBoard = {
id: 1, id: 'gid://gitlab/Board/1',
name: 'test', name: 'test',
labels: [], labels: [],
milestone_id: undefined, milestone: {},
assignee: {}, assignee: {},
assignee_id: undefined,
weight: null, weight: null,
hide_backlog_list: false, hideBacklogList: false,
hide_closed_list: false, hideClosedList: false,
}; };
const defaultProps = { const defaultProps = {
...@@ -249,7 +248,7 @@ describe('BoardForm', () => { ...@@ -249,7 +248,7 @@ describe('BoardForm', () => {
mutation: updateBoardMutation, mutation: updateBoardMutation,
variables: { variables: {
input: expect.objectContaining({ input: expect.objectContaining({
id: `gid://gitlab/Board/${currentBoard.id}`, id: currentBoard.id,
}), }),
}, },
}); });
...@@ -275,7 +274,7 @@ describe('BoardForm', () => { ...@@ -275,7 +274,7 @@ describe('BoardForm', () => {
mutation: updateBoardMutation, mutation: updateBoardMutation,
variables: { variables: {
input: expect.objectContaining({ input: expect.objectContaining({
id: `gid://gitlab/Board/${currentBoard.id}`, id: currentBoard.id,
}), }),
}, },
}); });
...@@ -323,7 +322,7 @@ describe('BoardForm', () => { ...@@ -323,7 +322,7 @@ describe('BoardForm', () => {
expect(mutate).toHaveBeenCalledWith({ expect(mutate).toHaveBeenCalledWith({
mutation: destroyBoardMutation, mutation: destroyBoardMutation,
variables: { variables: {
id: 'gid://gitlab/Board/1', id: currentBoard.id,
}, },
}); });
......
...@@ -30,17 +30,27 @@ export const listObj = { ...@@ -30,17 +30,27 @@ export const listObj = {
}, },
}; };
export const listObjDuplicate = { export const mockGroupBoardResponse = {
id: listObj.id, data: {
position: 1, workspace: {
title: 'Test', board: {
list_type: 'label', id: 'gid://gitlab/Board/1',
weight: 3, name: 'Development',
label: { },
id: listObj.label.id, __typename: 'Group',
title: 'Test', },
color: '#ff0000', },
description: 'testing;', };
export const mockProjectBoardResponse = {
data: {
workspace: {
board: {
id: 'gid://gitlab/Board/2',
name: 'Development',
},
__typename: 'Project',
},
}, },
}; };
...@@ -634,8 +644,8 @@ export const mockProjectLabelsResponse = { ...@@ -634,8 +644,8 @@ export const mockProjectLabelsResponse = {
labels: { labels: {
nodes: [mockLabel1, mockLabel2], nodes: [mockLabel1, mockLabel2],
}, },
__typename: 'Project',
}, },
__typename: 'Project',
}, },
}; };
...@@ -646,7 +656,7 @@ export const mockGroupLabelsResponse = { ...@@ -646,7 +656,7 @@ export const mockGroupLabelsResponse = {
labels: { labels: {
nodes: [mockLabel1, mockLabel2], nodes: [mockLabel1, mockLabel2],
}, },
__typename: 'Group',
}, },
__typename: 'Group',
}, },
}; };
import { import {
isGid, isGid,
getIdFromGraphQLId, getIdFromGraphQLId,
getZeroBasedIdFromGraphQLId,
convertToGraphQLId, convertToGraphQLId,
convertToGraphQLIds, convertToGraphQLIds,
convertFromGraphQLIds, convertFromGraphQLIds,
...@@ -51,6 +52,10 @@ describe('getIdFromGraphQLId', () => { ...@@ -51,6 +52,10 @@ describe('getIdFromGraphQLId', () => {
input: 'gid://gitlab/Environments/', input: 'gid://gitlab/Environments/',
output: null, output: null,
}, },
{
input: 'gid://gitlab/Environments/0',
output: null,
},
{ {
input: 'gid://gitlab/Environments/123', input: 'gid://gitlab/Environments/123',
output: 123, output: 123,
...@@ -66,6 +71,55 @@ describe('getIdFromGraphQLId', () => { ...@@ -66,6 +71,55 @@ describe('getIdFromGraphQLId', () => {
}); });
}); });
describe('getZeroBasedIdFromGraphQLId', () => {
[
{
input: '',
output: null,
},
{
input: null,
output: null,
},
{
input: 2,
output: 2,
},
{
input: 'gid://',
output: null,
},
{
input: 'gid://gitlab/',
output: null,
},
{
input: 'gid://gitlab/Environments',
output: null,
},
{
input: 'gid://gitlab/Environments/',
output: null,
},
{
input: 'gid://gitlab/Environments/0',
output: 0,
},
{
input: 'gid://gitlab/Environments/123',
output: 123,
},
{
input: 'gid://gitlab/DesignManagement::Version/2',
output: 2,
},
].forEach(({ input, output }) => {
it(`getZeroBasedIdFromGraphQLId returns ${output} when passed ${input}`, () => {
expect(getZeroBasedIdFromGraphQLId(input)).toBe(output);
});
});
});
describe('convertToGraphQLId', () => { describe('convertToGraphQLId', () => {
it('combines $type and $id into $result', () => { it('combines $type and $id into $result', () => {
expect(convertToGraphQLId(mockType, mockId)).toBe(mockGid); expect(convertToGraphQLId(mockType, mockId)).toBe(mockGid);
......
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