Commit 5ddfe091 authored by Simon Knox's avatar Simon Knox Committed by Savas Vedova

Split new column form into data/display components

Makes it easier to extend for different types of list
in EE
parent cbd3726e
......@@ -30,7 +30,7 @@ export default {
},
computed: {
...mapState(['labels', 'labelsLoading']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL', 'isEpicBoard']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
selectedLabel() {
if (!this.selectedId) {
return null;
......@@ -47,7 +47,7 @@ export default {
methods: {
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
highlight(listId) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
if (this.shouldUseGraphQL) {
this.highlightList(listId);
} else {
const list = boardsStore.state.lists.find(({ id }) => id === listId);
......@@ -70,7 +70,7 @@ export default {
return;
}
if (this.shouldUseGraphQL || this.isEpicBoard) {
if (this.shouldUseGraphQL) {
this.createList({ labelId: this.selectedId });
} else {
const listObj = {
......@@ -118,13 +118,17 @@ export default {
</template>
<template slot="items">
<gl-form-radio-group v-model="selectedId" class="gl-overflow-y-auto gl-px-5 gl-pt-3">
<gl-form-radio-group
v-if="labels.length > 0"
v-model="selectedId"
class="gl-overflow-y-auto gl-px-5 gl-pt-3"
>
<label
v-for="label in labels"
:key="label.id"
class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
>
<gl-form-radio :value="label.id" class="gl-mb-0 gl-mr-3" />
<gl-form-radio :value="label.id" class="gl-mb-0" />
<span
class="dropdown-label-box gl-top-0"
:style="{
......
......@@ -5,10 +5,11 @@ import { __ } from '~/locale';
export default {
i18n: {
add: __('Add'),
add: __('Add to board'),
cancel: __('Cancel'),
newList: __('New list'),
noneSelected: __('None'),
noResults: __('No matching results'),
selected: __('Selected'),
},
components: {
......@@ -40,6 +41,11 @@ export default {
default: null,
},
},
data() {
return {
searchValue: '',
};
},
methods: {
...mapActions(['setAddColumnFormVisibility']),
},
......@@ -83,6 +89,7 @@ export default {
>
<gl-search-box-by-type
id="board-available-column-entities"
v-model="searchValue"
debounce="250"
:placeholder="searchPlaceholder"
@input="$emit('filter-items', $event)"
......@@ -97,10 +104,12 @@ export default {
</gl-skeleton-loader>
</div>
<slot v-else name="items"></slot>
<slot v-else name="items">
<p class="gl-mx-5">{{ $options.i18n.noResults }}</p>
</slot>
</div>
<div
class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10"
class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
>
<gl-button
data-testid="cancelAddNewColumn"
......@@ -111,7 +120,7 @@ export default {
<gl-button
data-testid="addNewColumnButton"
:disabled="!selectedId"
variant="success"
variant="confirm"
class="gl-mr-4"
@click="$emit('add-list')"
>{{ $options.i18n.add }}</gl-button
......
......@@ -3,10 +3,10 @@ import { GlAlert } from '@gitlab/ui';
import { sortBy } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
import defaultSortableConfig from '~/sortable/sortable_config';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardAddNewColumn from './board_add_new_column.vue';
import BoardColumn from './board_column.vue';
import BoardColumnDeprecated from './board_column_deprecated.vue';
......
<script>
import {
GlFormGroup,
GlFormRadio,
GlFormRadioGroup,
GlFormSelect,
GlLabel,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
export default {
i18n: {
listType: __('List type'),
labelListDescription: __('A label list displays issues with the selected label.'),
milestoneListDescription: __('A milestone list displays issues in the selected milestone.'),
selectLabel: __('Select label'),
selectMilestone: __('Select milestone'),
searchLabels: __('Search labels'),
searchMilestones: __('Search milestones'),
},
columnTypes: [
{ value: ListType.label, text: __('Label') },
{ value: ListType.milestone, text: __('Milestone') },
],
components: {
BoardAddNewColumnForm,
GlFormGroup,
GlFormRadio,
GlFormRadioGroup,
GlFormSelect,
GlLabel,
},
directives: {
GlTooltip,
},
inject: ['scopedLabelsAvailable'],
data() {
return {
selectedId: null,
columnType: ListType.label,
};
},
computed: {
...mapState(['labels', 'labelsLoading', 'milestones', 'milestonesLoading']),
...mapGetters(['getListByTypeId', 'shouldUseGraphQL', 'isEpicBoard']),
items() {
if (this.labelTypeSelected) {
return this.labels;
}
if (this.milestoneTypeSelected) {
return this.milestones;
}
return [];
},
labelTypeSelected() {
return this.columnType === ListType.label;
},
milestoneTypeSelected() {
return this.columnType === ListType.milestone;
},
selectedLabel() {
if (!this.labelTypeSelected) {
return null;
}
return this.labels.find(({ id }) => id === this.selectedId);
},
selectedMilestone() {
if (!this.milestoneTypeSelected) {
return null;
}
return this.milestones.find(({ id }) => id === this.selectedId);
},
selectedItem() {
if (!this.selectedId) {
return null;
}
if (this.labelTypeSelected) {
return this.selectedLabel;
}
if (this.milestoneTypeSelected) {
return this.selectedMilestone;
}
return null;
},
columnForSelected() {
if (!this.columnType) {
return false;
}
const key = `${this.columnType}Id`;
return this.getListByTypeId({
[key]: this.selectedId,
});
},
loading() {
if (this.columnType === ListType.label) {
return this.labelsLoading;
}
if (this.columnType === ListType.milestone) {
return this.milestonesLoading;
}
return false;
},
formDescription() {
if (this.labelTypeSelected) {
return this.$options.i18n.labelListDescription;
}
if (this.milestoneTypeSelected) {
return this.$options.i18n.milestoneListDescription;
}
return null;
},
searchLabel() {
if (this.labelTypeSelected) {
return this.$options.i18n.selectLabel;
}
if (this.milestoneTypeSelected) {
return this.$options.i18n.selectMilestone;
}
return null;
},
searchPlaceholder() {
if (this.labelTypeSelected) {
return this.$options.i18n.searchLabels;
}
if (this.milestoneTypeSelected) {
return this.$options.i18n.searchMilestones;
}
return null;
},
},
created() {
this.filterItems();
},
methods: {
...mapActions([
'createList',
'fetchLabels',
'highlightList',
'setAddColumnFormVisibility',
'fetchMilestones',
]),
highlight(listId) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
this.highlightList(listId);
} else {
const list = boardsStore.state.lists.find(({ id }) => id === listId);
list.highlighted = true;
setTimeout(() => {
list.highlighted = false;
}, 2000);
}
},
addList() {
if (!this.selectedItem) {
return;
}
this.setAddColumnFormVisibility(false);
if (this.columnForSelected) {
const listId = this.columnForSelected.id;
this.highlight(listId);
return;
}
if (this.shouldUseGraphQL || this.isEpicBoard) {
// eslint-disable-next-line @gitlab/require-i18n-strings
this.createList({ [`${this.columnType}Id`]: this.selectedId });
} else {
const { length } = boardsStore.state.lists;
const position = this.hideClosed ? length - 1 : length - 2;
const listObj = {
// eslint-disable-next-line @gitlab/require-i18n-strings
[`${this.columnType}Id`]: getIdFromGraphQLId(this.selectedId),
title: this.selectedItem.title,
position,
list_type: this.columnType,
};
if (this.labelTypeSelected) {
listObj.label = this.selectedLabel;
} else if (this.milestoneTypeSelected) {
listObj.milestone = {
...this.selectedMilestone,
id: getIdFromGraphQLId(this.selectedMilestone.id),
};
}
boardsStore.new(listObj);
}
},
filterItems(searchTerm) {
switch (this.columnType) {
case ListType.milestone:
this.fetchMilestones(searchTerm);
break;
case ListType.label:
default:
this.fetchLabels(searchTerm);
}
},
showScopedLabels(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
},
setColumnType() {
this.selectedId = null;
this.filterItems();
},
},
};
</script>
<template>
<board-add-new-column-form
:loading="loading"
:form-description="formDescription"
:search-label="searchLabel"
:search-placeholder="searchPlaceholder"
:selected-id="selectedId"
@filter-items="filterItems"
@add-list="addList"
>
<template slot="select-list-type">
<gl-form-group
v-if="!isEpicBoard"
:label="$options.i18n.listType"
class="gl-px-5 gl-py-0 gl-mt-5"
label-for="list-type"
>
<gl-form-select
id="list-type"
v-model="columnType"
:options="$options.columnTypes"
@change="setColumnType"
/>
</gl-form-group>
</template>
<template slot="selected">
<div v-if="selectedLabel">
<gl-label
v-gl-tooltip
:title="selectedLabel.title"
:description="selectedLabel.description"
:background-color="selectedLabel.color"
:scoped="showScopedLabels(selectedLabel)"
/>
</div>
<div v-else-if="selectedMilestone" class="gl-text-truncate">
{{ selectedMilestone.title }}
</div>
</template>
<template slot="items">
<gl-form-radio-group
v-if="items.length > 0"
v-model="selectedId"
class="gl-overflow-y-auto gl-px-5 gl-pt-3"
>
<label
v-for="item in items"
:key="item.id"
class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
>
<gl-form-radio :value="item.id" class="gl-mb-0" />
<span
v-if="labelTypeSelected"
class="dropdown-label-box gl-top-0"
:style="{
backgroundColor: item.color,
}"
></span>
<span>{{ item.title }}</span>
</label>
</gl-form-radio-group>
</template>
</board-add-new-column-form>
</template>
......@@ -2,9 +2,9 @@
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import { isListDraggable } from '~/boards/boards_util';
import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue';
import { n__ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
import { DRAGGABLE_TAG } from '../constants';
......@@ -48,10 +48,9 @@ export default {
return (listId) => this.getUnassignedIssues(listId);
},
unassignedIssuesCount() {
return this.lists.reduce(
(total, list) => total + this.listsFlags[list.id]?.unassignedIssuesCount || 0,
0,
);
return this.lists.reduce((total, list) => {
return total + (this.listsFlags[list.id]?.unassignedIssuesCount || 0);
}, 0);
},
unassignedIssuesCountTooltipText() {
return n__(`%d unassigned issue`, `%d unassigned issues`, this.unassignedIssuesCount);
......
query GroupBoardMilestones($fullPath: ID!, $searchTerm: String) {
group(fullPath: $fullPath) {
# TODO: add includeAncestors: https://gitlab.com/gitlab-org/gitlab/-/issues/323433
milestones(searchTitle: $searchTerm) {
nodes {
id
title
}
}
}
}
query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String) {
project(fullPath: $fullPath) {
milestones(searchTitle: $searchTerm, includeAncestors: true) {
nodes {
id
title
}
}
}
}
......@@ -33,11 +33,13 @@ import epicQuery from '../graphql/epic.query.graphql';
import createEpicBoardListMutation from '../graphql/epic_board_list_create.mutation.graphql';
import epicBoardListsQuery from '../graphql/epic_board_lists.query.graphql';
import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
import issueSetEpicMutation from '../graphql/issue_set_epic.mutation.graphql';
import issueSetWeightMutation from '../graphql/issue_set_weight.mutation.graphql';
import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql';
import listsEpicsQuery from '../graphql/lists_epics.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import updateBoardEpicUserPreferencesMutation from '../graphql/updateBoardEpicUserPreferences.mutation.graphql';
import boardsStoreEE from './boards_store_ee';
......@@ -557,6 +559,50 @@ export default {
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
},
fetchMilestones({ state, commit }, searchTerm) {
commit(types.RECEIVE_MILESTONES_REQUEST);
const { fullPath, boardType } = state;
const variables = {
fullPath,
searchTerm,
};
let query;
if (boardType === BoardType.project) {
query = projectBoardMilestonesQuery;
}
if (boardType === BoardType.group) {
query = groupBoardMilestonesQuery;
}
if (!query) {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('Unknown board type');
}
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
const errors = data[boardType]?.errors;
const milestones = data[boardType]?.milestones.nodes;
if (errors?.[0]) {
throw new Error(errors[0]);
}
commit(types.RECEIVE_MILESTONES_SUCCESS, milestones);
})
.catch((e) => {
commit(types.RECEIVE_MILESTONES_FAILURE);
throw e;
});
},
createList: ({ getters, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
if (!getters.isEpicBoard) {
dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
......
import { issuableTypes } from '~/boards/constants';
import { find } from 'lodash';
import { issuableTypes, ListType } from '~/boards/constants';
import gettersCE from '~/boards/stores/getters';
export default {
......@@ -7,6 +8,32 @@ export default {
isSwimlanesOn: (state) => {
return Boolean(gon?.features?.swimlanes && state.isShowingEpicsSwimlanes);
},
getListByTypeId: (state) => ({ assigneeId, labelId, milestoneId }) => {
if (assigneeId) {
return find(
state.boardLists,
(l) => l.listType === ListType.assignee && l.assignee?.id === assigneeId,
);
}
if (labelId) {
return find(
state.boardLists,
(l) => l.listType === ListType.label && l.label?.id === labelId,
);
}
if (milestoneId) {
return find(
state.boardLists,
(l) => l.listType === ListType.milestone && l.milestone?.id === milestoneId,
);
}
return null;
},
getIssuesByEpic: (state, getters) => (listId, epicId) => {
return getters
.getBoardItemsByList(listId)
......
......@@ -33,3 +33,6 @@ export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
export const SET_BOARD_EPIC_USER_PREFERENCES = 'SET_BOARD_EPIC_USER_PREFERENCES';
export const RECEIVE_MILESTONES_REQUEST = 'RECEIVE_MILESTONES_REQUEST';
export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
export const RECEIVE_MILESTONES_FAILURE = 'RECEIVE_MILESTONES_FAILURE';
......@@ -3,7 +3,7 @@ import Vue from 'vue';
import { moveIssueListHelper } from '~/boards/boards_util';
import { issuableTypes } from '~/boards/constants';
import mutationsCE, { addIssueToList, removeIssueFromList } from '~/boards/stores/mutations';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
import { ErrorMessages } from '../constants';
import * as mutationTypes from './mutation_types';
......@@ -189,4 +189,18 @@ export default {
Vue.set(epic, 'userPreferences', userPreferences);
}
},
[mutationTypes.RECEIVE_MILESTONES_REQUEST](state) {
state.milestonesLoading = true;
},
[mutationTypes.RECEIVE_MILESTONES_SUCCESS](state, milestones) {
state.milestones = milestones;
state.milestonesLoading = false;
},
[mutationTypes.RECEIVE_MILESTONES_FAILURE](state) {
state.milestonesLoading = false;
state.error = __('Failed to load milestones.');
},
};
......@@ -12,4 +12,6 @@ export default () => ({
epicsCacheById: {},
epicFetchInProgress: false,
epicsFlags: {},
milestones: [],
milestonesLoading: false,
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User adds milestone lists', :js do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:group_board) { create(:board, group: group) }
let_it_be(:project_board) { create(:board, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:milestone) { create(:milestone, group: group) }
let_it_be(:group_backlog_list) { create(:backlog_list, board: group_board) }
let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
before_all do
project.add_maintainer(user)
group.add_owner(user)
end
where(:board_type, :graphql_board_lists_enabled) do
:project | true
:project | false
:group | true
:group | false
end
with_them do
before do
stub_licensed_features(board_milestone_lists: true)
sign_in(user)
set_cookie('sidebar_collapsed', 'true')
stub_feature_flags(
graphql_board_lists: graphql_board_lists_enabled,
board_new_list: true
)
if board_type == :project
visit project_board_path(project, project_board)
elsif board_type == :group
visit group_board_path(group, group_board)
end
wait_for_all_requests
end
it 'creates milestone column' do
click_button button_text
wait_for_all_requests
select('Milestone', from: 'List type')
add_milestone_list(milestone)
wait_for_all_requests
expect(page).to have_selector('.board', text: milestone.title)
expect(find('.board:nth-child(2) .board-card')).to have_content(issue.title)
end
end
def add_milestone_list(milestone)
page.within('.board-add-new-list') do
find('label', text: milestone.title).click
click_button 'Add'
end
end
def button_text
'Create list'
end
end
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardAddNewColumn from 'ee/boards/components/board_add_new_column.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import defaultState from '~/boards/stores/state';
import { mockLists } from '../mock_data';
const mockLabelList = mockLists[1];
Vue.use(Vuex);
describe('Board card layout', () => {
let wrapper;
let shouldUseGraphQL;
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
return new Vuex.Store({
state: {
...defaultState,
...state,
},
actions,
getters,
});
};
const mountComponent = ({
selectedId,
labels = [],
getListByTypeId = jest.fn(),
actions = {},
} = {}) => {
wrapper = extendedWrapper(
shallowMount(BoardAddNewColumn, {
stubs: {
BoardAddNewColumnForm,
},
data() {
return {
selectedId,
};
},
store: createStore({
actions: {
fetchLabels: jest.fn(),
setAddColumnFormVisibility: jest.fn(),
...actions,
},
getters: {
shouldUseGraphQL: () => shouldUseGraphQL,
getListByTypeId: () => getListByTypeId,
isEpicBoard: () => false,
},
state: {
labels,
labelsLoading: false,
},
}),
provide: {
scopedLabelsAvailable: true,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
const findSearchInput = () => wrapper.find(GlSearchBoxByType);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
beforeEach(() => {
shouldUseGraphQL = true;
});
it('shows form title & search input', () => {
mountComponent();
expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList);
expect(findSearchInput().exists()).toBe(true);
});
it('clicking cancel hides the form', () => {
const setAddColumnFormVisibility = jest.fn();
mountComponent({
actions: {
setAddColumnFormVisibility,
},
});
cancelButton().vm.$emit('click');
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
});
describe('Add list button', () => {
it('is disabled if no item is selected', () => {
mountComponent();
expect(submitButton().props('disabled')).toBe(true);
});
it('adds a new list on click', async () => {
const labelId = mockLabelList.label.id;
const highlightList = jest.fn();
const createList = jest.fn();
mountComponent({
labels: [mockLabelList.label],
selectedId: labelId,
actions: {
createList,
highlightList,
},
});
await nextTick();
submitButton().vm.$emit('click');
expect(highlightList).not.toHaveBeenCalled();
expect(createList).toHaveBeenCalledWith(expect.anything(), { labelId });
});
it('highlights existing list if trying to re-add', async () => {
const getListByTypeId = jest.fn().mockReturnValue(mockLabelList);
const highlightList = jest.fn();
const createList = jest.fn();
mountComponent({
labels: [mockLabelList.label],
selectedId: mockLabelList.label.id,
getListByTypeId,
actions: {
createList,
highlightList,
},
});
await nextTick();
submitButton().vm.$emit('click');
expect(highlightList).toHaveBeenCalledWith(expect.anything(), mockLabelList.id);
expect(createList).not.toHaveBeenCalled();
});
});
});
......@@ -86,7 +86,7 @@ describe('EpicsSwimlanes', () => {
});
it('displays BoardListHeader components for lists', () => {
expect(wrapper.findAll(BoardListHeader)).toHaveLength(2);
expect(wrapper.findAll(BoardListHeader)).toHaveLength(4);
});
it('displays EpicLane components for epic', () => {
......
......@@ -36,6 +36,35 @@ export const mockLists = [
milestone: null,
preset: false,
},
{
id: 'gid://gitlab/List/3',
title: 'Assignee list',
position: 0,
listType: 'assignee',
collapsed: false,
label: null,
maxIssueCount: 0,
assignee: {
id: 'gid://gitlab/',
},
milestone: null,
preset: false,
},
{
id: 'gid://gitlab/List/4',
title: 'Milestone list',
position: 0,
listType: 'milestone',
collapsed: false,
label: null,
maxIssueCount: 0,
assignee: null,
milestone: {
id: 'gid://gitlab/Milestone/1',
title: 'A milestone',
},
preset: false,
},
];
export const mockListsWithModel = mockLists.map((listMock) =>
......@@ -57,6 +86,17 @@ const assignees = [
},
];
export const mockMilestones = [
{
id: 'gid://gitlab/Milestone/1',
title: 'Milestone 1',
},
{
id: 'gid://gitlab/Milestone/2',
title: 'Milestone 2',
},
];
const labels = [
{
id: 'gid://gitlab/GroupLabel/5',
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import Vuex from 'vuex';
import { GroupByParamType } from 'ee/boards/constants';
import actions, { gqlClient } from 'ee/boards/stores/actions';
import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
import * as types from 'ee/boards/stores/mutation_types';
import mutations from 'ee/boards/stores/mutations';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { formatListIssues, formatBoardLists } from '~/boards/boards_util';
......@@ -11,7 +14,9 @@ import { issuableTypes } from '~/boards/constants';
import * as typesCE from '~/boards/stores/mutation_types';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import { mockLists, mockIssue, mockIssue2, mockEpic, rawIssue } from '../mock_data';
import { mockLists, mockIssue, mockIssue2, mockEpic, rawIssue, mockMilestones } from '../mock_data';
Vue.use(Vuex);
const expectNotImplemented = (action) => {
it('is not implemented', () => {
......@@ -1059,3 +1064,84 @@ describe('moveIssue', () => {
});
});
});
describe('fetchMilestones', () => {
const queryResponse = {
data: {
project: {
milestones: {
nodes: mockMilestones,
},
},
},
};
const queryErrors = {
data: {
project: {
errors: ['You cannot view these milestones'],
milestones: {},
},
},
};
function createStore({
state = {
boardType: 'project',
fullPath: 'gitlab-org/gitlab',
milestones: [],
milestonesLoading: false,
},
} = {}) {
return new Vuex.Store({
state,
mutations,
});
}
it('throws error if state.boardType is not group or project', () => {
const store = createStore({
state: {
boardType: 'invalid',
},
});
expect(() => actions.fetchMilestones(store)).toThrow(new Error('Unknown board type'));
});
it('sets milestonesLoading to true', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
actions.fetchMilestones(store);
expect(store.state.milestonesLoading).toBe(true);
});
describe('success', () => {
it('sets state.milestones from query result', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
await actions.fetchMilestones(store);
expect(store.state.milestonesLoading).toBe(false);
expect(store.state.milestones).toBe(mockMilestones);
});
});
describe('failure', () => {
it('sets state.milestones from query result', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryErrors);
const store = createStore();
await expect(actions.fetchMilestones(store)).rejects.toThrow();
expect(store.state.milestonesLoading).toBe(false);
expect(store.state.error).toBe('Failed to load milestones.');
});
});
});
......@@ -7,6 +7,7 @@ import {
mockIssuesByListId,
mockEpics,
issues,
mockLists,
} from '../mock_data';
describe('EE Boards Store Getters', () => {
......@@ -90,4 +91,35 @@ describe('EE Boards Store Getters', () => {
).toEqual([mockIssue3, mockIssue4]);
});
});
describe('getListByTypeId', () => {
const [, labelList, assigneeList, milestoneList] = mockLists;
it('returns label list by labelId', () => {
const labelId = labelList.label.id;
expect(getters.getListByTypeId({ boardLists: mockLists })({ labelId })).toEqual(labelList);
});
it('returns assignee list by assigneeId', () => {
const assigneeId = assigneeList.assignee.id;
expect(getters.getListByTypeId({ boardLists: mockLists })({ assigneeId })).toEqual(
assigneeList,
);
});
it('returns milestone list by milestoneId', () => {
const milestoneId = milestoneList.milestone.id;
expect(getters.getListByTypeId({ boardLists: mockLists })({ milestoneId })).toEqual(
milestoneList,
);
});
it('returns nothing if not results', () => {
expect(
getters.getListByTypeId({ boardLists: mockLists })({ labelId: 'not found' }),
).toBeUndefined();
});
});
});
......@@ -1348,6 +1348,9 @@ msgstr ""
msgid "A merge request hasn't yet been merged"
msgstr ""
msgid "A milestone list displays issues in the selected milestone."
msgstr ""
msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details"
msgstr ""
......@@ -1929,6 +1932,9 @@ msgstr ""
msgid "Add to Slack"
msgstr ""
msgid "Add to board"
msgstr ""
msgid "Add to epic"
msgstr ""
......@@ -12587,6 +12593,9 @@ msgstr ""
msgid "Failed to load labels. Please try again."
msgstr ""
msgid "Failed to load milestones."
msgstr ""
msgid "Failed to load milestones. Please try again."
msgstr ""
......@@ -18170,6 +18179,9 @@ msgstr ""
msgid "List the merge requests that must be merged before this one."
msgstr ""
msgid "List type"
msgstr ""
msgid "List view"
msgstr ""
......
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