Commit 24cb6278 authored by Simon Knox's avatar Simon Knox

Use vuex and graphql for removing board lists

Only works for group boards currently due to feature
flag restrictions
parent b40e8c75
...@@ -69,14 +69,18 @@ export default { ...@@ -69,14 +69,18 @@ export default {
eventHub.$off('sidebar.closeAll', this.unsetActiveId); eventHub.$off('sidebar.closeAll', this.unsetActiveId);
}, },
methods: { methods: {
...mapActions(['unsetActiveId']), ...mapActions(['unsetActiveId', 'removeList']),
showScopedLabels(label) { showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label); return boardsStore.scopedLabels.enabled && isScopedLabel(label);
}, },
deleteBoard() { deleteBoard() {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm(__('Are you sure you want to delete this list?'))) { if (window.confirm(__('Are you sure you want to remove this list?'))) {
if (this.shouldUseGraphQL) {
this.removeList(this.activeId);
} else {
this.activeList.destroy(); this.activeList.destroy();
}
this.unsetActiveId(); this.unsetActiveId();
} }
}, },
......
...@@ -7,6 +7,7 @@ import { deprecatedCreateFlash as flash } from '~/flash'; ...@@ -7,6 +7,7 @@ import { deprecatedCreateFlash as flash } from '~/flash';
import CreateLabelDropdown from '../../create_label'; import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import { fullLabelId } from '../boards_util'; import { fullLabelId } from '../boards_util';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import store from '~/boards/stores'; import store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
...@@ -61,7 +62,7 @@ export default function initNewListDropdown() { ...@@ -61,7 +62,7 @@ export default function initNewListDropdown() {
const active = boardsStore.findListByLabelId(label.id); const active = boardsStore.findListByLabelId(label.id);
const $li = $('<li />'); const $li = $('<li />');
const $a = $('<a />', { const $a = $('<a />', {
class: active ? `is-active js-board-list-${active.id}` : '', class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
text: label.title, text: label.title,
href: '#', href: '#',
}); });
......
mutation DestroyBoardList($listId: ID!) {
destroyBoardList(input: { listId: $listId }) {
errors
}
}
...@@ -18,6 +18,7 @@ import boardLabelsQuery from '../queries/board_labels.query.graphql'; ...@@ -18,6 +18,7 @@ import boardLabelsQuery from '../queries/board_labels.query.graphql';
import createBoardListMutation from '../queries/board_list_create.mutation.graphql'; import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
import updateBoardListMutation from '../queries/board_list_update.mutation.graphql'; import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql'; import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
import destroyBoardListMutation from '../queries/board_list_destroy.mutation.graphql';
import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql'; import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql'; import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql'; import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
...@@ -212,8 +213,26 @@ export default { ...@@ -212,8 +213,26 @@ export default {
}); });
}, },
deleteList: () => { removeList: ({ state, commit }, listId) => {
notImplemented(); const listsBackup = { ...state.boardLists };
commit(types.REMOVE_LIST, listId);
return gqlClient
.mutate({
mutation: destroyBoardListMutation,
variables: {
listId,
},
})
.then(({ data: { destroyBoardList: { errors } } }) => {
if (errors.length > 0) {
commit(types.REMOVE_LIST_FAILURE, listsBackup);
}
})
.catch(() => {
commit(types.REMOVE_LIST_FAILURE, listsBackup);
});
}, },
fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false }) => { fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false }) => {
......
/* eslint-disable no-shadow, no-param-reassign,consistent-return */ /* eslint-disable no-shadow, no-param-reassign,consistent-return */
/* global List */ /* global List */
/* global ListIssue */ /* global ListIssue */
import $ from 'jquery';
import { sortBy, pick } 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';
...@@ -119,8 +118,12 @@ const boardsStore = { ...@@ -119,8 +118,12 @@ const boardsStore = {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821 // https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
}); });
}, },
updateNewListDropdown(listId) { updateNewListDropdown(listId) {
$(`.js-board-list-${listId}`).removeClass('is-active'); // eslint-disable-next-line no-unused-expressions
document
.querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
?.classList.remove('is-active');
}, },
shouldAddBlankState() { shouldAddBlankState() {
// Decide whether to add the blank state // Decide whether to add the blank state
......
...@@ -12,9 +12,8 @@ export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; ...@@ -12,9 +12,8 @@ export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR'; export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
export const MOVE_LIST = 'MOVE_LIST'; export const MOVE_LIST = 'MOVE_LIST';
export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE'; export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST'; export const REMOVE_LIST = 'REMOVE_LIST';
export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS'; export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
export const REQUEST_ISSUES_FOR_LIST = 'REQUEST_ISSUES_FOR_LIST'; export const REQUEST_ISSUES_FOR_LIST = 'REQUEST_ISSUES_FOR_LIST';
export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE'; export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE';
export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS'; export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS';
......
...@@ -93,16 +93,13 @@ export default { ...@@ -93,16 +93,13 @@ export default {
Vue.set(state, 'boardLists', backupList); Vue.set(state, 'boardLists', backupList);
}, },
[mutationTypes.REQUEST_REMOVE_LIST]: () => { [mutationTypes.REMOVE_LIST]: (state, listId) => {
notImplemented(); Vue.delete(state.boardLists, listId);
}, },
[mutationTypes.RECEIVE_REMOVE_LIST_SUCCESS]: () => { [mutationTypes.REMOVE_LIST_FAILURE](state, listsBackup) {
notImplemented(); state.error = s__('Boards|An error occurred while removing the list. Please try again.');
}, state.boardLists = listsBackup;
[mutationTypes.RECEIVE_REMOVE_LIST_ERROR]: () => {
notImplemented();
}, },
[mutationTypes.REQUEST_ISSUES_FOR_LIST]: (state, { listId, fetchNext }) => { [mutationTypes.REQUEST_ISSUES_FOR_LIST]: (state, { listId, fetchNext }) => {
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
* @returns {Number} * @returns {Number}
*/ */
export const getIdFromGraphQLId = (gid = '') => export const getIdFromGraphQLId = (gid = '') =>
parseInt((gid || '').replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null; parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null;
export const MutationOperationMode = { export const MutationOperationMode = {
Append: 'APPEND', Append: 'APPEND',
......
...@@ -3588,9 +3588,6 @@ msgstr "" ...@@ -3588,9 +3588,6 @@ msgstr ""
msgid "Are you sure you want to delete this device? This action cannot be undone." msgid "Are you sure you want to delete this device? This action cannot be undone."
msgstr "" msgstr ""
msgid "Are you sure you want to delete this list?"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?" msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "" msgstr ""
...@@ -3641,6 +3638,9 @@ msgstr "" ...@@ -3641,6 +3638,9 @@ msgstr ""
msgid "Are you sure you want to remove this identity?" msgid "Are you sure you want to remove this identity?"
msgstr "" msgstr ""
msgid "Are you sure you want to remove this list?"
msgstr ""
msgid "Are you sure you want to reset registration token?" msgid "Are you sure you want to reset registration token?"
msgstr "" msgstr ""
...@@ -4409,6 +4409,9 @@ msgstr "" ...@@ -4409,6 +4409,9 @@ msgstr ""
msgid "Boards|An error occurred while moving the issue. Please try again." msgid "Boards|An error occurred while moving the issue. Please try again."
msgstr "" msgstr ""
msgid "Boards|An error occurred while removing the list. Please try again."
msgstr ""
msgid "Boards|An error occurred while updating the list. Please try again." msgid "Boards|An error occurred while updating the list. Please try again."
msgstr "" msgstr ""
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global List */ /* global List */
import Vue from 'vue'; import Vue from 'vue';
import { keyBy } from 'lodash';
import '~/boards/models/list'; import '~/boards/models/list';
import '~/boards/models/issue'; import '~/boards/models/issue';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
...@@ -310,6 +311,8 @@ export const mockLists = [ ...@@ -310,6 +311,8 @@ export const mockLists = [
}, },
]; ];
export const mockListsById = keyBy(mockLists, 'id');
export const mockListsWithModel = mockLists.map(listMock => export const mockListsWithModel = mockLists.map(listMock =>
Vue.observable(new List({ ...listMock, doNotFetchIssues: true })), Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
); );
......
...@@ -2,6 +2,7 @@ import testAction from 'helpers/vuex_action_helper'; ...@@ -2,6 +2,7 @@ import testAction from 'helpers/vuex_action_helper';
import { import {
mockListsWithModel, mockListsWithModel,
mockLists, mockLists,
mockListsById,
mockIssue, mockIssue,
mockIssueWithModel, mockIssueWithModel,
mockIssue2WithModel, mockIssue2WithModel,
...@@ -13,6 +14,7 @@ import actions, { gqlClient } from '~/boards/stores/actions'; ...@@ -13,6 +14,7 @@ import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types'; import * as types from '~/boards/stores/mutation_types';
import { inactiveId } from '~/boards/constants'; import { inactiveId } from '~/boards/constants';
import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql'; import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql';
import destroyBoardListMutation from '~/boards/queries/board_list_destroy.mutation.graphql';
import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql'; import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util'; import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util';
...@@ -318,8 +320,82 @@ describe('updateList', () => { ...@@ -318,8 +320,82 @@ describe('updateList', () => {
}); });
}); });
describe('deleteList', () => { describe('removeList', () => {
expectNotImplemented(actions.deleteList); let state;
const list = mockLists[0];
const listId = list.id;
const mutationVariables = {
mutation: destroyBoardListMutation,
variables: {
listId,
},
};
beforeEach(() => {
state = {
boardLists: mockListsById,
};
});
afterEach(() => {
state = null;
});
it('optimistically deletes the list', () => {
const commit = jest.fn();
actions.removeList({ commit, state }, listId);
expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]);
});
it('keeps the updated list if remove succeeds', async () => {
const commit = jest.fn();
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
destroyBoardList: {
errors: [],
},
},
});
await actions.removeList({ commit, state }, listId);
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]);
});
it('restores the list if update fails', async () => {
const commit = jest.fn();
jest.spyOn(gqlClient, 'mutate').mockResolvedValue(Promise.reject());
await actions.removeList({ commit, state }, listId);
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
expect(commit.mock.calls).toEqual([
[types.REMOVE_LIST, listId],
[types.REMOVE_LIST_FAILURE, mockListsById],
]);
});
it('restores the list if update response has errors', async () => {
const commit = jest.fn();
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
destroyBoardList: {
errors: ['update failed, ID invalid'],
},
},
});
await actions.removeList({ commit, state }, listId);
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
expect(commit.mock.calls).toEqual([
[types.REMOVE_LIST, listId],
[types.REMOVE_LIST_FAILURE, mockListsById],
]);
});
}); });
describe('fetchIssuesForList', () => { describe('fetchIssuesForList', () => {
......
...@@ -184,16 +184,43 @@ describe('Board Store Mutations', () => { ...@@ -184,16 +184,43 @@ describe('Board Store Mutations', () => {
}); });
}); });
describe('REQUEST_REMOVE_LIST', () => { describe('REMOVE_LIST', () => {
expectNotImplemented(mutations.REQUEST_REMOVE_LIST); it('removes list from boardLists', () => {
const [list, secondList] = mockListsWithModel;
const expected = {
[secondList.id]: secondList,
};
state = {
...state,
boardLists: { ...initialBoardListsState },
};
mutations[types.REMOVE_LIST](state, list.id);
expect(state.boardLists).toEqual(expected);
}); });
});
describe('REMOVE_LIST_FAILURE', () => {
it('restores lists from backup', () => {
const backupLists = { ...initialBoardListsState };
describe('RECEIVE_REMOVE_LIST_SUCCESS', () => { mutations[types.REMOVE_LIST_FAILURE](state, backupLists);
expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_SUCCESS);
expect(state.boardLists).toEqual(backupLists);
}); });
describe('RECEIVE_REMOVE_LIST_ERROR', () => { it('sets error state', () => {
expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR); const backupLists = { ...initialBoardListsState };
state = {
...state,
error: undefined,
};
mutations[types.REMOVE_LIST_FAILURE](state, backupLists);
expect(state.error).toEqual('An error occurred while removing the list. Please try again.');
});
}); });
describe('RESET_ISSUES', () => { describe('RESET_ISSUES', () => {
......
...@@ -10,6 +10,10 @@ describe('getIdFromGraphQLId', () => { ...@@ -10,6 +10,10 @@ describe('getIdFromGraphQLId', () => {
input: null, input: null,
output: null, output: null,
}, },
{
input: 2,
output: 2,
},
{ {
input: 'gid://', input: 'gid://',
output: null, output: null,
......
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