Commit cb2d374b authored by Florie Guibert's avatar Florie Guibert

Boards refactor - Fetch issues with GraphQL

Fetch issues per list with VueX action
parent 3d9f7a44
<script> <script>
import { mapGetters, mapActions } from 'vuex';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits'; import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import Tooltip from '~/vue_shared/directives/tooltip'; import Tooltip from '~/vue_shared/directives/tooltip';
import EmptyComponent from '~/vue_shared/components/empty_component'; import EmptyComponent from '~/vue_shared/components/empty_component';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardBlankState from './board_blank_state.vue'; import BoardBlankState from './board_blank_state.vue';
import BoardList from './board_list.vue'; import BoardList from './board_list.vue';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
...@@ -21,7 +23,7 @@ export default { ...@@ -21,7 +23,7 @@ export default {
directives: { directives: {
Tooltip, Tooltip,
}, },
mixins: [isWipLimitsOn], mixins: [isWipLimitsOn, glFeatureFlagMixin()],
props: { props: {
list: { list: {
type: Object, type: Object,
...@@ -62,6 +64,7 @@ export default { ...@@ -62,6 +64,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['getIssues']),
showBoardListAndBoardInfo() { showBoardListAndBoardInfo() {
return this.list.type !== ListType.blank && this.list.type !== ListType.promotion; return this.list.type !== ListType.blank && this.list.type !== ListType.promotion;
}, },
...@@ -69,19 +72,36 @@ export default { ...@@ -69,19 +72,36 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`; return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
}, },
listIssues() {
if (!this.glFeatures.graphqlBoardLists) {
return this.list.issues;
}
return this.getIssues(this.list.id);
},
shouldFetchIssues() {
return this.glFeatures.graphqlBoardLists && this.list.type !== ListType.blank;
},
}, },
watch: { watch: {
filter: { filter: {
handler() { handler() {
this.list.page = 1; if (this.shouldFetchIssues) {
this.list.getIssues(true).catch(() => { this.fetchIssuesForList(this.list.id);
// TODO: handle request error } else {
}); this.list.page = 1;
this.list.getIssues(true).catch(() => {
// TODO: handle request error
});
}
}, },
deep: true, deep: true,
}, },
}, },
mounted() { mounted() {
if (this.shouldFetchIssues) {
this.fetchIssuesForList(this.list.id);
}
const instance = this; const instance = this;
const sortableOptions = getBoardSortableDefaultOptions({ const sortableOptions = getBoardSortableDefaultOptions({
...@@ -108,6 +128,7 @@ export default { ...@@ -108,6 +128,7 @@ export default {
Sortable.create(this.$el.parentNode, sortableOptions); Sortable.create(this.$el.parentNode, sortableOptions);
}, },
methods: { methods: {
...mapActions(['fetchIssuesForList']),
showListNewIssueForm(listId) { showListNewIssueForm(listId) {
eventHub.$emit('showForm', listId); eventHub.$emit('showForm', listId);
}, },
...@@ -142,7 +163,7 @@ export default { ...@@ -142,7 +163,7 @@ export default {
:disabled="disabled" :disabled="disabled"
:group-id="groupId || null" :group-id="groupId || null"
:issue-link-base="issueLinkBase" :issue-link-base="issueLinkBase"
:issues="list.issues" :issues="listIssues"
:list="list" :list="list"
:loading="list.loading" :loading="list.loading"
:root-path="rootPath" :root-path="rootPath"
......
...@@ -6,6 +6,7 @@ import boardCard from './board_card.vue'; ...@@ -6,6 +6,7 @@ import boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { import {
getBoardSortableDefaultOptions, getBoardSortableDefaultOptions,
...@@ -24,6 +25,7 @@ export default { ...@@ -24,6 +25,7 @@ export default {
boardNewIssue, boardNewIssue,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [glFeatureFlagMixin()],
props: { props: {
groupId: { groupId: {
type: Number, type: Number,
...@@ -83,6 +85,7 @@ export default { ...@@ -83,6 +85,7 @@ export default {
deep: true, deep: true,
}, },
issues() { issues() {
if (this.glFeatures.graphqlBoardLists) return;
this.$nextTick(() => { this.$nextTick(() => {
if ( if (
this.scrollHeight() <= this.listHeight() && this.scrollHeight() <= this.listHeight() &&
...@@ -413,6 +416,8 @@ export default { ...@@ -413,6 +416,8 @@ export default {
this.showIssueForm = !this.showIssueForm; this.showIssueForm = !this.showIssueForm;
}, },
onScroll() { onScroll() {
if (this.glFeatures.graphqlBoardLists) return;
if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) { if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) {
this.loadNextPage(); this.loadNextPage();
} }
......
...@@ -129,6 +129,9 @@ export default { ...@@ -129,6 +129,9 @@ export default {
collapsedTooltipTitle() { collapsedTooltipTitle() {
return this.listTitle || this.listAssignee; return this.listTitle || this.listAssignee;
}, },
shouldDisplaySwimlanes() {
return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn;
},
}, },
methods: { methods: {
...mapActions(['updateList']), ...mapActions(['updateList']),
...@@ -158,7 +161,7 @@ export default { ...@@ -158,7 +161,7 @@ export default {
} }
}, },
updateListFunction() { updateListFunction() {
if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesHeader) { if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded }); this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded });
} else { } else {
this.list.update(); this.list.update();
...@@ -184,7 +187,7 @@ export default { ...@@ -184,7 +187,7 @@ export default {
<h3 <h3
:class="{ :class="{
'user-can-drag': !disabled && !list.preset, 'user-can-drag': !disabled && !list.preset,
'gl-py-3': !list.isExpanded && !isSwimlanesHeader, 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
'gl-border-b-0': !list.isExpanded || isSwimlanesHeader, 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
'gl-py-2': !list.isExpanded && isSwimlanesHeader, 'gl-py-2': !list.isExpanded && isSwimlanesHeader,
}" }"
......
...@@ -42,6 +42,9 @@ export default { ...@@ -42,6 +42,9 @@ export default {
} }
return this.title === ''; return this.title === '';
}, },
shouldDisplaySwimlanes() {
return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn;
},
}, },
mounted() { mounted() {
this.$refs.input.focus(); this.$refs.input.focus();
...@@ -75,7 +78,7 @@ export default { ...@@ -75,7 +78,7 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`); eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel(); this.cancel();
if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) { if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
this.addListIssue({ list: this.list, issue, position: 0 }); this.addListIssue({ list: this.list, issue, position: 0 });
} }
...@@ -85,7 +88,7 @@ export default { ...@@ -85,7 +88,7 @@ export default {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
if (!this.glFeatures.boardsWithSwimlanes || !this.isSwimlanesOn) { if (!this.shouldDisplaySwimlanes && !this.glFeatures.graphqlBoardLists) {
boardsStore.setIssueDetail(issue); boardsStore.setIssueDetail(issue);
boardsStore.setListDetail(this.list); boardsStore.setListDetail(this.list);
} }
...@@ -95,7 +98,7 @@ export default { ...@@ -95,7 +98,7 @@ export default {
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
// Remove the issue // Remove the issue
if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) { if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
this.addListIssueFailure({ list: this.list, issue }); this.addListIssueFailure({ list: this.list, issue });
} else { } else {
this.list.removeIssue(issue); this.list.removeIssue(issue);
......
...@@ -27,7 +27,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -27,7 +27,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
updateObject(path) { updateObject(path) {
this.store.path = path.substr(1); this.store.path = path.substr(1);
if (gon.features.boardsWithSwimlanes) { if (gon.features.boardsWithSwimlanes || gon.features.graphqlBoardLists) {
boardsStore.updateFiltersUrl(); boardsStore.updateFiltersUrl();
boardsStore.performSearch(); boardsStore.performSearch();
} }
......
...@@ -7,15 +7,10 @@ fragment IssueNode on Issue { ...@@ -7,15 +7,10 @@ fragment IssueNode on Issue {
referencePath: reference(full: true) referencePath: reference(full: true)
dueDate dueDate
timeEstimate timeEstimate
weight
confidential confidential
webUrl webUrl
subscribed subscribed
blocked
relativePosition relativePosition
epic {
id
}
assignees { assignees {
nodes { nodes {
...User ...User
......
#import "./issue.fragment.graphql" #import "ee_else_ce/boards/queries/issue.fragment.graphql"
query ListIssues( query ListIssues(
$fullPath: ID! $fullPath: ID!
$boardId: ID! $boardId: ID!
$id: ID
$filters: BoardIssueInput $filters: BoardIssueInput
$isGroup: Boolean = false $isGroup: Boolean = false
$isProject: Boolean = false $isProject: Boolean = false
) { ) {
group(fullPath: $fullPath) @include(if: $isGroup) { group(fullPath: $fullPath) @include(if: $isGroup) {
board(id: $boardId) { board(id: $boardId) {
lists { lists(id: $id) {
nodes { nodes {
id id
issues(filters: $filters) { issues(filters: $filters) {
...@@ -23,7 +24,7 @@ query ListIssues( ...@@ -23,7 +24,7 @@ query ListIssues(
} }
project(fullPath: $fullPath) @include(if: $isProject) { project(fullPath: $fullPath) @include(if: $isProject) {
board(id: $boardId) { board(id: $boardId) {
lists { lists(id: $id) {
nodes { nodes {
id id
issues(filters: $filters) { issues(filters: $filters) {
......
...@@ -79,10 +79,10 @@ export default { ...@@ -79,10 +79,10 @@ export default {
lists = lists.nodes.map(list => lists = lists.nodes.map(list =>
boardStore.updateListPosition({ boardStore.updateListPosition({
...list, ...list,
id: getIdFromGraphQLId(list.id), doNotFetchIssues: true,
}), }),
); );
commit(types.RECEIVE_LISTS, sortBy(lists, 'position')); 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.find(l => l.type === ListType.backlog)) {
dispatch('createList', { backlog: true }); dispatch('createList', { backlog: true });
...@@ -113,7 +113,7 @@ export default { ...@@ -113,7 +113,7 @@ export default {
commit(types.CREATE_LIST_FAILURE); commit(types.CREATE_LIST_FAILURE);
} else { } else {
const list = data.boardListCreate?.list; const list = data.boardListCreate?.list;
dispatch('addList', { ...list, id: getIdFromGraphQLId(list.id) }); dispatch('addList', list);
} }
}) })
.catch(() => { .catch(() => {
...@@ -124,8 +124,8 @@ export default { ...@@ -124,8 +124,8 @@ export default {
addList: ({ state, commit }, list) => { addList: ({ state, commit }, list) => {
const lists = state.boardLists; const lists = state.boardLists;
// Temporarily using positioning logic from boardStore // Temporarily using positioning logic from boardStore
lists.push(boardStore.updateListPosition(list)); lists.push(boardStore.updateListPosition({ ...list, doNotFetchIssues: true }));
commit(types.RECEIVE_LISTS, sortBy(lists, 'position')); commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position'));
}, },
showWelcomeList: ({ state, dispatch }) => { showWelcomeList: ({ state, dispatch }) => {
...@@ -197,8 +197,33 @@ export default { ...@@ -197,8 +197,33 @@ export default {
notImplemented(); notImplemented();
}, },
fetchIssuesForList: () => { fetchIssuesForList: ({ state, commit }, listId) => {
notImplemented(); const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
const variables = {
fullPath,
boardId: fullBoardId(boardId),
id: listId,
filters: filterParams,
isGroup: boardType === BoardType.group,
isProject: boardType === BoardType.project,
};
return gqlClient
.query({
query: listsIssuesQuery,
context: {
isSingleRequest: true,
},
variables,
})
.then(({ data }) => {
const { lists } = data[boardType]?.board;
const listIssues = formatListIssues(lists);
commit(types.RECEIVE_ISSUES_FOR_LIST_SUCCESS, { listIssues, listId });
})
.catch(() => commit(types.RECEIVE_ISSUES_FOR_LIST_FAILURE, listId));
}, },
fetchIssuesForAllLists: ({ state, commit }) => { fetchIssuesForAllLists: ({ state, commit }) => {
......
...@@ -304,7 +304,11 @@ const boardsStore = { ...@@ -304,7 +304,11 @@ const boardsStore = {
onNewListIssueResponse(list, issue, data) { onNewListIssueResponse(list, issue, data) {
issue.refreshData(data); issue.refreshData(data);
if (!gon.features.boardsWithSwimlanes && list.issuesSize > 1) { if (
!gon.features.boardsWithSwimlanes &&
!gon.features.graphqlBoardLists &&
list.issues.length > 1
) {
const moveBeforeId = list.issues[1].id; const moveBeforeId = list.issues[1].id;
this.moveIssue(issue.id, null, null, null, moveBeforeId); this.moveIssue(issue.id, null, null, null, moveBeforeId);
} }
...@@ -723,6 +727,10 @@ const boardsStore = { ...@@ -723,6 +727,10 @@ const boardsStore = {
newListIssue(list, issue) { newListIssue(list, issue) {
list.addIssue(issue, null, 0); list.addIssue(issue, null, 0);
list.issuesSize += 1; list.issuesSize += 1;
let listId = list.id;
if (typeof listId === 'string') {
listId = getIdFromGraphQLId(listId);
}
return this.newIssue(list.id, issue) return this.newIssue(list.id, issue)
.then(res => res.data) .then(res => res.data)
......
...@@ -14,6 +14,11 @@ export default { ...@@ -14,6 +14,11 @@ export default {
return state.issues[id] || {}; return state.issues[id] || {};
}, },
getIssues: (state, getters) => listId => {
const listIssueIds = state.issuesByListId[listId] || [];
return listIssueIds.map(id => getters.getIssueById(id));
},
getActiveIssue: state => { getActiveIssue: state => {
return state.issues[state.activeId] || {}; return state.issues[state.activeId] || {};
}, },
......
...@@ -2,7 +2,7 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA'; ...@@ -2,7 +2,7 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_FILTERS = 'SET_FILTERS'; 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_LISTS = 'RECEIVE_LISTS'; export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
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';
...@@ -13,6 +13,8 @@ export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST'; ...@@ -13,6 +13,8 @@ export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST';
export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS'; export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS';
export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR'; export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
export const REQUEST_ISSUES_FOR_ALL_LISTS = 'REQUEST_ISSUES_FOR_ALL_LISTS'; export const REQUEST_ISSUES_FOR_ALL_LISTS = 'REQUEST_ISSUES_FOR_ALL_LISTS';
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_ALL_LISTS_SUCCESS = 'RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS'; export const RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS = 'RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS';
export const RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE = 'RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE'; export const RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE = 'RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE';
export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE'; export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
......
...@@ -35,7 +35,7 @@ export default { ...@@ -35,7 +35,7 @@ export default {
state.showPromotion = showPromotion; state.showPromotion = showPromotion;
}, },
[mutationTypes.RECEIVE_LISTS]: (state, lists) => { [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
state.boardLists = lists; state.boardLists = lists;
}, },
...@@ -89,6 +89,20 @@ export default { ...@@ -89,6 +89,20 @@ export default {
notImplemented(); notImplemented();
}, },
[mutationTypes.RECEIVE_ISSUES_FOR_LIST_SUCCESS]: (state, { listIssues, listId }) => {
const { listData, issues } = listIssues;
Vue.set(state, 'issues', { ...state.issues, ...issues });
Vue.set(state.issuesByListId, listId, listData[listId]);
const listIndex = state.boardLists.findIndex(l => l.id === listId);
Vue.set(state.boardLists[listIndex], 'loading', false);
},
[mutationTypes.RECEIVE_ISSUES_FOR_LIST_FAILURE]: (state, listId) => {
state.error = __('An error occurred while fetching the board issues. Please reload the page.');
const listIndex = state.boardLists.findIndex(l => l.id === listId);
Vue.set(state.boardLists[listIndex], 'loading', false);
},
[mutationTypes.REQUEST_ISSUES_FOR_ALL_LISTS]: state => { [mutationTypes.REQUEST_ISSUES_FOR_ALL_LISTS]: state => {
state.isLoadingIssues = true; state.isLoadingIssues = true;
}, },
......
...@@ -116,7 +116,6 @@ ...@@ -116,7 +116,6 @@
.board-title { .board-title {
flex-direction: column; flex-direction: column;
height: 100%;
} }
.board-title-caret { .board-title-caret {
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" } #board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
= render 'shared/issuable/search_bar', type: :boards, board: board = render 'shared/issuable/search_bar', type: :boards, board: board
- if Feature.enabled?(:boards_with_swimlanes, current_board_parent) - if Feature.enabled?(:boards_with_swimlanes, current_board_parent) || Feature.enabled?(:graphql_board_lists, current_board_parent)
%board-content{ "v-cloak" => "true", %board-content{ "v-cloak" => "true",
"ref" => "board_content", "ref" => "board_content",
":lists" => "state.lists", ":lists" => "state.lists",
......
#import "~/graphql_shared/fragments/user.fragment.graphql"
fragment IssueNode on Issue {
id
iid
title
referencePath: reference(full: true)
dueDate
timeEstimate
weight
confidential
webUrl
subscribed
blocked
relativePosition
epic {
id
}
assignees {
nodes {
...User
}
}
labels {
nodes {
id
title
color
description
}
}
}
...@@ -3,11 +3,6 @@ import gettersCE from '~/boards/stores/getters'; ...@@ -3,11 +3,6 @@ import gettersCE from '~/boards/stores/getters';
export default { export default {
...gettersCE, ...gettersCE,
getIssues: (state, getters) => listId => {
const listIssueIds = state.issuesByListId[listId] || [];
return listIssueIds.map(id => getters.getIssueById(id));
},
getIssuesByEpic: (state, getters) => (listId, epicId) => { getIssuesByEpic: (state, getters) => (listId, epicId) => {
return getters.getIssues(listId).filter(issue => issue.epic && issue.epic.id === epicId); return getters.getIssues(listId).filter(issue => issue.epic && issue.epic.id === epicId);
}, },
......
...@@ -15,6 +15,8 @@ RSpec.describe 'label issues', :js do ...@@ -15,6 +15,8 @@ RSpec.describe 'label issues', :js do
before do before do
stub_licensed_features(multiple_group_issue_boards: true) stub_licensed_features(multiple_group_issue_boards: true)
# stubbing until sidebar work is done: https://gitlab.com/gitlab-org/gitlab/-/issues/230711
stub_feature_flags(graphql_board_lists: false)
group.add_maintainer(user) group.add_maintainer(user)
sign_in(user) sign_in(user)
......
import getters from 'ee/boards/stores/getters'; import getters from 'ee/boards/stores/getters';
import { import {
mockIssue, mockIssue,
mockIssue2,
mockIssue3, mockIssue3,
mockIssue4, mockIssue4,
mockIssues, mockIssues,
...@@ -15,16 +14,6 @@ describe('EE Boards Store Getters', () => { ...@@ -15,16 +14,6 @@ describe('EE Boards Store Getters', () => {
issues, issues,
}; };
describe('getIssues', () => {
it('returns issues for a given listId', () => {
const getIssueById = issueId => [mockIssue, mockIssue2].find(({ id }) => id === issueId);
expect(getters.getIssues(boardsState, { getIssueById })('gid://gitlab/List/2')).toEqual(
mockIssues,
);
});
});
describe('getIssuesByEpic', () => { describe('getIssuesByEpic', () => {
it('returns issues for a given listId and epicId', () => { it('returns issues for a given listId and epicId', () => {
const getIssues = () => mockIssues; const getIssues = () => mockIssues;
......
...@@ -19,6 +19,8 @@ RSpec.describe 'Group Issue Boards', :js do ...@@ -19,6 +19,8 @@ RSpec.describe 'Group Issue Boards', :js do
let(:card) { find('.board:nth-child(1)').first('.board-card') } let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do before do
# stubbing until sidebar work is done: https://gitlab.com/gitlab-org/gitlab/-/issues/230711
stub_feature_flags(graphql_board_lists: false)
sign_in(user) sign_in(user)
visit group_board_path(group, board) visit group_board_path(group, board)
......
...@@ -98,12 +98,35 @@ export const mockMilestone = { ...@@ -98,12 +98,35 @@ export const mockMilestone = {
due_date: '2019-12-31', due_date: '2019-12-31',
}; };
const assignees = [
{
id: 'gid://gitlab/User/2',
username: 'angelina.herman',
name: 'Bernardina Bosco',
avatar: 'https://www.gravatar.com/avatar/eb7b664b13a30ad9f9ba4b61d7075470?s=80&d=identicon',
webUrl: 'http://127.0.0.1:3000/angelina.herman',
},
];
const labels = [
{
id: 'gid://gitlab/GroupLabel/5',
title: 'Cosync',
color: '#34ebec',
description: null,
},
];
export const rawIssue = { export const rawIssue = {
title: 'Testing', title: 'Issue 1',
id: 'gid://gitlab/Issue/1', id: 'gid://gitlab/Issue/436',
iid: 1, iid: 27,
dueDate: null,
timeEstimate: 0,
weight: null,
confidential: false, confidential: false,
referencePath: 'gitlab-org/gitlab-test#1', referencePath: 'gitlab-org/gitlab-test#27',
path: '/gitlab-org/gitlab-test/-/issues/27',
labels: { labels: {
nodes: [ nodes: [
{ {
...@@ -115,23 +138,24 @@ export const rawIssue = { ...@@ -115,23 +138,24 @@ export const rawIssue = {
], ],
}, },
assignees: { assignees: {
nodes: [ nodes: assignees,
{ },
id: 1, epic: {
name: 'name', id: 'gid://gitlab/Epic/41',
username: 'username',
avatar_url: 'http://avatar_url',
},
],
}, },
}; };
export const mockIssue = { export const mockIssue = {
title: 'Testing', id: 'gid://gitlab/Issue/436',
id: 1, iid: 27,
iid: 1, title: 'Issue 1',
dueDate: null,
timeEstimate: 0,
weight: null,
confidential: false, confidential: false,
referencePath: 'gitlab-org/gitlab-test#1', referencePath: 'gitlab-org/gitlab-test#27',
path: '/gitlab-org/gitlab-test/-/issues/27',
assignees,
labels: [ labels: [
{ {
id: 1, id: 1,
...@@ -140,44 +164,64 @@ export const mockIssue = { ...@@ -140,44 +164,64 @@ export const mockIssue = {
description: 'testing', description: 'testing',
}, },
], ],
assignees: [ epic: {
{ id: 'gid://gitlab/Epic/41',
id: 1, },
name: 'name',
username: 'username',
avatar_url: 'http://avatar_url',
},
],
}; };
export const mockIssueWithModel = new ListIssue(mockIssue); export const mockIssueWithModel = new ListIssue(mockIssue);
export const mockIssue2 = { export const mockIssue2 = {
title: 'Planning', id: 'gid://gitlab/Issue/437',
id: 2, iid: 28,
iid: 2, title: 'Issue 2',
dueDate: null,
timeEstimate: 0,
weight: null,
confidential: false, confidential: false,
referencePath: 'gitlab-org/gitlab-test#2', referencePath: 'gitlab-org/gitlab-test#2',
labels: [ path: '/gitlab-org/gitlab-test/-/issues/28',
{ assignees,
id: 1, labels,
title: 'plan', epic: {
color: 'blue', id: 'gid://gitlab/Epic/40',
description: 'planning', },
},
],
assignees: [
{
id: 1,
name: 'name',
username: 'username',
avatar_url: 'http://avatar_url',
},
],
}; };
export const mockIssue2WithModel = new ListIssue(mockIssue2); export const mockIssue2WithModel = new ListIssue(mockIssue2);
export const mockIssue3 = {
id: 'gid://gitlab/Issue/438',
iid: 29,
title: 'Issue 3',
referencePath: '#29',
dueDate: null,
timeEstimate: 0,
weight: null,
confidential: false,
path: '/gitlab-org/gitlab-test/-/issues/28',
assignees,
labels,
epic: null,
};
export const mockIssue4 = {
id: 'gid://gitlab/Issue/439',
iid: 30,
title: 'Issue 4',
referencePath: '#30',
dueDate: null,
timeEstimate: 0,
weight: null,
confidential: false,
path: '/gitlab-org/gitlab-test/-/issues/28',
assignees,
labels,
epic: null,
};
export const mockIssues = [mockIssue, mockIssue2];
export const BoardsMockData = { export const BoardsMockData = {
GET: { GET: {
'/test/-/boards/1/lists/300/issues?id=300&page=1': { '/test/-/boards/1/lists/300/issues?id=300&page=1': {
...@@ -239,6 +283,7 @@ export const mockLists = [ ...@@ -239,6 +283,7 @@ export const mockLists = [
label: null, label: null,
assignee: null, assignee: null,
milestone: null, milestone: null,
loading: false,
}, },
{ {
id: 'gid://gitlab/List/2', id: 'gid://gitlab/List/2',
...@@ -255,9 +300,22 @@ export const mockLists = [ ...@@ -255,9 +300,22 @@ export const mockLists = [
}, },
assignee: null, assignee: null,
milestone: null, milestone: null,
loading: false,
}, },
]; ];
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 })),
); );
export const mockIssuesByListId = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue3.id, mockIssue4.id],
'gid://gitlab/List/2': mockIssues.map(({ id }) => id),
};
export const issues = {
[mockIssue.id]: mockIssue,
[mockIssue2.id]: mockIssue2,
[mockIssue3.id]: mockIssue3,
[mockIssue4.id]: mockIssue4,
};
...@@ -3,7 +3,6 @@ import { ...@@ -3,7 +3,6 @@ import {
mockListsWithModel, mockListsWithModel,
mockLists, mockLists,
mockIssue, mockIssue,
mockIssue2,
mockIssueWithModel, mockIssueWithModel,
mockIssue2WithModel, mockIssue2WithModel,
rawIssue, rawIssue,
...@@ -134,7 +133,7 @@ describe('createList', () => { ...@@ -134,7 +133,7 @@ describe('createList', () => {
{ backlog: true }, { backlog: true },
state, state,
[], [],
[{ type: 'addList', payload: { ...backlogList, id: 1 } }], [{ type: 'addList', payload: backlogList }],
done, done,
); );
}); });
...@@ -232,19 +231,15 @@ describe('deleteList', () => { ...@@ -232,19 +231,15 @@ describe('deleteList', () => {
expectNotImplemented(actions.deleteList); expectNotImplemented(actions.deleteList);
}); });
describe('fetchIssuesForList', () => {
expectNotImplemented(actions.fetchIssuesForList);
});
describe('moveIssue', () => { describe('moveIssue', () => {
const listIssues = { const listIssues = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], 'gid://gitlab/List/1': [436, 437],
'gid://gitlab/List/2': [], 'gid://gitlab/List/2': [],
}; };
const issues = { const issues = {
'1': mockIssueWithModel, '436': mockIssueWithModel,
'2': mockIssue2WithModel, '437': mockIssue2WithModel,
}; };
const state = { const state = {
...@@ -269,7 +264,7 @@ describe('moveIssue', () => { ...@@ -269,7 +264,7 @@ describe('moveIssue', () => {
testAction( testAction(
actions.moveIssue, actions.moveIssue,
{ {
issueId: mockIssue.id, issueId: '436',
issueIid: mockIssue.iid, issueIid: mockIssue.iid,
issuePath: mockIssue.referencePath, issuePath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1', fromListId: 'gid://gitlab/List/1',
...@@ -308,7 +303,7 @@ describe('moveIssue', () => { ...@@ -308,7 +303,7 @@ describe('moveIssue', () => {
testAction( testAction(
actions.moveIssue, actions.moveIssue,
{ {
issueId: mockIssue.id, issueId: '436',
issueIid: mockIssue.iid, issueIid: mockIssue.iid,
issuePath: mockIssue.referencePath, issuePath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1', fromListId: 'gid://gitlab/List/1',
......
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';
describe('Boards - Getters', () => { describe('Boards - Getters', () => {
describe('getLabelToggleState', () => { describe('getLabelToggleState', () => {
...@@ -115,4 +116,18 @@ describe('Boards - Getters', () => { ...@@ -115,4 +116,18 @@ describe('Boards - Getters', () => {
expect(getters.getActiveIssue(state)).toEqual(expected); expect(getters.getActiveIssue(state)).toEqual(expected);
}); });
}); });
describe('getIssues', () => {
const boardsState = {
issuesByListId: mockIssuesByListId,
issues,
};
it('returns issues for a given listId', () => {
const getIssueById = issueId => [mockIssue, mockIssue2].find(({ id }) => id === issueId);
expect(getters.getIssues(boardsState, { getIssueById })('gid://gitlab/List/2')).toEqual(
mockIssues,
);
});
});
}); });
...@@ -54,11 +54,11 @@ describe('Board Store Mutations', () => { ...@@ -54,11 +54,11 @@ describe('Board Store Mutations', () => {
}); });
}); });
describe('RECEIVE_LISTS', () => { describe('RECEIVE_BOARD_LISTS_SUCCESS', () => {
it('Should set boardLists to state', () => { it('Should set boardLists to state', () => {
const lists = [listObj, listObjDuplicate]; const lists = [listObj, listObjDuplicate];
mutations[types.RECEIVE_LISTS](state, lists); mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, lists);
expect(state.boardLists).toEqual(lists); expect(state.boardLists).toEqual(lists);
}); });
...@@ -145,6 +145,33 @@ describe('Board Store Mutations', () => { ...@@ -145,6 +145,33 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR); expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR);
}); });
describe('RECEIVE_ISSUES_FOR_LIST_SUCCESS', () => {
it('updates issuesByListId and issues on state', () => {
const listIssues = {
'gid://gitlab/List/1': [mockIssue.id],
};
const issues = {
'1': mockIssue,
};
state = {
...state,
isLoadingIssues: true,
issuesByListId: {},
issues: {},
boardLists: mockListsWithModel,
};
mutations.RECEIVE_ISSUES_FOR_LIST_SUCCESS(state, {
listIssues: { listData: listIssues, issues },
listId: 'gid://gitlab/List/1',
});
expect(state.issuesByListId).toEqual(listIssues);
expect(state.issues).toEqual(issues);
});
});
describe('REQUEST_ISSUES_FOR_ALL_LISTS', () => { describe('REQUEST_ISSUES_FOR_ALL_LISTS', () => {
it('sets isLoadingIssues to true', () => { it('sets isLoadingIssues to true', () => {
expect(state.isLoadingIssues).toBe(false); expect(state.isLoadingIssues).toBe(false);
...@@ -155,10 +182,28 @@ describe('Board Store Mutations', () => { ...@@ -155,10 +182,28 @@ describe('Board Store Mutations', () => {
}); });
}); });
describe('RECEIVE_ISSUES_FOR_LIST_FAILURE', () => {
it('sets error message', () => {
state = {
...state,
boardLists: mockListsWithModel,
error: undefined,
};
const listId = 'gid://gitlab/List/1';
mutations.RECEIVE_ISSUES_FOR_LIST_FAILURE(state, listId);
expect(state.error).toEqual(
'An error occurred while fetching the board issues. Please reload the page.',
);
});
});
describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => { describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => {
it('sets isLoadingIssues to false and updates issuesByListId object', () => { it('sets isLoadingIssues to false and updates issuesByListId object', () => {
const listIssues = { const listIssues = {
'': [mockIssue.id], 'gid://gitlab/List/1': [mockIssue.id],
}; };
const issues = { const issues = {
'1': mockIssue, '1': mockIssue,
...@@ -287,7 +332,7 @@ describe('Board Store Mutations', () => { ...@@ -287,7 +332,7 @@ describe('Board Store Mutations', () => {
describe('MOVE_ISSUE_SUCCESS', () => { describe('MOVE_ISSUE_SUCCESS', () => {
it('updates issue in issues state', () => { it('updates issue in issues state', () => {
const issues = { const issues = {
'1': { id: rawIssue.id }, '436': { id: rawIssue.id },
}; };
state = { state = {
...@@ -299,7 +344,7 @@ describe('Board Store Mutations', () => { ...@@ -299,7 +344,7 @@ describe('Board Store Mutations', () => {
issue: rawIssue, issue: rawIssue,
}); });
expect(state.issues).toEqual({ '1': { ...mockIssueWithModel, id: 1 } }); expect(state.issues).toEqual({ '436': { ...mockIssueWithModel, id: 436 } });
}); });
}); });
......
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