Commit 39474ce8 authored by Florie Guibert's avatar Florie Guibert

Board refactor - Move fetch lists GraphQL to VueX

- VueX action to fetch board lists
- VueX action to add blank list
- VueX action to show promotion list
- VueX action to add backlog list in GraphQL
parent c19c206e
......@@ -22,6 +22,10 @@ export function formatListIssues(listIssues) {
}, {});
}
export function fullBoardId(boardId) {
return `gid://gitlab/Board/${boardId}`;
}
export default {
getMilestone,
formatListIssues,
......
<script>
import { mapState } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
......@@ -47,6 +47,18 @@ export default {
isSwimlanesOn() {
return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes;
},
boardListsToUse() {
return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists;
},
},
mounted() {
if (this.glFeatures.graphqlBoardLists) {
this.fetchLists();
this.showPromotionList();
}
},
methods: {
...mapActions(['fetchLists', 'showPromotionList']),
},
};
</script>
......@@ -62,7 +74,7 @@ export default {
data-qa-selector="boards_list"
>
<board-column
v-for="list in lists"
v-for="list in boardListsToUse"
:key="list.id"
ref="board"
:can-admin-list="canAdminList"
......@@ -77,7 +89,7 @@ export default {
<epics-swimlanes
v-else
ref="swimlanes"
:lists="boardLists"
:lists="boardListsToUse"
:can-admin-list="canAdminList"
:disabled="disabled"
:board-id="boardId"
......
......@@ -6,6 +6,7 @@ import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { inactiveId } from '~/boards/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
......@@ -23,13 +24,17 @@ export default {
BoardSettingsListTypes: () =>
import('ee_component/boards/components/board_settings_list_types.vue'),
},
mixins: [glFeatureFlagMixin()],
computed: {
...mapState(['activeId']),
...mapState(['activeId', 'boardLists']),
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects
*/
if (this.glFeatures.graphqlBoardLists) {
return this.boardLists.find(({ id }) => id === this.activeId);
}
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
},
isSidebarOpen() {
......
......@@ -24,7 +24,6 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
import './models/label';
import './models/assignee';
import { BoardType } from './constants';
import toggleFocusMode from '~/boards/toggle_focus';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
......@@ -44,10 +43,7 @@ import {
parseBoolean,
urlParamsToObject,
} from '~/lib/utils/common_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
import projectBoardQuery from './queries/project_board.query.graphql';
import groupQuery from './queries/group_board.query.graphql';
Vue.use(VueApollo);
......@@ -119,7 +115,12 @@ export default () => {
boardId: this.boardId,
fullPath: $boardApp.dataset.fullPath,
};
this.setInitialBoardData({ ...endpoints, boardType: this.parent });
this.setInitialBoardData({
...endpoints,
boardType: this.parent,
disabled: this.disabled,
showPromotion: parseBoolean($boardApp.getAttribute('data-show-promotion')),
});
boardsStore.setEndpoints(endpoints);
boardsStore.rootPath = this.boardsEndpoint;
......@@ -144,42 +145,7 @@ export default () => {
boardsStore.disabled = this.disabled;
if (gon.features.graphqlBoardLists) {
this.$apollo.addSmartQuery('lists', {
query() {
return this.parent === BoardType.group ? groupQuery : projectBoardQuery;
},
variables() {
return {
fullPath: this.state.endpoints.fullPath,
boardId: `gid://gitlab/Board/${this.boardId}`,
};
},
update(data) {
return this.getNodes(data);
},
result({ data, error }) {
if (error) {
throw error;
}
const lists = this.getNodes(data);
lists.forEach(list =>
boardsStore.addList({
...list,
id: getIdFromGraphQLId(list.id),
}),
);
boardsStore.addBlankState();
setPromotionState(boardsStore);
},
error() {
Flash(__('An error occurred while fetching the board lists. Please try again.'));
},
});
} else {
if (!gon.features.graphqlBoardLists) {
boardsStore
.all()
.then(res => res.data)
......
#import "./board_list.fragment.graphql"
mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean) {
boardListCreate(input: { boardId: $boardId, backlog: $backlog }) {
list {
...BoardListFragment
}
errors
}
}
......@@ -4,7 +4,6 @@ fragment BoardListShared on BoardList {
position
listType
collapsed
maxIssueCount
label {
id
title
......
import * as types from './mutation_types';
import Cookies from 'js-cookie';
import { sortBy } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import { BoardType } from '~/boards/constants';
import { formatListIssues } from '../boards_util';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { BoardType, ListType } from '~/boards/constants';
import * as types from './mutation_types';
import { formatListIssues, fullBoardId } from '../boards_util';
import boardStore from '~/boards/stores/boards_store';
import groupListsIssuesQuery from '../queries/group_lists_issues.query.graphql';
import projectListsIssuesQuery from '../queries/project_lists_issues.query.graphql';
const gqlClient = createDefaultClient();
import projectBoardQuery from '../queries/project_board.query.graphql';
import groupBoardQuery from '../queries/group_board.query.graphql';
import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
throw new Error('Not implemented!');
};
export const gqlClient = createDefaultClient();
export default {
setInitialBoardData: ({ commit }, data) => {
commit(types.SET_INITIAL_BOARD_DATA, data);
......@@ -26,15 +37,109 @@ export default {
commit(types.SET_FILTERS, filterParams);
},
fetchLists: () => {
notImplemented();
fetchLists: ({ commit, state, dispatch }) => {
const { endpoints, boardType } = state;
const { fullPath, boardId } = endpoints;
let query;
if (boardType === BoardType.group) {
query = groupBoardQuery;
} else if (boardType === BoardType.project) {
query = projectBoardQuery;
} else {
createFlash(__('Invalid board'));
return Promise.reject();
}
const variables = {
fullPath,
boardId: fullBoardId(boardId),
};
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
let { lists } = data[boardType]?.board;
// Temporarily using positioning logic from boardStore
lists = lists.nodes.map(list =>
boardStore.updateListPosition({
...list,
id: getIdFromGraphQLId(list.id),
}),
);
commit(types.RECEIVE_LISTS, sortBy(lists, 'position'));
// Backlog list needs to be created if it doesn't exist
if (!lists.find(l => l.type === ListType.backlog)) {
dispatch('createList', { backlog: true });
}
dispatch('showWelcomeList');
})
.catch(() => {
createFlash(
__('An error occurred while fetching the board lists. Please reload the page.'),
);
});
},
// This action only supports backlog list creation at this stage
// Future iterations will add the ability to create other list types
createList: ({ state, commit, dispatch }, { backlog = false }) => {
const { boardId } = state.endpoints;
gqlClient
.mutate({
mutation: createBoardListMutation,
variables: {
boardId: fullBoardId(boardId),
backlog,
},
})
.then(({ data }) => {
if (data?.boardListCreate?.errors.length) {
commit(types.CREATE_LIST_FAILURE);
} else {
const list = data.boardListCreate?.list;
dispatch('addList', { ...list, id: getIdFromGraphQLId(list.id) });
}
})
.catch(() => {
commit(types.CREATE_LIST_FAILURE);
});
},
generateDefaultLists: () => {
notImplemented();
addList: ({ state, commit }, list) => {
const lists = state.boardLists;
// Temporarily using positioning logic from boardStore
lists.push(boardStore.updateListPosition(list));
commit(types.RECEIVE_LISTS, sortBy(lists, 'position'));
},
createList: () => {
showWelcomeList: ({ state, dispatch }) => {
if (state.disabled) {
return;
}
if (
state.boardLists.find(list => list.type !== ListType.backlog && list.type !== ListType.closed)
) {
return;
}
if (parseBoolean(Cookies.get('issue_board_welcome_hidden'))) {
return;
}
dispatch('addList', {
id: 'blank',
listType: ListType.blank,
title: __('Welcome to your issue board!'),
position: 0,
});
},
showPromotionList: () => {},
generateDefaultLists: () => {
notImplemented();
},
......@@ -60,7 +165,7 @@ export default {
const variables = {
fullPath,
boardId: `gid://gitlab/Board/${boardId}`,
boardId: fullBoardId(boardId),
};
return gqlClient
......
export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
export const RECEIVE_LISTS = 'RECEIVE_LISTS';
export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
......
......@@ -7,10 +7,16 @@ const notImplemented = () => {
};
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA]: (state, data) => {
const { boardType, ...endpoints } = data;
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, showPromotion, ...endpoints } = data;
state.endpoints = endpoints;
state.boardType = boardType;
state.disabled = disabled;
state.showPromotion = showPromotion;
},
[mutationTypes.RECEIVE_LISTS]: (state, lists) => {
state.boardLists = lists;
},
[mutationTypes.SET_ACTIVE_ID](state, id) {
......@@ -21,6 +27,10 @@ export default {
state.filterParams = filterParams;
},
[mutationTypes.CREATE_LIST_FAILURE]: state => {
state.error = __('An error occurred while creating the list. Please try again.');
},
[mutationTypes.REQUEST_ADD_LIST]: () => {
notImplemented();
},
......
......@@ -3,6 +3,8 @@ import { inactiveId } from '~/boards/constants';
export default () => ({
endpoints: {},
boardType: null,
disabled: false,
showPromotion: false,
isShowingLabels: true,
activeId: inactiveId,
boardLists: [],
......
......@@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
push_frontend_feature_flag(:boards_with_swimlanes, group, default_enabled: false)
end
......
import { sortBy } from 'lodash';
import Cookies from 'js-cookie';
import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store';
import { __ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
import actionsCE from '~/boards/stores/actions';
import { BoardType, ListType } from '~/boards/constants';
import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types';
import createDefaultClient from '~/lib/graphql';
import { BoardType } from '~/boards/constants';
import groupEpicsSwimlanesQuery from '../queries/group_epics_swimlanes.query.graphql';
const notImplemented = () => {
......@@ -73,6 +76,22 @@ export default {
});
},
showPromotionList: ({ state, dispatch }) => {
if (
!state.showPromotion ||
parseBoolean(Cookies.get('promotion_issue_board_hidden')) ||
state.disabled
) {
return;
}
dispatch('addList', {
id: 'promotion',
listType: ListType.promotion,
title: __('Improve Issue Boards'),
position: 0,
});
},
fetchAllBoards: () => {
notImplemented();
},
......
......@@ -57,6 +57,7 @@ RSpec.describe 'Multiple Issue Boards', :js do
before do
stub_licensed_features(multiple_group_issue_boards: true)
stub_feature_flags(graphql_board_lists: false)
end
it_behaves_like 'multiple issue boards'
......
......@@ -3,6 +3,7 @@ import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
import actions from 'ee/boards/stores/actions';
import * as types from 'ee/boards/stores/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import { ListType } from '~/boards/constants';
jest.mock('axios');
......@@ -56,6 +57,34 @@ describe('updateListWipLimit', () => {
});
});
describe('showPromotionList', () => {
it('should dispatch addList action when conditions showPromotion is true', done => {
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
disabled: false,
boardLists: [{ type: 'backlog' }, { type: 'closed' }],
showPromotion: true,
};
const promotionList = {
id: 'promotion',
listType: ListType.promotion,
title: 'Improve Issue Boards',
position: 0,
};
testAction(
actions.showPromotionList,
{},
state,
[],
[{ type: 'addList', payload: promotionList }],
done,
);
});
});
describe('fetchAllBoards', () => {
expectNotImplemented(actions.fetchAllBoards);
});
......
......@@ -2591,6 +2591,9 @@ msgstr ""
msgid "An error occurred while committing your changes."
msgstr ""
msgid "An error occurred while creating the list. Please try again."
msgstr ""
msgid "An error occurred while decoding the file."
msgstr ""
......@@ -2666,6 +2669,9 @@ msgstr ""
msgid "An error occurred while fetching the board issues. Please reload the page."
msgstr ""
msgid "An error occurred while fetching the board lists. Please reload the page."
msgstr ""
msgid "An error occurred while fetching the board lists. Please try again."
msgstr ""
......@@ -12962,6 +12968,9 @@ msgstr ""
msgid "ImportProjects|Update of imported projects with realtime changes failed"
msgstr ""
msgid "Improve Issue Boards"
msgstr ""
msgid "Improve Issue boards"
msgstr ""
......@@ -13291,6 +13300,9 @@ msgstr ""
msgid "Invalid URL"
msgstr ""
msgid "Invalid board"
msgstr ""
msgid "Invalid container_name"
msgstr ""
......@@ -27586,6 +27598,9 @@ msgstr ""
msgid "Welcome to your Issue Board!"
msgstr ""
msgid "Welcome to your issue board!"
msgstr ""
msgid "We’ve been making changes to %{featureName} and we’d love your feedback %{linkStart}in this issue%{linkEnd} to help us improve the experience."
msgstr ""
......
......@@ -130,24 +130,43 @@ RSpec.describe 'Issue Boards new issue', :js do
context 'group boards' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:group_board) { create(:board, group: group) }
let_it_be(:list) { create(:list, board: group_board, position: 0) }
let_it_be(:project_label) { create(:label, project: project, name: 'label') }
let_it_be(:list) { create(:list, board: group_board, label: project_label, position: 0) }
context 'for unauthorized users' do
before do
sign_in(user)
visit group_board_path(group, group_board)
wait_for_requests
end
context 'when backlog does not exist' do
before do
sign_in(user)
visit group_board_path(group, group_board)
wait_for_requests
end
it 'displays new issue button in open list' do
expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1)
it 'does not display new issue button in label list' do
page.within('.board.is-draggable') do
expect(page).not_to have_selector('.issue-count-badge-add-button')
end
end
end
it 'does not display new issue button in label list' do
page.within('.board.is-draggable') do
expect(page).not_to have_selector('.issue-count-badge-add-button')
context 'when backlog list already exists' do
let!(:backlog_list) { create(:backlog_list, board: group_board) }
before do
sign_in(user)
visit group_board_path(group, group_board)
wait_for_requests
end
it 'displays new issue button in open list' do
expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1)
end
it 'does not display new issue button in label list' do
page.within('.board.is-draggable') do
expect(page).not_to have_selector('.issue-count-badge-add-button')
end
end
end
end
......
......@@ -296,6 +296,7 @@ RSpec.describe 'Labels Hierarchy', :js do
let(:board) { create(:board, group: parent) }
before do
stub_feature_flags(graphql_board_lists: false)
parent.add_developer(user)
visit group_board_path(parent, board)
find('.js-new-board-list').click
......
import testAction from 'helpers/vuex_action_helper';
import actions from '~/boards/stores/actions';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import { inactiveId } from '~/boards/constants';
import { inactiveId, ListType } from '~/boards/constants';
const expectNotImplemented = action => {
it('is not implemented', () => {
......@@ -62,8 +62,31 @@ describe('setActiveId', () => {
});
});
describe('fetchLists', () => {
expectNotImplemented(actions.fetchLists);
describe('showWelcomeList', () => {
it('should dispatch addList action', done => {
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
disabled: false,
boardLists: [{ type: 'backlog' }, { type: 'closed' }],
};
const blankList = {
id: 'blank',
listType: ListType.blank,
title: 'Welcome to your issue board!',
position: 0,
};
testAction(
actions.showWelcomeList,
{},
state,
[],
[{ type: 'addList', payload: blankList }],
done,
);
});
});
describe('generateDefaultLists', () => {
......@@ -71,7 +94,70 @@ describe('generateDefaultLists', () => {
});
describe('createList', () => {
expectNotImplemented(actions.createList);
it('should dispatch addList action when creating backlog list', done => {
const backlogList = {
id: 'gid://gitlab/List/1',
listType: 'backlog',
title: 'Open',
position: 0,
};
jest.spyOn(gqlClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
boardListCreate: {
list: backlogList,
errors: [],
},
},
}),
);
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
};
testAction(
actions.createList,
{ backlog: true },
state,
[],
[{ type: 'addList', payload: { ...backlogList, id: 1 } }],
done,
);
});
it('should commit CREATE_LIST_FAILURE mutation when API returns an error', done => {
jest.spyOn(gqlClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
boardListCreate: {
list: {},
errors: [{ foo: 'bar' }],
},
},
}),
);
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
};
testAction(
actions.createList,
{ backlog: true },
state,
[{ type: types.CREATE_LIST_FAILURE }],
[],
done,
);
});
});
describe('updateList', () => {
......
import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
import { mockIssue } from '../mock_data';
import { listObj, listObjDuplicate, mockIssue } from '../mock_data';
const expectNotImplemented = action => {
it('is not implemented', () => {
......@@ -26,11 +27,30 @@ describe('Board Store Mutations', () => {
fullPath: 'gitlab-org',
};
const boardType = 'group';
const disabled = false;
const showPromotion = false;
mutations.SET_INITIAL_BOARD_DATA(state, { ...endpoints, boardType });
mutations[types.SET_INITIAL_BOARD_DATA](state, {
...endpoints,
boardType,
disabled,
showPromotion,
});
expect(state.endpoints).toEqual(endpoints);
expect(state.boardType).toEqual(boardType);
expect(state.disabled).toEqual(disabled);
expect(state.showPromotion).toEqual(showPromotion);
});
});
describe('RECEIVE_LISTS', () => {
it('Should set boardLists to state', () => {
const lists = [listObj, listObjDuplicate];
mutations[types.RECEIVE_LISTS](state, lists);
expect(state.boardLists).toEqual(lists);
});
});
......
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