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