Commit 02d8146f authored by Florie Guibert's avatar Florie Guibert

Swimlanes - Add list to board

Store lists by id on VueX state
parent 18b27142
...@@ -2,11 +2,24 @@ import { sortBy } from 'lodash'; ...@@ -2,11 +2,24 @@ import { sortBy } from 'lodash';
import ListIssue from 'ee_else_ce/boards/models/issue'; import ListIssue from 'ee_else_ce/boards/models/issue';
import { ListType } from './constants'; import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
export function getMilestone() { export function getMilestone() {
return null; return null;
} }
export function formatBoardLists(lists) {
const formattedLists = lists.nodes.map(list =>
boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }),
);
return formattedLists.reduce((map, list) => {
return {
...map,
[list.id]: list,
};
}, {});
}
export function formatIssue(issue) { export function formatIssue(issue) {
return new ListIssue({ return new ListIssue({
...issue, ...issue,
...@@ -62,6 +75,13 @@ export function fullBoardId(boardId) { ...@@ -62,6 +75,13 @@ export function fullBoardId(boardId) {
return `gid://gitlab/Board/${boardId}`; return `gid://gitlab/Board/${boardId}`;
} }
export function fullLabelId(label) {
if (label.project_id !== null) {
return `gid://gitlab/ProjectLabel/${label.id}`;
}
return `gid://gitlab/GroupLabel/${label.id}`;
}
export function moveIssueListHelper(issue, fromList, toList) { export function moveIssueListHelper(issue, fromList, toList) {
if (toList.type === ListType.label) { if (toList.type === ListType.label) {
issue.addLabel(toList.label); issue.addLabel(toList.label);
...@@ -85,4 +105,5 @@ export default { ...@@ -85,4 +105,5 @@ export default {
formatIssue, formatIssue,
formatListIssues, formatListIssues,
fullBoardId, fullBoardId,
fullLabelId,
}; };
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { sortBy } from 'lodash';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import { GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
...@@ -30,7 +31,9 @@ export default { ...@@ -30,7 +31,9 @@ export default {
...mapState(['boardLists', 'error']), ...mapState(['boardLists', 'error']),
...mapGetters(['isSwimlanesOn']), ...mapGetters(['isSwimlanesOn']),
boardListsToUse() { boardListsToUse() {
return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists; const lists =
this.glFeatures.graphqlBoardLists || this.isSwimlanesOn ? this.boardLists : this.lists;
return sortBy([...Object.values(lists)], 'position');
}, },
}, },
mounted() { mounted() {
...@@ -68,7 +71,7 @@ export default { ...@@ -68,7 +71,7 @@ export default {
<template v-else> <template v-else>
<epics-swimlanes <epics-swimlanes
ref="swimlanes" ref="swimlanes"
:lists="boardLists" :lists="boardListsToUse"
:can-admin-list="canAdminList" :can-admin-list="canAdminList"
:disabled="disabled" :disabled="disabled"
/> />
......
...@@ -34,7 +34,7 @@ export default { ...@@ -34,7 +34,7 @@ export default {
referencing a List Model class. Reactivity only applies to plain JS objects referencing a List Model class. Reactivity only applies to plain JS objects
*/ */
if (this.glFeatures.graphqlBoardLists) { if (this.glFeatures.graphqlBoardLists) {
return this.boardLists.find(({ id }) => id === this.activeId); return this.boardLists[this.activeId];
} }
return boardsStore.state.lists.find(({ id }) => id === this.activeId); return boardsStore.state.lists.find(({ id }) => id === this.activeId);
}, },
......
...@@ -6,8 +6,14 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -6,8 +6,14 @@ import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash'; 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 store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
function shouldCreateListGraphQL(label) {
return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
}
$(document) $(document)
.off('created.label') .off('created.label')
.on('created.label', (e, label, addNewList) => { .on('created.label', (e, label, addNewList) => {
...@@ -15,6 +21,9 @@ $(document) ...@@ -15,6 +21,9 @@ $(document)
return; return;
} }
if (shouldCreateListGraphQL(label)) {
store.dispatch('createList', { labelId: fullLabelId(label) });
} else {
boardsStore.new({ boardsStore.new({
title: label.title, title: label.title,
position: boardsStore.state.lists.length - 2, position: boardsStore.state.lists.length - 2,
...@@ -25,6 +34,7 @@ $(document) ...@@ -25,6 +34,7 @@ $(document)
color: label.color, color: label.color,
}, },
}); });
}
}); });
export default function initNewListDropdown() { export default function initNewListDropdown() {
...@@ -74,7 +84,9 @@ export default function initNewListDropdown() { ...@@ -74,7 +84,9 @@ export default function initNewListDropdown() {
const label = options.selectedObj; const label = options.selectedObj;
e.preventDefault(); e.preventDefault();
if (!boardsStore.findListByLabelId(label.id)) { if (shouldCreateListGraphQL(label)) {
store.dispatch('createList', { labelId: fullLabelId(label) });
} else if (!boardsStore.findListByLabelId(label.id)) {
boardsStore.new({ boardsStore.new({
title: label.title, title: label.title,
position: boardsStore.state.lists.length - 2, position: boardsStore.state.lists.length - 2,
......
#import "./board_list.fragment.graphql" #import "ee_else_ce/boards/queries/board_list.fragment.graphql"
mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean) { mutation CreateBoardList(
boardListCreate(input: { boardId: $boardId, backlog: $backlog }) { $boardId: BoardID!
$backlog: Boolean
$labelId: LabelID
$milestoneId: MilestoneID
$assigneeId: UserID
) {
boardListCreate(
input: {
boardId: $boardId
backlog: $backlog
labelId: $labelId
milestoneId: $milestoneId
assigneeId: $assigneeId
}
) {
list { list {
...BoardListFragment ...BoardListFragment
} }
......
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { sortBy, pick } from 'lodash'; import { pick } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { BoardType, ListType, inactiveId } from '~/boards/constants'; import { BoardType, ListType, inactiveId } from '~/boards/constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { formatListIssues, fullBoardId, formatListsPageInfo } from '../boards_util'; import {
formatBoardLists,
formatListIssues,
fullBoardId,
formatListsPageInfo,
} from '../boards_util';
import boardStore from '~/boards/stores/boards_store'; import boardStore from '~/boards/stores/boards_store';
import listsIssuesQuery from '../queries/lists_issues.query.graphql'; import listsIssuesQuery from '../queries/lists_issues.query.graphql';
...@@ -71,38 +75,29 @@ export default { ...@@ -71,38 +75,29 @@ export default {
variables, variables,
}) })
.then(({ data }) => { .then(({ data }) => {
let { lists } = data[boardType]?.board; const { lists } = data[boardType]?.board;
// Temporarily using positioning logic from boardStore commit(types.RECEIVE_BOARD_LISTS_SUCCESS, formatBoardLists(lists));
lists = lists.nodes.map(list =>
boardStore.updateListPosition({
...list,
doNotFetchIssues: true,
}),
);
commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position'));
// Backlog list needs to be created if it doesn't exist // Backlog list needs to be created if it doesn't exist
if (!lists.find(l => l.type === ListType.backlog)) { if (!lists.nodes.find(l => l.listType === ListType.backlog)) {
dispatch('createList', { backlog: true }); dispatch('createList', { backlog: true });
} }
dispatch('showWelcomeList'); dispatch('showWelcomeList');
}) })
.catch(() => { .catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
createFlash(
__('An error occurred while fetching the board lists. Please reload the page.'),
);
});
}, },
// This action only supports backlog list creation at this stage createList: ({ state, commit, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
// Future iterations will add the ability to create other list types
createList: ({ state, commit, dispatch }, { backlog = false }) => {
const { boardId } = state.endpoints; const { boardId } = state.endpoints;
gqlClient gqlClient
.mutate({ .mutate({
mutation: createBoardListMutation, mutation: createBoardListMutation,
variables: { variables: {
boardId: fullBoardId(boardId), boardId: fullBoardId(boardId),
backlog, backlog,
labelId,
milestoneId,
assigneeId,
}, },
}) })
.then(({ data }) => { .then(({ data }) => {
...@@ -113,16 +108,15 @@ export default { ...@@ -113,16 +108,15 @@ export default {
dispatch('addList', list); dispatch('addList', list);
} }
}) })
.catch(() => { .catch(() => commit(types.CREATE_LIST_FAILURE));
commit(types.CREATE_LIST_FAILURE);
});
}, },
addList: ({ state, commit }, list) => { addList: ({ commit }, list) => {
const lists = state.boardLists;
// Temporarily using positioning logic from boardStore // Temporarily using positioning logic from boardStore
lists.push(boardStore.updateListPosition({ ...list, doNotFetchIssues: true })); commit(
commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position')); types.RECEIVE_ADD_LIST_SUCCESS,
boardStore.updateListPosition({ ...list, doNotFetchIssues: true }),
);
}, },
showWelcomeList: ({ state, dispatch }) => { showWelcomeList: ({ state, dispatch }) => {
...@@ -130,7 +124,9 @@ export default { ...@@ -130,7 +124,9 @@ export default {
return; return;
} }
if ( if (
state.boardLists.find(list => list.type !== ListType.backlog && list.type !== ListType.closed) Object.entries(state.boardLists).find(
([, list]) => list.type !== ListType.backlog && list.type !== ListType.closed,
)
) { ) {
return; return;
} }
...@@ -152,13 +148,16 @@ export default { ...@@ -152,13 +148,16 @@ export default {
notImplemented(); notImplemented();
}, },
moveList: ({ state, commit, dispatch }, { listId, newIndex, adjustmentValue }) => { moveList: (
{ state, commit, dispatch },
{ listId, replacedListId, newIndex, adjustmentValue },
) => {
const { boardLists } = state; const { boardLists } = state;
const backupList = [...boardLists]; const backupList = { ...boardLists };
const movedList = boardLists.find(({ id }) => id === listId); const movedList = boardLists[listId];
const newPosition = newIndex - 1; const newPosition = newIndex - 1;
const listAtNewIndex = boardLists[newIndex]; const listAtNewIndex = boardLists[replacedListId];
movedList.position = newPosition; movedList.position = newPosition;
listAtNewIndex.position += adjustmentValue; listAtNewIndex.position += adjustmentValue;
......
import { find } from 'lodash';
import { inactiveId } from '../constants'; import { inactiveId } from '../constants';
export default { export default {
...@@ -22,4 +23,16 @@ export default { ...@@ -22,4 +23,16 @@ export default {
getActiveIssue: state => { getActiveIssue: state => {
return state.issues[state.activeId] || {}; return state.issues[state.activeId] || {};
}, },
getListByLabelId: state => labelId => {
return find(state.boardLists, l => l.label?.id === labelId);
},
getListByTitle: state => title => {
return find(state.boardLists, l => l.title === title);
},
shouldUseGraphQL: () => {
return gon?.features?.graphqlBoardLists;
},
}; };
...@@ -3,6 +3,7 @@ export const SET_FILTERS = 'SET_FILTERS'; ...@@ -3,6 +3,7 @@ export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS'; export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE'; export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST'; export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST'; export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
......
import Vue from 'vue'; import Vue from 'vue';
import { sortBy, pull, union } from 'lodash'; import { pull, union } from 'lodash';
import { formatIssue, moveIssueListHelper } from '../boards_util'; import { formatIssue, moveIssueListHelper } from '../boards_util';
import * as mutationTypes from './mutation_types'; import * as mutationTypes from './mutation_types';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -10,16 +10,10 @@ const notImplemented = () => { ...@@ -10,16 +10,10 @@ const notImplemented = () => {
throw new Error('Not implemented!'); throw new Error('Not implemented!');
}; };
const getListById = ({ state, listId }) => {
const listIndex = state.boardLists.findIndex(l => l.id === listId);
const list = state.boardLists[listIndex];
return { listIndex, list };
};
export const removeIssueFromList = ({ state, listId, issueId }) => { export const removeIssueFromList = ({ state, listId, issueId }) => {
Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId)); Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
const { listIndex, list } = getListById({ state, listId }); const list = state.boardLists[listId];
Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize - 1 }); Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize - 1 });
}; };
export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => { export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
...@@ -32,8 +26,8 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter ...@@ -32,8 +26,8 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
} }
listIssues.splice(newIndex, 0, issueId); listIssues.splice(newIndex, 0, issueId);
Vue.set(state.issuesByListId, listId, listIssues); Vue.set(state.issuesByListId, listId, listIssues);
const { listIndex, list } = getListById({ state, listId }); const list = state.boardLists[listId];
Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize + 1 }); Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize + 1 });
}; };
export default { export default {
...@@ -49,6 +43,12 @@ export default { ...@@ -49,6 +43,12 @@ export default {
state.boardLists = lists; state.boardLists = lists;
}, },
[mutationTypes.RECEIVE_BOARD_LISTS_FAILURE]: state => {
state.error = s__(
'Boards|An error occurred while fetching the board lists. Please reload the page.',
);
},
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) { [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id; state.activeId = id;
state.sidebarType = sidebarType; state.sidebarType = sidebarType;
...@@ -66,8 +66,8 @@ export default { ...@@ -66,8 +66,8 @@ export default {
notImplemented(); notImplemented();
}, },
[mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: () => { [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: (state, list) => {
notImplemented(); Vue.set(state.boardLists, list.id, list);
}, },
[mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => { [mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => {
...@@ -76,10 +76,8 @@ export default { ...@@ -76,10 +76,8 @@ export default {
[mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => { [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => {
const { boardLists } = state; const { boardLists } = state;
const movedListIndex = state.boardLists.findIndex(l => l.id === movedList.id); Vue.set(boardLists, movedList.id, movedList);
Vue.set(boardLists, movedListIndex, movedList); Vue.set(boardLists, listAtNewIndex.id, listAtNewIndex);
Vue.set(boardLists, movedListIndex.position + 1, listAtNewIndex);
Vue.set(state, 'boardLists', sortBy(boardLists, 'position'));
}, },
[mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => { [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => {
...@@ -156,8 +154,8 @@ export default { ...@@ -156,8 +154,8 @@ export default {
state, state,
{ originalIssue, fromListId, toListId, moveBeforeId, moveAfterId }, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId },
) => { ) => {
const fromList = state.boardLists.find(l => l.id === fromListId); const fromList = state.boardLists[fromListId];
const toList = state.boardLists.find(l => l.id === toListId); const toList = state.boardLists[toListId];
const issue = moveIssueListHelper(originalIssue, fromList, toList); const issue = moveIssueListHelper(originalIssue, fromList, toList);
Vue.set(state.issues, issue.id, issue); Vue.set(state.issues, issue.id, issue);
......
...@@ -8,7 +8,7 @@ export default () => ({ ...@@ -8,7 +8,7 @@ export default () => ({
isShowingLabels: true, isShowingLabels: true,
activeId: inactiveId, activeId: inactiveId,
sidebarType: '', sidebarType: '',
boardLists: [], boardLists: {},
listsFlags: {}, listsFlags: {},
issuesByListId: {}, issuesByListId: {},
pageInfoByListId: {}, pageInfoByListId: {},
......
...@@ -7,7 +7,7 @@ class LabelEntity < Grape::Entity ...@@ -7,7 +7,7 @@ class LabelEntity < Grape::Entity
expose :color expose :color
expose :description expose :description
expose :group_id expose :group_id
expose :project_id expose :project_id, if: ->(label, _) { !label.is_a?(GlobalLabel) }
expose :template expose :template
expose :text_color expose :text_color
expose :created_at expose :created_at
......
...@@ -4,6 +4,6 @@ class LabelSerializer < BaseSerializer ...@@ -4,6 +4,6 @@ class LabelSerializer < BaseSerializer
entity LabelEntity entity LabelEntity
def represent_appearance(resource) def represent_appearance(resource)
represent(resource, { only: [:id, :title, :color, :text_color] }) represent(resource, { only: [:id, :title, :color, :text_color, :project_id] })
end end
end end
...@@ -6,7 +6,17 @@ export function fullEpicId(epicId) { ...@@ -6,7 +6,17 @@ export function fullEpicId(epicId) {
return `gid://gitlab/Epic/${epicId}`; return `gid://gitlab/Epic/${epicId}`;
} }
export function fullMilestoneId(milestoneId) {
return `gid://gitlab/Milestone/${milestoneId}`;
}
export function fullUserId(userId) {
return `gid://gitlab/User/${userId}`;
}
export default { export default {
getMilestone, getMilestone,
fullEpicId, fullEpicId,
fullMilestoneId,
fullUserId,
}; };
import Vue from 'vue'; import Vue from 'vue';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import vuexStore from '~/boards/stores';
import ListContainer from './list_container.vue'; import ListContainer from './list_container.vue';
import { fullMilestoneId, fullUserId } from '../../boards_util';
export default Vue.extend({ export default Vue.extend({
components: { components: {
...@@ -20,6 +22,7 @@ export default Vue.extend({ ...@@ -20,6 +22,7 @@ export default Vue.extend({
return { return {
loading: true, loading: true,
store: boardsStore, store: boardsStore,
vuexStore,
}; };
}, },
mounted() { mounted() {
...@@ -46,8 +49,7 @@ export default Vue.extend({ ...@@ -46,8 +49,7 @@ export default Vue.extend({
return foundName || username.indexOf(query) > -1; return foundName || username.indexOf(query) > -1;
}); });
}, },
handleItemClick(item) { prepareListObject(item) {
if (!this.store.findList('title', item.name)) {
const list = { const list = {
title: item.name, title: item.name,
position: this.store.state.lists.length - 2, position: this.store.state.lists.length - 2,
...@@ -60,6 +62,21 @@ export default Vue.extend({ ...@@ -60,6 +62,21 @@ export default Vue.extend({
list.user = item; list.user = item;
} }
return list;
},
handleItemClick(item) {
if (
this.vuexStore.getters.shouldUseGraphQL &&
!this.vuexStore.getters.getListByTitle(item.title)
) {
if (this.listType === 'milestones') {
this.vuexStore.dispatch('createList', { milestoneId: fullMilestoneId(item.id) });
} else if (this.listType === 'assignees') {
this.vuexStore.dispatch('createList', { assigneeId: fullUserId(item.id) });
}
} else if (!this.store.findList('title', item.title)) {
const list = this.prepareListObject(item);
this.store.new(list); this.store.new(list);
} }
}, },
......
...@@ -73,11 +73,13 @@ export default { ...@@ -73,11 +73,13 @@ export default {
methods: { methods: {
...mapActions(['moveList', 'fetchIssuesForList']), ...mapActions(['moveList', 'fetchIssuesForList']),
handleDragOnEnd(params) { handleDragOnEnd(params) {
const { newIndex, oldIndex, item } = params; const { newIndex, oldIndex, item, to } = params;
const { listId } = item.dataset; const { listId } = item.dataset;
const replacedListId = to.children[newIndex].dataset.listId;
this.moveList({ this.moveList({
listId, listId,
replacedListId,
newIndex, newIndex,
adjustmentValue: newIndex < oldIndex ? 1 : -1, adjustmentValue: newIndex < oldIndex ? 1 : -1,
}); });
......
import $ from 'jquery'; import $ from 'jquery';
import initNewListDropdown from '~/boards/components/new_list_dropdown'; import initNewListDropdown from '~/boards/components/new_list_dropdown';
import AssigneeList from './assignees_list_slector'; import AssigneeList from './assignees_list_selector';
import MilestoneList from './milestone_list_selector'; import MilestoneList from './milestone_list_selector';
const handleDropdownHide = e => { const handleDropdownHide = e => {
...@@ -12,7 +12,7 @@ const handleDropdownHide = e => { ...@@ -12,7 +12,7 @@ const handleDropdownHide = e => {
}; };
let assigneeList; let assigneeList;
let milstoneList; let milestoneList;
const handleDropdownTabClick = e => { const handleDropdownTabClick = e => {
const $addListEl = $('#js-add-list'); const $addListEl = $('#js-add-list');
...@@ -21,8 +21,8 @@ const handleDropdownTabClick = e => { ...@@ -21,8 +21,8 @@ const handleDropdownTabClick = e => {
assigneeList = AssigneeList(); assigneeList = AssigneeList();
} }
if (e.target.dataset.action === 'tab-milestones' && !milstoneList) { if (e.target.dataset.action === 'tab-milestones' && !milestoneList) {
milstoneList = MilestoneList(); milestoneList = MilestoneList();
} }
}; };
......
import { sortBy, pick } from 'lodash'; import { pick } from 'lodash';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
...@@ -10,7 +10,12 @@ import { EpicFilterType } from '../constants'; ...@@ -10,7 +10,12 @@ import { EpicFilterType } from '../constants';
import boardsStoreEE from './boards_store_ee'; import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { fullEpicId } from '../boards_util'; import { fullEpicId } from '../boards_util';
import { formatListIssues, formatListsPageInfo, fullBoardId } from '~/boards/boards_util'; import {
formatBoardLists,
formatListIssues,
formatListsPageInfo,
fullBoardId,
} from '~/boards/boards_util';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
...@@ -112,11 +117,7 @@ export default { ...@@ -112,11 +117,7 @@ export default {
commit(types.RECEIVE_EPICS_SUCCESS, epicsFormatted); commit(types.RECEIVE_EPICS_SUCCESS, epicsFormatted);
} else { } else {
if (lists) { if (lists) {
let boardLists = lists.nodes.map(list => commit(types.RECEIVE_BOARD_LISTS_SUCCESS, formatBoardLists(lists));
boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }),
);
boardLists = sortBy([...boardLists], 'position');
commit(types.RECEIVE_BOARD_LISTS_SUCCESS, boardLists);
} }
if (epicsFormatted) { if (epicsFormatted) {
......
...@@ -14,4 +14,11 @@ export default { ...@@ -14,4 +14,11 @@ export default {
getEpicById: state => epicId => { getEpicById: state => epicId => {
return state.epics.find(epic => epic.id === epicId); return state.epics.find(epic => epic.id === epicId);
}, },
shouldUseGraphQL: state => {
return (
(gon?.features?.boardsWithSwimlanes && state.isShowingEpicsSwimlanes) ||
gon?.features?.graphqlBoardLists
);
},
}; };
...@@ -141,8 +141,8 @@ export default { ...@@ -141,8 +141,8 @@ export default {
state, state,
{ originalIssue, fromListId, toListId, moveBeforeId, moveAfterId, epicId }, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId, epicId },
) => { ) => {
const fromList = state.boardLists.find(l => l.id === fromListId); const fromList = state.boardLists[fromListId];
const toList = state.boardLists.find(l => l.id === toListId); const toList = state.boardLists[toListId];
const issue = moveIssueListHelper(originalIssue, fromList, toList); const issue = moveIssueListHelper(originalIssue, fromList, toList);
......
...@@ -7,8 +7,15 @@ import { mockAssigneesList } from 'jest/boards/mock_data'; ...@@ -7,8 +7,15 @@ import { mockAssigneesList } from 'jest/boards/mock_data';
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 boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import { createStore } from '~/boards/stores';
describe('BoardListSelector', () => { describe('BoardListSelector', () => {
global.gon.features = {
...(global.gon.features || {}),
boardsWithSwimlanes: false,
graphqlBoardLists: false,
};
const dummyEndpoint = `${TEST_HOST}/users.json`; const dummyEndpoint = `${TEST_HOST}/users.json`;
const createComponent = () => const createComponent = () =>
...@@ -28,6 +35,7 @@ describe('BoardListSelector', () => { ...@@ -28,6 +35,7 @@ describe('BoardListSelector', () => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
vm = createComponent(); vm = createComponent();
vm.vuexStore = createStore();
}); });
afterEach(() => { afterEach(() => {
...@@ -86,8 +94,8 @@ describe('BoardListSelector', () => { ...@@ -86,8 +94,8 @@ describe('BoardListSelector', () => {
}); });
describe('handleItemClick', () => { describe('handleItemClick', () => {
it('creates new list in a store instance', () => { it('graphqlBoardLists FF off - creates new list in a store instance', () => {
jest.spyOn(vm.store, 'new').mockImplementation(() => {}); jest.spyOn(vm.store, 'new').mockReturnValue({});
const assignee = mockAssigneesList[0]; const assignee = mockAssigneesList[0];
expect(vm.store.findList('title', assignee.name)).not.toBeDefined(); expect(vm.store.findList('title', assignee.name)).not.toBeDefined();
...@@ -95,6 +103,20 @@ describe('BoardListSelector', () => { ...@@ -95,6 +103,20 @@ describe('BoardListSelector', () => {
expect(vm.store.new).toHaveBeenCalledWith(expect.any(Object)); expect(vm.store.new).toHaveBeenCalledWith(expect.any(Object));
}); });
it('graphqlBoardLists FF on - creates new list in a store instance', () => {
global.gon.features.graphqlBoardLists = true;
jest.spyOn(vm.vuexStore, 'dispatch').mockReturnValue({});
const assignee = mockAssigneesList[0];
expect(vm.vuexStore.getters.getListByTitle(assignee.name)).not.toBeDefined();
vm.handleItemClick(assignee);
expect(vm.vuexStore.dispatch).toHaveBeenCalledWith('createList', {
assigneeId: 'gid://gitlab/User/2',
});
});
}); });
}); });
}); });
import mutations from 'ee/boards/stores/mutations'; import mutations from 'ee/boards/stores/mutations';
import { import {
mockLists,
mockIssue, mockIssue,
mockIssue2, mockIssue2,
mockEpics, mockEpics,
...@@ -18,10 +17,15 @@ const expectNotImplemented = action => { ...@@ -18,10 +17,15 @@ const expectNotImplemented = action => {
const epicId = mockEpic.id; const epicId = mockEpic.id;
const initialBoardListsState = {
'gid://gitlab/List/1': mockListsWithModel[0],
'gid://gitlab/List/2': mockListsWithModel[1],
};
let state = { let state = {
issuesByListId: {}, issuesByListId: {},
issues: {}, issues: {},
boardLists: mockListsWithModel, boardLists: initialBoardListsState,
epicsFlags: { epicsFlags: {
[epicId]: { isLoading: true }, [epicId]: { isLoading: true },
}, },
...@@ -182,10 +186,10 @@ describe('RECEIVE_BOARD_LISTS_SUCCESS', () => { ...@@ -182,10 +186,10 @@ describe('RECEIVE_BOARD_LISTS_SUCCESS', () => {
boardLists: {}, boardLists: {},
}; };
mutations.RECEIVE_BOARD_LISTS_SUCCESS(state, mockLists); mutations.RECEIVE_BOARD_LISTS_SUCCESS(state, initialBoardListsState);
expect(state.epicsSwimlanesFetchInProgress).toBe(false); expect(state.epicsSwimlanesFetchInProgress).toBe(false);
expect(state.boardLists).toEqual(mockLists); expect(state.boardLists).toEqual(initialBoardListsState);
}); });
}); });
...@@ -273,7 +277,6 @@ describe('MOVE_ISSUE', () => { ...@@ -273,7 +277,6 @@ describe('MOVE_ISSUE', () => {
state = { state = {
...state, ...state,
issuesByListId: listIssues, issuesByListId: listIssues,
boardLists: mockListsWithModel,
issues, issues,
}; };
}); });
......
...@@ -2851,9 +2851,6 @@ msgstr "" ...@@ -2851,9 +2851,6 @@ msgstr ""
msgid "An error occurred while fetching the Service Desk address." msgid "An error occurred while fetching the Service Desk address."
msgstr "" 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." msgid "An error occurred while fetching the board lists. Please try again."
msgstr "" msgstr ""
...@@ -4185,6 +4182,9 @@ msgstr "" ...@@ -4185,6 +4182,9 @@ msgstr ""
msgid "Boards|An error occurred while fetching the board issues. Please reload the page." msgid "Boards|An error occurred while fetching the board issues. Please reload the page."
msgstr "" msgstr ""
msgid "Boards|An error occurred while fetching the board lists. Please reload the page."
msgstr ""
msgid "Boards|An error occurred while fetching the board swimlanes. Please reload the page." msgid "Boards|An error occurred while fetching the board swimlanes. Please reload the page."
msgstr "" msgstr ""
......
...@@ -177,16 +177,26 @@ describe('createList', () => { ...@@ -177,16 +177,26 @@ describe('createList', () => {
describe('moveList', () => { describe('moveList', () => {
it('should commit MOVE_LIST mutation and dispatch updateList action', done => { it('should commit MOVE_LIST mutation and dispatch updateList action', done => {
const initialBoardListsState = {
'gid://gitlab/List/1': mockListsWithModel[0],
'gid://gitlab/List/2': mockListsWithModel[1],
};
const state = { const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' }, endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group', boardType: 'group',
disabled: false, disabled: false,
boardLists: mockListsWithModel, boardLists: initialBoardListsState,
}; };
testAction( testAction(
actions.moveList, actions.moveList,
{ listId: 'gid://gitlab/List/1', newIndex: 1, adjustmentValue: 1 }, {
listId: 'gid://gitlab/List/1',
replacedListId: 'gid://gitlab/List/2',
newIndex: 1,
adjustmentValue: 1,
},
state, state,
[ [
{ {
...@@ -197,7 +207,11 @@ describe('moveList', () => { ...@@ -197,7 +207,11 @@ describe('moveList', () => {
[ [
{ {
type: 'updateList', type: 'updateList',
payload: { listId: 'gid://gitlab/List/1', position: 0, backupList: mockListsWithModel }, payload: {
listId: 'gid://gitlab/List/1',
position: 0,
backupList: initialBoardListsState,
},
}, },
], ],
done, done,
......
import getters from '~/boards/stores/getters'; import getters from '~/boards/stores/getters';
import { inactiveId } from '~/boards/constants'; import { inactiveId } from '~/boards/constants';
import { mockIssue, mockIssue2, mockIssues, mockIssuesByListId, issues } from '../mock_data'; import {
mockIssue,
mockIssue2,
mockIssues,
mockIssuesByListId,
issues,
mockListsWithModel,
} from '../mock_data';
describe('Boards - Getters', () => { describe('Boards - Getters', () => {
describe('getLabelToggleState', () => { describe('getLabelToggleState', () => {
...@@ -130,4 +137,25 @@ describe('Boards - Getters', () => { ...@@ -130,4 +137,25 @@ describe('Boards - Getters', () => {
); );
}); });
}); });
const boardsState = {
boardLists: {
'gid://gitlab/List/1': mockListsWithModel[0],
'gid://gitlab/List/2': mockListsWithModel[1],
},
};
describe('getListByLabelId', () => {
it('returns list for a given label id', () => {
expect(getters.getListByLabelId(boardsState)('gid://gitlab/GroupLabel/121')).toEqual(
mockListsWithModel[1],
);
});
});
describe('getListByTitle', () => {
it('returns list for a given list title', () => {
expect(getters.getListByTitle(boardsState)('To Do')).toEqual(mockListsWithModel[1]);
});
});
}); });
...@@ -2,8 +2,6 @@ import mutations from '~/boards/stores/mutations'; ...@@ -2,8 +2,6 @@ import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types'; import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state'; import defaultState from '~/boards/stores/state';
import { import {
listObj,
listObjDuplicate,
mockListsWithModel, mockListsWithModel,
mockLists, mockLists,
rawIssue, rawIssue,
...@@ -22,6 +20,11 @@ const expectNotImplemented = action => { ...@@ -22,6 +20,11 @@ const expectNotImplemented = action => {
describe('Board Store Mutations', () => { describe('Board Store Mutations', () => {
let state; let state;
const initialBoardListsState = {
'gid://gitlab/List/1': mockListsWithModel[0],
'gid://gitlab/List/2': mockListsWithModel[1],
};
beforeEach(() => { beforeEach(() => {
state = defaultState(); state = defaultState();
}); });
...@@ -56,11 +59,19 @@ describe('Board Store Mutations', () => { ...@@ -56,11 +59,19 @@ describe('Board Store Mutations', () => {
describe('RECEIVE_BOARD_LISTS_SUCCESS', () => { describe('RECEIVE_BOARD_LISTS_SUCCESS', () => {
it('Should set boardLists to state', () => { it('Should set boardLists to state', () => {
const lists = [listObj, listObjDuplicate]; mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, initialBoardListsState);
expect(state.boardLists).toEqual(initialBoardListsState);
});
});
mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, lists); describe('RECEIVE_BOARD_LISTS_FAILURE', () => {
it('Should set error in state', () => {
mutations[types.RECEIVE_BOARD_LISTS_FAILURE](state);
expect(state.boardLists).toEqual(lists); expect(state.error).toEqual(
'An error occurred while fetching the board lists. Please reload the page.',
);
}); });
}); });
...@@ -95,7 +106,13 @@ describe('Board Store Mutations', () => { ...@@ -95,7 +106,13 @@ describe('Board Store Mutations', () => {
}); });
describe('RECEIVE_ADD_LIST_SUCCESS', () => { describe('RECEIVE_ADD_LIST_SUCCESS', () => {
expectNotImplemented(mutations.RECEIVE_ADD_LIST_SUCCESS); it('adds list to boardLists state', () => {
mutations.RECEIVE_ADD_LIST_SUCCESS(state, mockListsWithModel[0]);
expect(state.boardLists).toEqual({
[mockListsWithModel[0].id]: mockListsWithModel[0],
});
});
}); });
describe('RECEIVE_ADD_LIST_ERROR', () => { describe('RECEIVE_ADD_LIST_ERROR', () => {
...@@ -106,7 +123,7 @@ describe('Board Store Mutations', () => { ...@@ -106,7 +123,7 @@ describe('Board Store Mutations', () => {
it('updates boardLists state with reordered lists', () => { it('updates boardLists state with reordered lists', () => {
state = { state = {
...state, ...state,
boardLists: mockListsWithModel, boardLists: initialBoardListsState,
}; };
mutations.MOVE_LIST(state, { mutations.MOVE_LIST(state, {
...@@ -114,7 +131,10 @@ describe('Board Store Mutations', () => { ...@@ -114,7 +131,10 @@ describe('Board Store Mutations', () => {
listAtNewIndex: mockListsWithModel[1], listAtNewIndex: mockListsWithModel[1],
}); });
expect(state.boardLists).toEqual([mockListsWithModel[1], mockListsWithModel[0]]); expect(state.boardLists).toEqual({
'gid://gitlab/List/2': mockListsWithModel[1],
'gid://gitlab/List/1': mockListsWithModel[0],
});
}); });
}); });
...@@ -122,13 +142,16 @@ describe('Board Store Mutations', () => { ...@@ -122,13 +142,16 @@ describe('Board Store Mutations', () => {
it('updates boardLists state with previous order and sets error message', () => { it('updates boardLists state with previous order and sets error message', () => {
state = { state = {
...state, ...state,
boardLists: [mockListsWithModel[1], mockListsWithModel[0]], boardLists: {
'gid://gitlab/List/2': mockListsWithModel[1],
'gid://gitlab/List/1': mockListsWithModel[0],
},
error: undefined, error: undefined,
}; };
mutations.UPDATE_LIST_FAILURE(state, mockListsWithModel); mutations.UPDATE_LIST_FAILURE(state, initialBoardListsState);
expect(state.boardLists).toEqual(mockListsWithModel); expect(state.boardLists).toEqual(initialBoardListsState);
expect(state.error).toEqual('An error occurred while updating the list. Please try again.'); expect(state.error).toEqual('An error occurred while updating the list. Please try again.');
}); });
}); });
...@@ -177,7 +200,7 @@ describe('Board Store Mutations', () => { ...@@ -177,7 +200,7 @@ describe('Board Store Mutations', () => {
'gid://gitlab/List/1': [], 'gid://gitlab/List/1': [],
}, },
issues: {}, issues: {},
boardLists: mockListsWithModel, boardLists: initialBoardListsState,
}; };
const listPageInfo = { const listPageInfo = {
...@@ -202,7 +225,7 @@ describe('Board Store Mutations', () => { ...@@ -202,7 +225,7 @@ describe('Board Store Mutations', () => {
it('sets error message', () => { it('sets error message', () => {
state = { state = {
...state, ...state,
boardLists: mockListsWithModel, boardLists: initialBoardListsState,
error: undefined, error: undefined,
}; };
...@@ -284,7 +307,7 @@ describe('Board Store Mutations', () => { ...@@ -284,7 +307,7 @@ describe('Board Store Mutations', () => {
state = { state = {
...state, ...state,
issuesByListId: listIssues, issuesByListId: listIssues,
boardLists: mockListsWithModel, boardLists: initialBoardListsState,
issues, issues,
}; };
...@@ -332,7 +355,7 @@ describe('Board Store Mutations', () => { ...@@ -332,7 +355,7 @@ describe('Board Store Mutations', () => {
state = { state = {
...state, ...state,
issuesByListId: listIssues, issuesByListId: listIssues,
boardLists: mockListsWithModel, boardLists: initialBoardListsState,
}; };
mutations.MOVE_ISSUE_FAILURE(state, { mutations.MOVE_ISSUE_FAILURE(state, {
...@@ -400,7 +423,7 @@ describe('Board Store Mutations', () => { ...@@ -400,7 +423,7 @@ describe('Board Store Mutations', () => {
...state, ...state,
issuesByListId: listIssues, issuesByListId: listIssues,
issues, issues,
boardLists: mockListsWithModel, boardLists: initialBoardListsState,
}; };
mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 }); mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 });
......
...@@ -37,11 +37,12 @@ RSpec.describe LabelSerializer do ...@@ -37,11 +37,12 @@ RSpec.describe LabelSerializer do
subject { serializer.represent_appearance(resource) } subject { serializer.represent_appearance(resource) }
it 'serializes only attributes used for appearance' do it 'serializes only attributes used for appearance' do
expect(subject.keys).to eq([:id, :title, :color, :text_color]) expect(subject.keys).to eq([:id, :title, :color, :project_id, :text_color])
expect(subject[:id]).to eq(resource.id) expect(subject[:id]).to eq(resource.id)
expect(subject[:title]).to eq(resource.title) expect(subject[:title]).to eq(resource.title)
expect(subject[:color]).to eq(resource.color) expect(subject[:color]).to eq(resource.color)
expect(subject[:text_color]).to eq(resource.text_color) expect(subject[:text_color]).to eq(resource.text_color)
expect(subject[:project_id]).to eq(resource.project_id)
end end
end end
end end
......
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