Commit 2b6d2bdd authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '218040-swimlanes-fetch-group-epics-graphql' into 'master'

Swimlanes - Fetch epics in GraphQL [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!39183
parents e8c7d7ed e803aca0
<script>
import { mapState } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
BoardColumn,
EpicsSwimlanes,
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
mixins: [glFeatureFlagMixin()],
props: {
......@@ -42,7 +43,7 @@ export default {
},
},
computed: {
...mapState(['isShowingEpicsSwimlanes', 'boardLists']),
...mapState(['isShowingEpicsSwimlanes', 'boardLists', 'error']),
isSwimlanesOn() {
return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes;
},
......@@ -52,6 +53,9 @@ export default {
<template>
<div>
<gl-alert v-if="error" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
<div
v-if="!isSwimlanesOn"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
......
import * as mutationTypes from './mutation_types';
import { __ } from '~/locale';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
......@@ -62,7 +63,7 @@ export default {
},
[mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE]: state => {
state.listIssueFetchFailure = true;
state.error = __('An error occurred while fetching the board issues. Please reload the page.');
state.isLoadingIssues = false;
},
......
......@@ -5,7 +5,10 @@ export default () => ({
boardType: null,
isShowingLabels: true,
activeId: inactiveId,
boardLists: [],
issuesByListId: {},
isLoadingIssues: false,
listIssueFetchFailure: false,
error: undefined,
// TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes: false,
});
fragment EpicNode on Epic {
id
iid
title
state
reference
webUrl
createdAt
closedAt
}
......@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board, default_enabled: true)
push_frontend_feature_flag(:boards_with_swimlanes, project, default_enabled: false)
end
private
......
<script>
import { GlButton, GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { __, n__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { formatDate } from '~/lib/utils/datetime_utility';
......@@ -27,10 +28,6 @@ export default {
type: Array,
required: true,
},
issues: {
type: Object,
required: true,
},
isLoadingIssues: {
type: Boolean,
required: false,
......@@ -51,6 +48,7 @@ export default {
};
},
computed: {
...mapGetters(['getIssuesByEpic']),
isOpen() {
return this.epic.state === statusType.open;
},
......@@ -70,8 +68,10 @@ export default {
return this.isOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
},
issuesCount() {
const { openedIssues, closedIssues } = this.epic.descendantCounts;
return openedIssues + closedIssues;
return this.lists.reduce(
(total, list) => total + this.getIssuesByEpic(list.id, this.epic.id).length,
0,
);
},
issuesCountTooltipText() {
return n__(`%d issue in this group`, `%d issues in this group`, this.issuesCount);
......@@ -90,13 +90,6 @@ export default {
},
},
methods: {
epicIssuesForList(listId) {
if (this.issues[listId]) {
return this.issues[listId].filter(issue => issue.epic && issue.epic.id === this.epic.id);
}
return [];
},
toggleExpanded() {
this.isExpanded = !this.isExpanded;
},
......@@ -156,10 +149,12 @@ export default {
v-for="list in lists"
:key="`${list.id}-issues`"
:list="list"
:issues="epicIssuesForList(list.id)"
:issues="getIssuesByEpic(list.id, epic.id)"
:is-loading="isLoadingIssues"
:disabled="disabled"
:root-path="rootPath"
:epic-id="epic.id"
:epic-is-confidential="epic.confidential"
/>
</div>
</div>
......
<script>
import { mapActions, mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import { n__ } from '~/locale';
......@@ -45,9 +45,10 @@ export default {
},
},
computed: {
...mapState(['epics', 'issuesByListId', 'isLoadingIssues']),
...mapState(['epics', 'isLoadingIssues']),
...mapGetters(['unassignedIssues']),
unassignedIssuesCount() {
return this.lists.reduce((total, list) => total + this.unassignedIssues(list).length, 0);
return this.lists.reduce((total, list) => total + this.unassignedIssues(list.id).length, 0);
},
unassignedIssuesCountTooltipText() {
return n__(`%d unassigned issue`, `%d unassigned issues`, this.unassignedIssuesCount);
......@@ -58,12 +59,6 @@ export default {
},
methods: {
...mapActions(['fetchIssuesForAllLists']),
unassignedIssues(list) {
if (this.issuesByListId[list.id]) {
return this.issuesByListId[list.id].filter(i => i.epic === null);
}
return [];
},
},
};
</script>
......@@ -99,7 +94,6 @@ export default {
:key="epic.id"
:epic="epic"
:lists="lists"
:issues="issuesByListId"
:is-loading-issues="isLoadingIssues"
:disabled="disabled"
:root-path="rootPath"
......@@ -129,7 +123,7 @@ export default {
v-for="list in lists"
:key="`${list.id}-issues`"
:list="list"
:issues="unassignedIssues(list)"
:issues="unassignedIssues(list.id)"
:group-id="groupId"
:is-unassigned-issues-lane="true"
:is-loading="isLoadingIssues"
......
......@@ -22,6 +22,7 @@ export default {
issues: {
type: Array,
required: true,
default: () => [],
},
groupId: {
type: Number,
......
query groupEpicsEE($fullPath: ID!) {
group(fullPath: $fullPath) {
epics(first: 10) {
nodes {
id
iid
title
state
reference
webUrl
createdAt
closedAt
descendantCounts {
openedIssues
closedIssues
}
issues {
nodes {
id
iid
title
referencePath: reference
dueDate
timeEstimate
weight
confidential
path: webUrl
assignees {
nodes {
id
username
name
avatar: avatarUrl
webUrl
}
}
labels {
nodes {
id
title
color
description
}
}
}
}
}
}
}
}
#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
#import "~/graphql_shared/fragments/epic.fragment.graphql"
query GroupBoardEE($fullPath: ID!, $boardId: ID!) {
group(fullPath: $fullPath) {
......@@ -8,6 +9,11 @@ query GroupBoardEE($fullPath: ID!, $boardId: ID!) {
...BoardListFragment
}
}
epics {
nodes {
...EpicNode
}
}
}
}
}
#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
#import "~/graphql_shared/fragments/epic.fragment.graphql"
query ProjectBoardEE($fullPath: ID!, $boardId: ID!) {
project(fullPath: $fullPath) {
board(id: $boardId) {
lists {
nodes {
...BoardListFragment
}
}
epics {
nodes {
...EpicNode
}
}
}
}
}
import axios from 'axios';
import { sortBy } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store';
import actionsCE from '~/boards/stores/actions';
import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types';
import createDefaultClient from '~/lib/graphql';
import epicsSwimlanes from '../queries/epics_swimlanes.query.graphql';
import groupEpics from '../queries/group_epics.query.graphql';
import { BoardType } from '~/boards/constants';
import groupEpicsSwimlanesQuery from '../queries/group_epics_swimlanes.query.graphql';
import projectEpicsSwimlanesQuery from '../queries/project_epics_swimlanes.query.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
......@@ -19,29 +20,12 @@ const gqlClient = createDefaultClient();
const fetchEpicsSwimlanes = ({ endpoints, boardType }) => {
const { fullPath, boardId } = endpoints;
const query = epicsSwimlanes;
const variables = {
fullPath,
boardId: `gid://gitlab/Board/${boardId}`,
};
const query =
boardType === BoardType.group ? groupEpicsSwimlanesQuery : projectEpicsSwimlanesQuery;
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
const { lists } = data[boardType]?.board;
return lists?.nodes;
});
};
const fetchEpics = ({ endpoints }) => {
const { fullPath } = endpoints;
const query = groupEpics;
const variables = {
fullPath,
boardId: `gid://gitlab/Board/${boardId}`,
};
return gqlClient
......@@ -50,9 +34,8 @@ const fetchEpics = ({ endpoints }) => {
variables,
})
.then(({ data }) => {
const { group } = data;
const epics = group?.epics.nodes || [];
return epics.map(e => ({
const { epics, lists } = data[boardType]?.board;
const epicsFormatted = epics.nodes.map(e => ({
...e,
issues: (e?.issues?.nodes || []).map(i => ({
...i,
......@@ -60,6 +43,10 @@ const fetchEpics = ({ endpoints }) => {
assignees: i.assignees?.nodes || [],
})),
}));
return {
epics: epicsFormatted,
lists: lists.nodes,
};
});
};
......@@ -108,8 +95,8 @@ export default {
commit(types.TOGGLE_EPICS_SWIMLANES);
if (state.isShowingEpicsSwimlanes) {
Promise.all([fetchEpicsSwimlanes(state), fetchEpics(state)])
.then(([lists, epics]) => {
fetchEpicsSwimlanes(state)
.then(({ lists, epics }) => {
if (lists) {
let boardLists = lists.map(list =>
boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }),
......
......@@ -2,4 +2,15 @@ import gettersCE from '~/boards/stores/getters';
export default {
...gettersCE,
getIssues: state => listId => {
return state.issuesByListId[listId] || [];
},
getIssuesByEpic: (state, getters) => (listId, epicId) => {
return getters.getIssues(listId).filter(issue => issue.epic && issue.epic.id === epicId);
},
unassignedIssues: (state, getters) => listId => {
return getters.getIssues(listId).filter(i => i.epic === null);
},
};
import mutationsCE from '~/boards/stores/mutations';
import { __ } from '~/locale';
import * as mutationTypes from './mutation_types';
const notImplemented = () => {
......@@ -75,7 +76,9 @@ export default {
},
[mutationTypes.RECEIVE_SWIMLANES_FAILURE]: state => {
state.epicsSwimlanesFetchFailure = true;
state.error = __(
'An error occurred while fetching the board swimlanes. Please reload the page.',
);
state.epicsSwimlanesFetchInProgress = false;
},
......
......@@ -5,8 +5,6 @@ export default () => ({
isShowingEpicsSwimlanes: false,
epicsSwimlanesFetchInProgress: false,
epicsSwimlanesFetchFailure: false,
epicsSwimlanes: {},
epics: {},
boardLists: [],
issuesByEpicId: {},
});
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import getters from 'ee/boards/stores/getters';
import { mockListsWithModel, mockIssuesByListId } from '../mock_data';
import BoardContent from '~/boards/components/board_content.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('BoardContent', () => {
let wrapper;
const defaultState = {
isShowingEpicsSwimlanes: false,
boardLists: mockListsWithModel,
error: undefined,
issuesByListId: mockIssuesByListId,
};
const createStore = (state = defaultState) => {
return new Vuex.Store({
state,
actions: {
fetchIssuesForAllLists: () => {},
},
getters,
});
};
const createComponent = state => {
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
localVue,
propsData: {
lists: mockListsWithModel,
canAdminList: true,
groupId: 1,
disabled: false,
issueLinkBase: '/',
rootPath: '/',
boardId: '1',
},
store,
provide: {
glFeatures: { boardsWithSwimlanes: true },
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('Swimlanes off', () => {
beforeEach(() => {
createComponent();
});
it('renders a BoardColumn component per list', () => {
expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length);
});
it('does not display EpicsSwimlanes component', () => {
expect(wrapper.contains(EpicsSwimlanes)).toBe(false);
expect(wrapper.contains(GlAlert)).toBe(false);
});
});
describe('Swimlanes on', () => {
beforeEach(() => {
createComponent({ isShowingEpicsSwimlanes: true });
});
it('does not display BoardColumn component', () => {
expect(wrapper.findAll(BoardColumn)).toHaveLength(0);
});
it('displays EpicsSwimlanes component', () => {
expect(wrapper.contains('.board-swimlanes')).toBe(true);
expect(wrapper.contains(GlAlert)).toBe(false);
});
it('displays alert if an error occurs when fetching swimlanes', () => {
createComponent({
isShowingEpicsSwimlanes: true,
error: 'An error occurred while fetching the board swimlanes. Please reload the page.',
});
expect(wrapper.contains(GlAlert)).toBe(true);
});
});
});
import Vue from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import EpicLane from 'ee/boards/components/epic_lane.vue';
import IssuesLaneList from 'ee/boards/components/issues_lane_list.vue';
import { GlIcon } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import { mockEpic, mockLists, mockIssues } from '../mock_data';
import List from '~/boards/models/list';
import axios from '~/lib/utils/axios_utils';
import getters from 'ee/boards/stores/getters';
import { mockEpic, mockListsWithModel, mockIssuesByListId } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('EpicLane', () => {
let wrapper;
let axiosMock;
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: mockIssues });
});
const createStore = () => {
return new Vuex.Store({
state: {
issuesByListId: mockIssuesByListId,
},
getters,
});
};
const createComponent = (props = {}) => {
const issues = mockLists.reduce((map, list) => {
return {
...map,
[list.id]: mockIssues,
};
}, {});
const store = createStore();
const defaultProps = {
epic: mockEpic,
lists: mockLists.map(listMock => Vue.observable(new List(listMock))),
issues,
lists: mockListsWithModel,
disabled: false,
rootPath: '/',
};
wrapper = shallowMount(EpicLane, {
localVue,
propsData: {
...defaultProps,
...props,
},
store,
});
};
afterEach(() => {
axiosMock.restore();
wrapper.destroy();
});
......@@ -61,8 +59,8 @@ describe('EpicLane', () => {
expect(wrapper.find(GlIcon).attributes('aria-label')).toEqual('Closed');
});
it('displays total count of issues in epic', () => {
expect(wrapper.find('[data-testid="epic-lane-issue-count"]').text()).toContain(5);
it('displays count of issues in epic which belong to board', () => {
expect(wrapper.find('[data-testid="epic-lane-issue-count"]').text()).toContain(2);
});
it('displays 2 icons', () => {
......
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import EpicsSwimlanes from 'ee/boards/components/epics_swimlanes.vue';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import EpicLane from 'ee/boards/components/epic_lane.vue';
import IssueLaneList from 'ee/boards/components/issues_lane_list.vue';
import getters from 'ee/boards/stores/getters';
import { GlIcon } from '@gitlab/ui';
import { mockListsWithModel, mockEpics, mockIssuesByListId } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('EpicsSwimlanes', () => {
let wrapper;
const createStore = () => {
return new Vuex.Store({
actions: {
fetchIssuesForAllLists: jest.fn(),
},
state: {
epics: mockEpics,
isLoadingIssues: false,
issuesByListId: mockIssuesByListId,
},
getters,
});
};
const createComponent = () => {
const store = createStore();
const defaultProps = {
lists: mockListsWithModel,
boardId: '1',
disabled: false,
rootPath: '/',
};
wrapper = shallowMount(EpicsSwimlanes, {
localVue,
propsData: defaultProps,
store,
});
};
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('displays BoardListHeader components for lists', () => {
expect(wrapper.findAll(BoardListHeader)).toHaveLength(2);
});
it('displays EpicLane components for epic', () => {
expect(wrapper.findAll(EpicLane)).toHaveLength(5);
});
it('displays IssueLaneList component', () => {
expect(wrapper.contains(IssueLaneList)).toBe(true);
});
it('displays issues icon and count for unassigned issue', () => {
expect(wrapper.find(GlIcon).props('name')).toEqual('issues');
expect(wrapper.find('[data-testid="issues-lane-issue-count"').text()).toEqual('2');
});
});
});
import Vue from 'vue';
import List from '~/boards/models/list';
export const mockLists = [
{
id: 1,
id: 'gid://gitlab/List/1',
title: 'Backlog',
position: null,
listType: 'backlog',
......@@ -11,7 +14,7 @@ export const mockLists = [
milestone: null,
},
{
id: 10,
id: 'gid://gitlab/List/2',
title: 'To Do',
position: 0,
listType: 'label',
......@@ -29,6 +32,10 @@ export const mockLists = [
},
];
export const mockListsWithModel = mockLists.map(listMock =>
Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
);
const defaultDescendantCounts = {
openedIssues: 0,
closedIssues: 0,
......@@ -53,7 +60,7 @@ const labels = [
},
];
const mockIssue = {
export const mockIssue = {
id: 'gid://gitlab/Issue/436',
iid: 27,
title: 'Issue 1',
......@@ -65,27 +72,62 @@ const mockIssue = {
path: '/gitlab-org/gitlab-test/-/issues/27',
assignees,
labels,
epic: {
id: 'gid://gitlab/Epic/41',
},
};
export const mockIssues = [
mockIssue,
{
id: 'gid://gitlab/Issue/437',
iid: 28,
title: 'Issue 2',
referencePath: '#28',
dueDate: null,
timeEstimate: 0,
weight: null,
confidential: false,
path: '/gitlab-org/gitlab-test/-/issues/28',
assignees,
labels,
export const mockIssue2 = {
id: 'gid://gitlab/Issue/437',
iid: 28,
title: 'Issue 2',
referencePath: '#28',
dueDate: null,
timeEstimate: 0,
weight: null,
confidential: false,
path: '/gitlab-org/gitlab-test/-/issues/28',
assignees,
labels,
epic: {
id: 'gid://gitlab/Epic/40',
},
];
};
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 mockEpic = {
id: 1,
id: 'gid://gitlab/Epic/41',
iid: 1,
title: 'Epic title',
state: 'opened',
......@@ -99,7 +141,7 @@ export const mockEpic = {
export const mockEpics = [
{
id: 41,
id: 'gid://gitlab/Epic/41',
iid: 2,
description: null,
title: 'Another marketing',
......@@ -116,7 +158,7 @@ export const mockEpics = [
},
},
{
id: 40,
id: 'gid://gitlab/Epic/40',
iid: 1,
description: null,
title: 'Marketing epic',
......@@ -130,7 +172,7 @@ export const mockEpics = [
hasParent: false,
},
{
id: 39,
id: 'gid://gitlab/Epic/39',
iid: 12,
description: null,
title: 'Epic with end in first timeframe month',
......@@ -144,7 +186,7 @@ export const mockEpics = [
hasParent: false,
},
{
id: 38,
id: 'gid://gitlab/Epic/38',
iid: 11,
description: null,
title: 'Epic with end date out of range',
......@@ -158,7 +200,7 @@ export const mockEpics = [
hasParent: false,
},
{
id: 37,
id: 'gid://gitlab/Epic/37',
iid: 10,
description: null,
title: 'Epic with timeline in same month',
......@@ -172,3 +214,8 @@ export const mockEpics = [
hasParent: false,
},
];
export const mockIssuesByListId = {
'gid://gitlab/List/1': [mockIssue, mockIssue3, mockIssue4],
'gid://gitlab/List/2': mockIssues,
};
import getters from 'ee/boards/stores/getters';
import { mockIssue, mockIssue3, mockIssue4, mockIssues, mockIssuesByListId } from '../mock_data';
describe('EE Boards Store Getters', () => {
const boardsState = {
issuesByListId: mockIssuesByListId,
};
describe('getIssues', () => {
it('returns issues for a given listId', () => {
expect(getters.getIssues(boardsState)('gid://gitlab/List/2')).toEqual(mockIssues);
});
});
describe('getIssuesByEpic', () => {
it('returns issues for a given listId and epicId', () => {
const getIssues = () => mockIssues;
expect(
getters.getIssuesByEpic(boardsState, { getIssues })(
'gid://gitlab/List/2',
'gid://gitlab/Epic/41',
),
).toEqual([mockIssue]);
});
});
describe('unassignedIssues', () => {
it('returns issues for a given listId and epicId', () => {
const getIssues = () => [mockIssue, mockIssue3, mockIssue4];
expect(getters.unassignedIssues(boardsState, { getIssues })('gid://gitlab/List/1')).toEqual([
mockIssue3,
mockIssue4,
]);
});
});
});
......@@ -118,16 +118,18 @@ describe('RECEIVE_BOARD_LISTS_SUCCESS', () => {
});
describe('RECEIVE_SWIMLANES_FAILURE', () => {
it('sets epicsSwimlanesFetchInProgress to false and epicsSwimlanesFetchFailure to true', () => {
it('sets epicsSwimlanesFetchInProgress to false and sets error message', () => {
const state = {
epicsSwimlanesFetchInProgress: true,
epicsSwimlanesFetchFailure: false,
error: undefined,
};
mutations.RECEIVE_SWIMLANES_FAILURE(state);
expect(state.epicsSwimlanesFetchInProgress).toBe(false);
expect(state.epicsSwimlanesFetchFailure).toBe(true);
expect(state.error).toEqual(
'An error occurred while fetching the board swimlanes. Please reload the page.',
);
});
});
......
......@@ -2657,9 +2657,15 @@ msgstr ""
msgid "An error occurred while fetching the Service Desk address."
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 try again."
msgstr ""
msgid "An error occurred while fetching the board swimlanes. Please reload the page."
msgstr ""
msgid "An error occurred while fetching the builds."
msgstr ""
......
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import { mockListsWithModel } from '../mock_data';
import BoardContent from '~/boards/components/board_content.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('BoardContent', () => {
let wrapper;
const defaultState = {
isShowingEpicsSwimlanes: false,
boardLists: mockListsWithModel,
error: undefined,
};
const createStore = (state = defaultState) => {
return new Vuex.Store({
state,
actions: {
fetchIssuesForAllLists: () => {},
},
});
};
const createComponent = state => {
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
localVue,
propsData: {
lists: mockListsWithModel,
canAdminList: true,
groupId: 1,
disabled: false,
issueLinkBase: '/',
rootPath: '/',
boardId: '1',
},
store,
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a BoardColumn component per list', () => {
expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length);
});
it('does not display EpicsSwimlanes component', () => {
expect(wrapper.contains(EpicsSwimlanes)).toBe(false);
expect(wrapper.contains(GlAlert)).toBe(false);
});
});
import Vue from 'vue';
import List from '~/boards/models/list';
import boardsStore from '~/boards/stores/boards_store';
export const boardObj = {
......@@ -165,3 +167,36 @@ export const setMockEndpoints = (opts = {}) => {
boardId,
});
};
export const mockLists = [
{
id: 'gid://gitlab/List/1',
title: 'Backlog',
position: null,
listType: 'backlog',
collapsed: false,
label: null,
assignee: null,
milestone: null,
},
{
id: 'gid://gitlab/List/2',
title: 'To Do',
position: 0,
listType: 'label',
collapsed: false,
label: {
id: 'gid://gitlab/GroupLabel/121',
title: 'To Do',
color: '#F0AD4E',
textColor: '#FFFFFF',
description: null,
},
assignee: null,
milestone: null,
},
];
export const mockListsWithModel = mockLists.map(listMock =>
Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
);
......@@ -113,6 +113,23 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.REQUEST_ADD_ISSUE);
});
describe('RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE', () => {
it('sets isLoadingIssues to false and sets error message', () => {
state = {
...state,
isLoadingIssues: true,
error: undefined,
};
mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE(state);
expect(state.isLoadingIssues).toBe(false);
expect(state.error).toEqual(
'An error occurred while fetching the board issues. Please reload the page.',
);
});
});
describe('RECEIVE_ADD_ISSUE_SUCCESS', () => {
expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_SUCCESS);
});
......
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