Commit 78ef57fa authored by Florie Guibert's avatar Florie Guibert

Delete legacy board store and models

Clean up legacy board code
parent d98c7844
......@@ -21,11 +21,6 @@ export default {
},
inject: ['canAdminList'],
props: {
lists: {
type: Array,
required: false,
default: () => [],
},
disabled: {
type: Boolean,
required: true,
......
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
......@@ -289,14 +288,10 @@ export default {
setBoardLabels(labels) {
labels.forEach((label) => {
if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
this.board.labels.push(
new ListLabel({
id: label.id,
title: label.title,
color: label.color,
this.board.labels.push({
...label,
textColor: label.text_color,
}),
);
});
} else if (!label.set) {
this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
}
......
<script>
import {
GlLoadingIcon,
GlSearchBoxByType,
GlDropdown,
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownItem,
GlModalDirective,
} from '@gitlab/ui';
import { throttle } from 'lodash';
import { mapGetters, mapState } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import httpStatusCodes from '~/lib/utils/http_status';
import groupQuery from '../graphql/group_boards.query.graphql';
import projectQuery from '../graphql/project_boards.query.graphql';
import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
export default {
name: 'BoardsSelector',
components: {
BoardForm,
GlLoadingIcon,
GlSearchBoxByType,
GlDropdown,
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownItem,
},
directives: {
GlModalDirective,
},
props: {
currentBoard: {
type: Object,
required: true,
},
throttleDuration: {
type: Number,
default: 200,
required: false,
},
boardBaseUrl: {
type: String,
required: true,
},
hasMissingBoards: {
type: Boolean,
required: true,
},
canAdminBoard: {
type: Boolean,
required: true,
},
multipleIssueBoardsAvailable: {
type: Boolean,
required: true,
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
groupId: {
type: Number,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: true,
},
weights: {
type: Array,
required: true,
},
enabledScopedLabels: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
hasScrollFade: false,
loadingBoards: 0,
loadingRecentBoards: false,
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
state: boardsStore.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
store: boardsStore,
filterTerm: '',
};
},
computed: {
...mapState(['boardType']),
...mapGetters(['isGroupBoard']),
parentType() {
return this.boardType;
},
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
currentPage() {
return this.state.currentPage;
},
filteredBoards() {
return this.boards.filter((board) =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
board() {
return this.state.currentBoard;
},
showDelete() {
return this.boards.length > 1;
},
scrollFadeClass() {
return {
'fade-out': !this.hasScrollFade,
};
},
showRecentSection() {
return (
this.recentBoards.length &&
this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
!this.filterTerm.length
);
},
},
watch: {
filteredBoards() {
this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade);
},
},
created() {
boardsStore.setCurrentBoard(this.currentBoard);
},
methods: {
showPage(page) {
boardsStore.showPage(page);
},
cancel() {
this.showPage('');
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
return;
}
this.$apollo.addSmartQuery('boards', {
variables() {
return { fullPath: this.state.endpoints.fullPath };
},
query() {
return this.isGroupBoard ? groupQuery : projectQuery;
},
loadingKey: 'loadingBoards',
update(data) {
if (!data?.[this.parentType]) {
return [];
}
return data[this.parentType].boards.edges.map(({ node }) => ({
id: getIdFromGraphQLId(node.id),
name: node.name,
}));
},
});
this.loadingRecentBoards = true;
boardsStore
.recentBoards()
.then((res) => {
this.recentBoards = res.data;
})
.catch((err) => {
/**
* If user is unauthorized we'd still want to resolve the
* request to display all boards.
*/
if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
this.recentBoards = []; // recent boards are empty
return;
}
throw err;
})
.then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => {
this.setScrollFade();
})
.catch(() => {})
.finally(() => {
this.loadingRecentBoards = false;
});
},
isScrolledUp() {
const { content } = this.$refs;
if (!content) {
return false;
}
const currentPosition = this.contentClientHeight + content.scrollTop;
return currentPosition < this.maxPosition;
},
initScrollFade() {
const { content } = this.$refs;
if (!content) {
return;
}
this.scrollFadeInitialized = true;
this.contentClientHeight = content.clientHeight;
this.maxPosition = content.scrollHeight;
},
setScrollFade() {
if (!this.scrollFadeInitialized) this.initScrollFade();
this.hasScrollFade = this.isScrolledUp();
},
},
};
</script>
<template>
<div class="boards-switcher js-boards-selector gl-mr-3">
<span class="boards-selector-wrapper js-boards-selector-wrapper">
<gl-dropdown
data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height"
:text="board.name"
@show="loadBoards"
>
<p class="gl-new-dropdown-header-top" @mousedown.prevent>
{{ s__('IssueBoards|Switch board') }}
</p>
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
<div
v-if="!loading"
ref="content"
data-qa-selector="boards_dropdown_content"
class="dropdown-content flex-fill"
@scroll.passive="throttledSetScrollFade"
>
<gl-dropdown-item
v-show="filteredBoards.length === 0"
class="gl-pointer-events-none text-secondary"
>
{{ s__('IssueBoards|No matching boards found') }}
</gl-dropdown-item>
<gl-dropdown-section-header v-if="showRecentSection">
{{ __('Recent') }}
</gl-dropdown-section-header>
<template v-if="showRecentSection">
<gl-dropdown-item
v-for="recentBoard in recentBoards"
:key="`recent-${recentBoard.id}`"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${recentBoard.id}`"
>
{{ recentBoard.name }}
</gl-dropdown-item>
</template>
<gl-dropdown-divider v-if="showRecentSection" />
<gl-dropdown-section-header v-if="showRecentSection">
{{ __('All') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="otherBoard in filteredBoards"
:key="otherBoard.id"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${otherBoard.id}`"
>
{{ otherBoard.name }}
</gl-dropdown-item>
<gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
{{
s__(
'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
)
}}
</gl-dropdown-item>
</div>
<div
v-show="filteredBoards.length > 0"
class="dropdown-content-faded-mask"
:class="scrollFadeClass"
></div>
<gl-loading-icon v-if="loading" size="sm" />
<div v-if="canAdminBoard">
<gl-dropdown-divider />
<gl-dropdown-item
v-if="multipleIssueBoardsAvailable"
v-gl-modal-directive="'board-config-modal'"
data-qa-selector="create_new_board_button"
@click.prevent="showPage('new')"
>
{{ s__('IssueBoards|Create new board') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="showDelete"
v-gl-modal-directive="'board-config-modal'"
class="text-danger js-delete-board"
@click.prevent="showPage('delete')"
>
{{ s__('IssueBoards|Delete board') }}
</gl-dropdown-item>
</div>
</gl-dropdown>
<board-form
v-if="currentPage"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard"
:current-page="state.currentPage"
@cancel="cancel"
/>
</span>
</div>
</template>
export const setWeightFetchingState = () => {};
export const setEpicFetchingState = () => {};
export const getMilestoneTitle = () => ({});
......@@ -4,33 +4,19 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
import { setWeightFetchingState, setEpicFetchingState } from 'ee_else_ce/boards/ee_functions';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import './models/label';
import './models/assignee';
import '~/boards/models/milestone';
import '~/boards/models/project';
import '~/boards/filters/due_date_filters';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import toggleFocusMode from '~/boards/toggle_focus';
import createDefaultClient from '~/lib/graphql';
import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
......@@ -77,8 +63,6 @@ export default () => {
initBoardsFilteredSearch(apolloProvider);
}
boardsStore.create();
// eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
el: $boardApp,
......@@ -116,22 +100,13 @@ export default () => {
apolloProvider,
data() {
return {
state: boardsStore.state,
loading: 0,
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
disabled: parseBoolean($boardApp.dataset.disabled),
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: boardsStore.detail,
parent: $boardApp.dataset.parent,
detailIssueVisible: false,
};
},
computed: {
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
},
created() {
this.setInitialBoardData({
boardId: $boardApp.dataset.boardId,
......@@ -154,129 +129,29 @@ export default () => {
: null,
},
});
boardsStore.setEndpoints({
boardsEndpoint: this.boardsEndpoint,
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: $boardApp.dataset.boardId,
fullPath: $boardApp.dataset.fullPath,
});
boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens);
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
eventHub.$on('toggleDetailIssue', this.toggleDetailIssue);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
eventHub.$off('toggleDetailIssue', this.toggleDetailIssue);
},
mounted() {
if (!gon?.features?.issueBoardsFilteredSearch) {
this.filterManager = new FilteredSearchBoards(
boardsStore.filter,
true,
boardsStore.cantEdit,
);
this.filterManager = new FilteredSearchBoards({ path: '' }, true, []);
this.filterManager.setup();
}
this.performSearch();
boardsStore.disabled = this.disabled;
},
methods: {
...mapActions(['setInitialBoardData', 'performSearch', 'setError']),
updateTokens() {
this.filterManager.updateTokens();
},
updateDetailIssue(newIssue, multiSelect = false) {
const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
setWeightFetchingState(newIssue, true);
setEpicFetchingState(newIssue, true);
boardsStore
.getIssueInfo(sidebarInfoEndpoint)
.then((res) => res.data)
.then((data) => {
const {
subscribed,
totalTimeSpent,
timeEstimate,
humanTimeEstimate,
humanTotalTimeSpent,
weight,
epic,
assignees,
} = convertObjectPropsToCamelCase(data);
newIssue.setFetchingState('subscriptions', false);
setWeightFetchingState(newIssue, false);
setEpicFetchingState(newIssue, false);
newIssue.updateData({
humanTimeSpent: humanTotalTimeSpent,
timeSpent: totalTimeSpent,
humanTimeEstimate,
timeEstimate,
subscribed,
weight,
epic,
assignees,
});
})
.catch(() => {
newIssue.setFetchingState('subscriptions', false);
setWeightFetchingState(newIssue, false);
this.setError({ message: __('An error occurred while fetching sidebar data') });
});
}
if (multiSelect) {
boardsStore.toggleMultiSelect(newIssue);
if (boardsStore.detail.issue) {
boardsStore.clearDetailIssue();
return;
}
return;
}
boardsStore.setIssueDetail(newIssue);
},
clearDetailIssue(multiSelect = false) {
if (multiSelect) {
boardsStore.clearMultiSelect();
}
boardsStore.clearDetailIssue();
},
toggleSubscription(id) {
const { issue } = boardsStore.detail;
if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true);
boardsStore
.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
.then(() => {
issue.setFetchingState('subscriptions', false);
issue.updateData({
subscribed: !issue.subscribed,
});
})
.catch(() => {
issue.setFetchingState('subscriptions', false);
this.setError({
message: __('An error occurred when toggling the notification subscription'),
});
});
}
},
getNodes(data) {
return data[this.parent]?.board?.lists.nodes;
toggleDetailIssue(hasSidebar) {
this.detailIssueVisible = hasSidebar;
},
},
});
......
export default class ListAssignee {
constructor(obj) {
this.id = obj.id;
this.name = obj.name;
this.username = obj.username;
this.avatar = obj.avatarUrl || obj.avatar_url || obj.avatar || gon.default_avatar_url;
this.path = obj.path;
this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
}
}
window.ListAssignee = ListAssignee;
/* eslint-disable no-unused-vars */
/* global ListLabel */
/* global ListMilestone */
/* global ListAssignee */
import axios from '~/lib/utils/axios_utils';
import './label';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import boardsStore from '../stores/boards_store';
import IssueProject from './project';
class ListIssue {
constructor(obj) {
this.subscribed = obj.subscribed;
this.labels = [];
this.assignees = [];
this.selected = false;
this.position = obj.position || obj.relative_position || obj.relativePosition || Infinity;
this.isFetching = {
subscriptions: true,
};
this.closed = obj.closed;
this.isLoading = {};
this.refreshData(obj);
}
refreshData(obj) {
boardsStore.refreshIssueData(this, obj);
}
addLabel(label) {
boardsStore.addIssueLabel(this, label);
}
findLabel(findLabel) {
return boardsStore.findIssueLabel(this, findLabel);
}
removeLabel(removeLabel) {
boardsStore.removeIssueLabel(this, removeLabel);
}
removeLabels(labels) {
boardsStore.removeIssueLabels(this, labels);
}
addAssignee(assignee) {
boardsStore.addIssueAssignee(this, assignee);
}
findAssignee(findAssignee) {
return boardsStore.findIssueAssignee(this, findAssignee);
}
setAssignees(assignees) {
boardsStore.setIssueAssignees(this, assignees);
}
removeAssignee(removeAssignee) {
boardsStore.removeIssueAssignee(this, removeAssignee);
}
removeAllAssignees() {
boardsStore.removeAllIssueAssignees(this);
}
addMilestone(milestone) {
boardsStore.addIssueMilestone(this, milestone);
}
removeMilestone(removeMilestone) {
boardsStore.removeIssueMilestone(this, removeMilestone);
}
getLists() {
return boardsStore.state.lists.filter((list) => list.findIssue(this.id));
}
updateData(newData) {
boardsStore.updateIssueData(this, newData);
}
setFetchingState(key, value) {
boardsStore.setIssueFetchingState(this, key, value);
}
setLoadingState(key, value) {
boardsStore.setIssueLoadingState(this, key, value);
}
update() {
return boardsStore.updateIssue(this);
}
}
window.ListIssue = ListIssue;
export default ListIssue;
export default class ListIteration {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
this.description = obj.description;
}
}
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default class ListLabel {
constructor(obj) {
Object.assign(this, convertObjectPropsToCamelCase(obj, { dropKeys: ['priority'] }), {
priority: obj.priority !== null ? obj.priority : Infinity,
});
}
}
window.ListLabel = ListLabel;
/* eslint-disable class-methods-use-this */
import createFlash from '~/flash';
import { __ } from '~/locale';
import boardsStore from '../stores/boards_store';
import ListAssignee from './assignee';
import ListIteration from './iteration';
import ListLabel from './label';
import ListMilestone from './milestone';
import 'ee_else_ce/boards/models/issue';
const TYPES = {
backlog: {
isPreset: true,
isExpandable: true,
isBlank: false,
},
closed: {
isPreset: true,
isExpandable: true,
isBlank: false,
},
blank: {
isPreset: true,
isExpandable: false,
isBlank: true,
},
default: {
// includes label, assignee, and milestone lists
isPreset: false,
isExpandable: true,
isBlank: false,
},
};
class List {
constructor(obj) {
this.id = obj.id;
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type || obj.listType;
const typeInfo = this.getTypeInfo(this.type);
this.preset = Boolean(typeInfo.isPreset);
this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = !obj.collapsed;
this.page = 1;
this.highlighted = obj.highlighted;
this.loading = true;
this.loadingMore = false;
this.issues = obj.issues || [];
this.issuesSize = obj.issuesSize || obj.issuesCount || 0;
this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0;
if (obj.label) {
this.label = new ListLabel(obj.label);
} else if (obj.user || obj.assignee) {
this.assignee = new ListAssignee(obj.user || obj.assignee);
this.title = this.assignee.name;
} else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
this.title = this.milestone.title;
} else if (IS_EE && obj.iteration) {
this.iteration = new ListIteration(obj.iteration);
this.title = this.iteration.title;
}
// doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
// Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/229416
if (!typeInfo.isBlank && this.id && !obj.doNotFetchIssues) {
this.getIssues().catch(() => {
// TODO: handle request error
});
}
}
guid() {
const s4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}
save() {
return boardsStore.saveList(this);
}
destroy() {
boardsStore.destroy(this);
}
update() {
return boardsStore.updateListFunc(this);
}
nextPage() {
return boardsStore.goToNextPage(this);
}
getIssues(emptyIssues = true) {
return boardsStore.getListIssues(this, emptyIssues);
}
newIssue(issue) {
return boardsStore.newListIssue(this, issue);
}
addMultipleIssues(issues, listFrom, newIndex) {
boardsStore.addMultipleListIssues(this, issues, listFrom, newIndex);
}
addIssue(issue, listFrom, newIndex) {
boardsStore.addListIssue(this, issue, listFrom, newIndex);
}
moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
boardsStore.moveListIssues(this, issue, oldIndex, newIndex, moveBeforeId, moveAfterId);
}
moveMultipleIssues({ issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) {
boardsStore
.moveListMultipleIssues({
list: this,
issues,
oldIndicies,
newIndex,
moveBeforeId,
moveAfterId,
})
.catch(() =>
createFlash({
message: __('Something went wrong while moving issues.'),
}),
);
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
boardsStore.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId).catch(() => {
// TODO: handle request error
});
}
updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) {
boardsStore
.moveMultipleIssues({
ids: issues.map((issue) => issue.id),
fromListId: listFrom.id,
toListId: this.id,
moveBeforeId,
moveAfterId,
})
.catch(() =>
createFlash({
message: __('Something went wrong while moving issues.'),
}),
);
}
findIssue(id) {
return boardsStore.findListIssue(this, id);
}
removeMultipleIssues(removeIssues) {
return boardsStore.removeListMultipleIssues(this, removeIssues);
}
removeIssue(removeIssue) {
return boardsStore.removeListIssues(this, removeIssue);
}
getTypeInfo(type) {
return TYPES[type] || TYPES.default;
}
onNewIssueResponse(issue, data) {
boardsStore.onNewListIssueResponse(this, issue, data);
}
}
window.List = List;
export default List;
export default class ListMilestone {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
if (IS_EE) {
this.path = obj.path;
this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
this.description = obj.description;
}
}
}
window.ListMilestone = ListMilestone;
export default class IssueProject {
constructor(obj) {
this.id = obj.id;
this.path = obj.path;
this.fullPath = obj.path_with_namespace;
}
}
......@@ -18,6 +18,7 @@ import {
} from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import eventHub from '~/boards/eventhub';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -61,10 +62,12 @@ export default {
setActiveId({ commit }, { id, sidebarType }) {
commit(types.SET_ACTIVE_ID, { id, sidebarType });
eventHub.$emit('toggleDetailIssue', true);
},
unsetActiveId({ dispatch }) {
dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
eventHub.$emit('toggleDetailIssue', false);
},
setFilters: ({ commit, state: { issuableType } }, filters) => {
......
This diff is collapsed.
// this is just to make ee_else_ce happy and will be cleaned up in https://gitlab.com/gitlab-org/gitlab-foss/issues/59807
export default {
initEESpecific() {},
};
......@@ -18,5 +18,5 @@
= render 'shared/issuable/search_bar', type: :boards, board: board
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
%board-content{ ":lists" => "state.lists", ":disabled" => "disabled" }
%board-content{ ":disabled" => "disabled" }
%board-settings-sidebar
<script>
import { GlButton } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
export default {
components: {
GlButton,
},
props: {
item: {
type: Object,
required: true,
},
},
computed: {
avatarAltText() {
return sprintf(__("%{name}'s avatar"), {
name: this.item.name,
});
},
},
methods: {
handleItemClick() {
this.$emit('onItemSelect', this.item);
},
},
};
</script>
<template>
<li class="filter-dropdown-item" @click="handleItemClick">
<gl-button variant="link" category="primary" class="dropdown-user">
<div class="avatar-container s32 flex-shrink-0">
<img :alt="avatarAltText" :src="item.avatar_url" class="avatar s32 lazy" />
</div>
<div class="text-truncate dropdown-user-details">
<div class="text-truncate">{{ item.name }}</div>
<div class="text-truncate dropdown-light-content">@{{ item.username }}</div>
</div>
</gl-button>
</li>
</template>
import Vue from 'vue';
import vuexStore from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import { fullMilestoneId, fullUserId } from '../../boards_util';
import ListContainer from './list_container.vue';
export default Vue.extend({
components: {
ListContainer,
},
props: {
listPath: {
type: String,
required: true,
},
listType: {
type: String,
required: true,
},
},
data() {
return {
loading: true,
store: boardsStore,
vuexStore,
};
},
mounted() {
this.loadList();
},
methods: {
loadList() {
return this.store.loadList(this.listPath, this.listType).then(() => {
this.loading = false;
});
},
filterItems(term, items) {
const query = term.toLowerCase();
return items.filter((item) => {
const name = item.name ? item.name.toLowerCase() : item.title.toLowerCase();
const foundName = name.indexOf(query) > -1;
if (this.listType === 'milestones') {
return foundName;
}
const username = item.username.toLowerCase();
return foundName || username.indexOf(query) > -1;
});
},
prepareListObject(item) {
const list = {
title: item.name,
position: this.store.state.lists.length - 2,
list_type: this.listType,
};
if (this.listType === 'milestones') {
list.milestone = item;
} else if (this.listType === 'assignees') {
list.user = item;
}
return list;
},
handleItemClick(item) {
if (!this.vuexStore.getters.getListByTitle(item.title)) {
if (this.listType === 'milestones') {
this.vuexStore.dispatch('createList', { milestoneId: fullMilestoneId(item.id) });
} else if (this.listType === 'assignees') {
this.vuexStore.dispatch('createList', { assigneeId: fullUserId(item.id) });
}
}
},
},
render(createElement) {
return createElement('list-container', {
props: {
loading: this.loading,
items: this.store.state[this.listType],
listType: this.listType,
},
on: {
onItemSelect: this.handleItemClick,
},
});
},
});
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import ListContent from './list_content.vue';
import ListFilter from './list_filter.vue';
export default {
components: {
ListFilter,
ListContent,
GlLoadingIcon,
},
props: {
loading: {
type: Boolean,
required: true,
},
items: {
type: Array,
required: true,
},
listType: {
type: String,
required: true,
},
},
data() {
return {
query: '',
};
},
computed: {
filteredItems() {
if (!this.query) return this.items;
const query = this.query.toLowerCase();
return this.items.filter((item) => {
const name = item.name ? item.name.toLowerCase() : item.title.toLowerCase();
if (this.listType === 'milestones') {
return name.indexOf(query) > -1;
}
const username = item.username.toLowerCase();
return name.indexOf(query) > -1 || username.indexOf(query) > -1;
});
},
},
methods: {
handleSearch(query) {
this.query = query;
},
handleItemClick(item) {
this.$emit('onItemSelect', item);
},
},
};
</script>
<template>
<div class="dropdown-assignees-list">
<div v-if="loading" class="dropdown-loading"><gl-loading-icon size="sm" /></div>
<list-filter @onSearchInput="handleSearch" />
<list-content
v-if="!loading"
:items="filteredItems"
:list-type="listType"
@onItemSelect="handleItemClick"
/>
</div>
</template>
<script>
import AssigneesListItem from './assignees_list_item.vue';
import MilestoneListItem from './milestones_list_item.vue';
export default {
props: {
items: {
type: Array,
required: true,
},
listType: {
type: String,
required: true,
},
},
computed: {
listContentComponent() {
return this.listType === 'assignees' ? AssigneesListItem : MilestoneListItem;
},
},
methods: {
handleItemClick(item) {
this.$emit('onItemSelect', item);
},
},
};
</script>
<template>
<div class="dropdown-content">
<ul>
<component
:is="listContentComponent"
v-for="item in items"
:key="item.id"
:item="item"
@onItemSelect="handleItemClick"
/>
</ul>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
export default {
components: {
GlIcon,
},
data() {
return {
query: '',
};
},
methods: {
handleInputChange() {
this.$emit('onSearchInput', this.query);
},
handleInputClear() {
this.query = '';
this.handleInputChange();
},
},
};
</script>
<template>
<div :class="{ 'has-value': !!query }" class="dropdown-input">
<input
v-model.trim="query"
:placeholder="__('Search')"
type="search"
class="dropdown-input-field"
@keyup="handleInputChange"
/>
<gl-icon name="search" class="dropdown-input-search" data-hidden="true" />
<gl-icon name="close" class="dropdown-input-clear" @click="handleInputClear" />
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
export default {
components: {
GlButton,
},
props: {
item: {
type: Object,
required: true,
},
},
methods: {
handleItemClick() {
this.$emit('onItemSelect', this.item);
},
},
};
</script>
<template>
<li>
<gl-button category="tertiary" class="gl-rounded-0!" @click="handleItemClick">
<span class="gl-white-space-normal">{{ item.title }}</span>
</gl-button>
</li>
</template>
export const setWeightFetchingState = (issue, value) => {
issue.setFetchingState('weight', value);
};
export const setEpicFetchingState = (issue, value) => {
issue.setFetchingState('epic', value);
};
export const getMilestoneTitle = ($boardApp) => ({
milestoneTitle: $boardApp.dataset.boardMilestoneTitle,
});
import ListIssue from '~/boards/models/issue';
import IssueProject from '~/boards/models/project';
import boardsStore from '~/boards/stores/boards_store';
class ListIssueEE extends ListIssue {
constructor(obj) {
super(obj, {
IssueProject,
});
this.weight = obj.weight;
if (obj.project) {
this.project = new IssueProject(obj.project);
}
}
updateEpic(newEpic) {
boardsStore.updateIssueEpic(this, newEpic);
}
}
window.ListIssue = ListIssueEE;
export default ListIssueEE;
/* eslint-disable no-param-reassign */
import ListAssignee from '~/boards/models/assignee';
import List from '~/boards/models/list';
import ListMilestone from '~/boards/models/milestone';
class ListEE extends List {
constructor(...args) {
super(...args);
this.totalWeight = args[0]?.totalWeight || 0;
}
getIssues(...args) {
return super.getIssues(...args).then((data) => {
this.totalWeight = data.total_weight;
});
}
addIssue(issue, ...args) {
super.addIssue(issue, ...args);
if (issue.weight) {
this.totalWeight += issue.weight;
}
}
removeIssue(issue, ...args) {
if (issue.weight) {
this.totalWeight -= issue.weight;
}
super.removeIssue(issue, ...args);
}
addWeight(weight) {
this.totalWeight += weight;
}
onNewIssueResponse(issue, data) {
issue.milestone = data.milestone ? new ListMilestone(data.milestone) : data.milestone;
issue.assignees = Array.isArray(data.assignees)
? data.assignees.map((assignee) => new ListAssignee(assignee))
: data.assignees;
issue.labels = data.labels;
super.onNewIssueResponse(issue, data);
}
}
window.List = ListEE;
export default ListEE;
/* eslint-disable class-methods-use-this, no-param-reassign */
/*
no-param-reassign is disabled because one method of BoardsStoreEE
modify the passed parameter in conformity with non-ee BoardsStore.
*/
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
const NO_ITERATION_TITLE = 'No+Iteration';
const NO_MILESTONE_TITLE = 'No+Milestone';
class BoardsStoreEE {
initEESpecific(boardsStore) {
this.$boardApp = document.getElementById('board-app');
this.store = boardsStore;
this.store.loadList = (listPath, listType) => this.loadList(listPath, listType);
const superSetCurrentBoard = this.store.setCurrentBoard.bind(this.store);
this.store.setCurrentBoard = (board) => {
superSetCurrentBoard(board);
this.store.state.assignees = [];
this.store.state.milestones = [];
};
const baseCreate = this.store.create.bind(this.store);
this.store.create = () => {
baseCreate();
if (this.$boardApp) {
const {
dataset: {
boardMilestoneId,
boardMilestoneTitle,
boardIterationTitle,
boardIterationId,
boardAssigneeUsername,
labels,
boardWeight,
weightFeatureAvailable,
scopedLabels,
},
} = this.$boardApp;
this.store.boardConfig = {
milestoneId: parseInt(boardMilestoneId, 10),
milestoneTitle: boardMilestoneTitle || '',
iterationId: parseInt(boardIterationId, 10),
iterationTitle: boardIterationTitle || '',
assigneeUsername: boardAssigneeUsername,
labels: JSON.parse(labels || []),
weight: parseInt(boardWeight, 10),
};
this.store.cantEdit = [];
this.store.weightFeatureAvailable = parseBoolean(weightFeatureAvailable);
this.store.scopedLabels = {
enabled: parseBoolean(scopedLabels),
};
}
};
this.store.updateFiltersUrl = (replaceState = false) => {
if (!this.store.filter.path) {
return;
}
if (replaceState) {
window.history.replaceState(null, null, `?${this.store.filter.path}`);
} else {
window.history.pushState(null, null, `?${this.store.filter.path}`);
}
};
this.store.updateIssueEpic = this.updateIssueEpic;
sidebarEventHub.$on('updateWeight', this.updateWeight.bind(this));
Object.assign(this.store, {
updateWeight(endpoint, weight = null) {
return axios.put(endpoint, {
weight,
});
},
});
}
initBoardFilters() {
const updateFilterPath = (key, value) => {
if (!value) return;
const querystring = `${key}=${value}`;
this.store.filter.path = [querystring]
.concat(
this.store.filter.path
.split('&')
.filter((param) => param.match(new RegExp(`^${key}=(.*)$`, 'g')) === null),
)
.join('&');
};
let { milestoneTitle } = this.store.boardConfig;
if (this.store.boardConfig.milestoneId === 0) {
milestoneTitle = NO_MILESTONE_TITLE;
} else {
milestoneTitle = encodeURIComponent(milestoneTitle);
}
if (milestoneTitle) {
updateFilterPath('milestone_title', milestoneTitle);
this.store.cantEdit.push('milestone');
}
let { iterationTitle } = this.store.boardConfig;
if (this.store.boardConfig.iterationId === 0) {
iterationTitle = NO_ITERATION_TITLE;
} else {
iterationTitle = encodeURIComponent(iterationTitle);
}
if (iterationTitle) {
updateFilterPath('iteration_id', iterationTitle);
this.store.cantEdit.push('iteration');
}
let { weight } = this.store.boardConfig;
if (weight !== -1) {
if (weight === 0) {
weight = '0';
}
if (weight === -2) {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
weight = 'None';
}
updateFilterPath('weight', weight);
}
updateFilterPath('assignee_username', this.store.boardConfig.assigneeUsername);
if (this.store.boardConfig.assigneeUsername) {
this.store.cantEdit.push('assignee');
}
const filterPath = this.store.filter.path.split('&');
this.store.boardConfig.labels.forEach((label) => {
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
if (labelIndex === -1) {
filterPath.push(param);
}
this.store.cantEdit.push({
name: 'label',
value: label.title,
});
});
this.store.filter.path = filterPath.join('&');
this.store.updateFiltersUrl(true);
}
setMaxIssueCountOnList(id, maxIssueCount) {
this.store.findList('id', id).maxIssueCount = maxIssueCount;
}
updateIssueEpic(issue, newEpic) {
issue.epic = newEpic;
}
updateWeight({ id, value: newWeight }) {
const { issue } = this.store.detail;
if (issue.id === id && issue.sidebarInfoEndpoint) {
issue.setLoadingState('weight', true);
this.store
.updateWeight(issue.sidebarInfoEndpoint, newWeight)
.then((res) => res.data)
.then((data) => {
const lists = issue.getLists();
const oldWeight = issue.weight;
const weightDiff = newWeight - oldWeight;
issue.setLoadingState('weight', false);
issue.updateData({
weight: data.weight,
});
lists.forEach((list) => {
list.addWeight(weightDiff);
});
})
.catch(() => {
issue.setLoadingState('weight', false);
createFlash({
message: __('An error occurred when updating the issue weight'),
});
});
}
}
loadList(listPath, listType) {
if (this.store.state[listType].length) {
return Promise.resolve();
}
return axios
.get(listPath)
.then(({ data }) => {
this.store.state[listType] = data;
})
.catch(() => {
createFlash({
message: sprintf(__('Something went wrong while fetching %{listType} list'), {
listType,
}),
});
});
}
}
export default new BoardsStoreEE();
// This file is duplicated in ~/boards/models/label.js
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default class ListLabel {
......
......@@ -92,11 +92,8 @@ export default () => {
state: {},
loading: 0,
allowSubEpics: parseBoolean($boardApp.dataset.subEpicsFeatureAvailable),
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
disabled: parseBoolean($boardApp.dataset.disabled),
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
parent: $boardApp.dataset.parent,
detailIssueVisible: false,
};
......
import Api from 'ee/api';
import { noneEpic } from 'ee/vue_shared/constants';
import boardsStore from '~/boards/stores/boards_store';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { formatDate, timeFor } from '~/lib/utils/datetime_utility';
import { s__, __ } from '~/locale';
import { s__ } from '~/locale';
import * as types from './mutation_types';
......@@ -59,43 +57,8 @@ export const fetchEpics = ({ state, dispatch }, search = '') => {
export const requestIssueUpdate = ({ commit }) => commit(types.REQUEST_ISSUE_UPDATE);
export const receiveIssueUpdateSuccess = ({ state, commit }, { data, epic, isRemoval = false }) => {
/*
If EpicsSelect is loaded within Boards, -
we need to update "boardsStore.issue.detail.epic" which has -
a differently formatted timestamp that includes '<strong>' tag.
However, "data.epic" in the response of the API POST doesn't have '<strong>' tag.
("epic" param is also in a different format).
*/
function insertStrongTag(humanReadableTimestamp) {
if (humanReadableTimestamp === __('Past due')) {
return `<strong>${humanReadableTimestamp}</strong>`;
}
// Insert strong tag for for any number in the string.
// I.e., "3 days remaining" or "Осталось 3 дней"
// A similar transformation is done in the backend:
// app/serializers/entity_date_helper.rb
return humanReadableTimestamp.replace(/\d+/, '<strong>$&</strong>');
}
// Verify if update was successful
if (data.epic.id === epic.id && data.issue.id === state.issueId) {
if (boardsStore.detail.issue.updateEpic) {
const formattedEpic = isRemoval
? { epic_issue_id: noneEpic.id }
: {
epic_issue_id: data.id,
group_id: data.epic.group_id,
human_readable_end_date: formatDate(data.epic.end_date, 'mmm d, yyyy'),
human_readable_timestamp: insertStrongTag(timeFor(data.epic.end_date)),
id: data.epic.id,
iid: data.epic.iid,
title: data.epic.title,
url: `/groups/${data.epic.web_url.replace(/.+groups\//, '')}`,
};
boardsStore.detail.issue.updateEpic(formattedEpic);
}
commit(types.RECEIVE_ISSUE_UPDATE_SUCCESS, {
selectedEpic: isRemoval ? noneEpic : epic,
selectedEpicIssueId: data.id,
......
import { shallowMount } from '@vue/test-utils';
import AssigneesListItem from 'ee/boards/components/boards_list_selector/assignees_list_item.vue';
import { mockAssigneesList } from 'jest/boards/mock_data';
describe('AssigneesListItem', () => {
const assignee = mockAssigneesList[0];
let wrapper;
beforeEach(() => {
wrapper = shallowMount(AssigneesListItem, {
propsData: {
item: assignee,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders component container element with class `filter-dropdown-item`', () => {
expect(wrapper.find('.filter-dropdown-item').exists()).toBe(true);
});
it('emits `onItemSelect` event on component click and sends `assignee` as event param', () => {
wrapper.find('.filter-dropdown-item').trigger('click');
expect(wrapper.emitted().onItemSelect[0]).toEqual([assignee]);
});
describe('avatar', () => {
it('has alt text', () => {
expect(wrapper.find('.avatar').attributes('alt')).toBe(`${assignee.name}'s avatar`);
});
it('has src url', () => {
expect(wrapper.find('.avatar').attributes('src')).toBe(assignee.avatar_url);
});
});
describe('user details', () => {
it('shows assignee name', () => {
expect(wrapper.find('.dropdown-user-details').text()).toContain(assignee.name);
});
it('shows assignee username', () => {
expect(wrapper.find('.dropdown-user-details .dropdown-light-content').text()).toContain(
`@${assignee.username}`,
);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import BoardListSelector from 'ee/boards/components/boards_list_selector/';
import mountComponent from 'helpers/vue_mount_component_helper';
import { mockAssigneesList } from 'jest/boards/mock_data';
import { TEST_HOST } from 'spec/test_constants';
import { createStore } from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
describe('BoardListSelector', () => {
const dummyEndpoint = `${TEST_HOST}/users.json`;
const createComponent = () =>
mountComponent(BoardListSelector, {
listPath: dummyEndpoint,
listType: 'assignees',
});
let vm;
let mock;
boardsStore.create();
boardsStore.state.assignees = [];
beforeEach(() => {
mock = new MockAdapter(axios);
vm = createComponent();
vm.vuexStore = createStore();
});
afterEach(() => {
vm.$destroy();
mock.restore();
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.loading).toBe(true);
expect(vm.store).toBe(boardsStore);
});
});
describe('methods', () => {
describe('loadList', () => {
it('calls axios.get and sets response to store.state.assignees', (done) => {
mock.onGet(dummyEndpoint).reply(200, mockAssigneesList);
boardsStore.state.assignees = [];
vm.loadList()
.then(() => {
expect(vm.loading).toBe(false);
expect(vm.store.state.assignees.length).toBe(mockAssigneesList.length);
})
.then(done)
.catch(done.fail);
});
it('does not call axios.get when store.state.assignees is not empty', (done) => {
jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve());
boardsStore.state.assignees = mockAssigneesList;
vm.loadList()
.then(() => {
expect(axios.get).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('calls axios.get and shows Flash error when request fails', (done) => {
mock.onGet(dummyEndpoint).replyOnce(500, {});
boardsStore.state.assignees = [];
vm.loadList()
.then(() => {
expect(vm.loading).toBe(false);
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while fetching assignees list',
});
})
.then(done)
.catch(done.fail);
});
});
describe('handleItemClick', () => {
it('creates new list in a store instance', () => {
jest.spyOn(vm.vuexStore, 'dispatch').mockReturnValue({});
const assignee = mockAssigneesList[0];
expect(vm.vuexStore.getters.getListByTitle(assignee.name)).not.toBeDefined();
vm.handleItemClick(assignee);
expect(vm.vuexStore.dispatch).toHaveBeenCalledWith('createList', {
assigneeId: 'gid://gitlab/User/2',
});
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import ListContainer from 'ee/boards/components/boards_list_selector/list_container.vue';
import ListContent from 'ee/boards/components/boards_list_selector/list_content.vue';
import ListFilter from 'ee/boards/components/boards_list_selector/list_filter.vue';
import { mockAssigneesList } from 'jest/boards/mock_data';
describe('ListContainer', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(ListContainer, {
propsData: {
loading: false,
items: mockAssigneesList,
listType: 'assignees',
},
});
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('filteredItems', () => {
it('returns assignees list as it is when `query` is empty', () => {
wrapper.setData({ query: '' });
expect(wrapper.vm.filteredItems).toHaveLength(mockAssigneesList.length);
});
it('returns filtered assignees list as it is when `query` has name', () => {
const assignee = mockAssigneesList[0];
wrapper.setData({ query: assignee.name });
expect(wrapper.vm.filteredItems).toHaveLength(1);
expect(wrapper.vm.filteredItems[0].name).toBe(assignee.name);
});
it('returns filtered assignees list as it is when `query` has username', () => {
const assignee = mockAssigneesList[0];
wrapper.setData({ query: assignee.username });
expect(wrapper.vm.filteredItems).toHaveLength(1);
expect(wrapper.vm.filteredItems[0].username).toBe(assignee.username);
});
});
});
describe('methods', () => {
describe('handleSearch', () => {
it('sets value of param `query` to component prop `query`', () => {
const query = 'foobar';
wrapper.vm.handleSearch(query);
expect(wrapper.vm.query).toBe(query);
});
});
describe('handleItemClick', () => {
it('emits `onItemSelect` event on component and sends `assignee` as event param', () => {
const assignee = mockAssigneesList[0];
wrapper.vm.handleItemClick(assignee);
expect(wrapper.emitted().onItemSelect[0]).toEqual([assignee]);
});
});
});
describe('template', () => {
it('renders component container element with class `dropdown-assignees-list`', () => {
expect(wrapper.classes('dropdown-assignees-list')).toBe(true);
});
it('renders loading animation when prop `loading` is true', () => {
wrapper.setProps({ loading: true });
return Vue.nextTick().then(() => {
expect(wrapper.find('.dropdown-loading').exists()).toBe(true);
});
});
it('renders dropdown body elements', () => {
expect(wrapper.find(ListFilter).exists()).toBe(true);
expect(wrapper.find(ListContent).exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import ListContent from 'ee/boards/components/boards_list_selector/list_content.vue';
import { mockAssigneesList } from 'jest/boards/mock_data';
describe('ListContent', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(ListContent, {
propsData: {
items: mockAssigneesList,
listType: 'assignees',
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('emits `onItemSelect` event on component and sends `assignee` as event param', () => {
const assignee = mockAssigneesList[0];
wrapper.vm.handleItemClick(assignee);
expect(wrapper.emitted().onItemSelect[0]).toEqual([assignee]);
});
it('renders component container element with class `dropdown-content`', () => {
expect(wrapper.classes('dropdown-content')).toBe(true);
});
it('renders UL parent element as child within container', () => {
expect(wrapper.find('ul').exists()).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import ListFilter from 'ee/boards/components/boards_list_selector/list_filter.vue';
describe('ListFilter', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(ListFilter);
});
afterEach(() => {
wrapper.destroy();
});
describe('input field', () => {
it('emits `onSearchInput` event on keyup and sends input text as event param', () => {
const input = wrapper.find('input');
input.setValue('foobar');
input.trigger('keyup');
expect(wrapper.emitted().onSearchInput[0]).toEqual(['foobar']);
});
});
describe('clear button', () => {
let input;
beforeEach(() => {
// Pre-populate input field with text
input = wrapper.find('input');
input.setValue('foobar');
input.trigger('keyup');
});
it('clears input field and emits `onSearchInput` event with empty value', () => {
expect(input.element.value).toBe('foobar');
wrapper.find('.dropdown-input-clear').vm.$emit('click');
return Vue.nextTick().then(() => {
expect(input.element.value).toBe('');
expect(wrapper.emitted().onSearchInput[1]).toEqual(['']);
});
});
});
describe('template', () => {
it('renders component container element with class `dropdown-input`', () => {
expect(wrapper.classes('dropdown-input')).toBe(true);
});
it('renders class `has-value` on container element when prop `query` is not empty', () => {
wrapper.setData({ query: 'foobar' });
return Vue.nextTick().then(() => {
expect(wrapper.classes('has-value')).toBe(true);
});
});
it('removes class `has-value` from container element when prop `query` is empty', () => {
wrapper.setData({ query: '' });
return Vue.nextTick().then(() => {
expect(wrapper.classes('has-value')).toBe(false);
});
});
it('renders search input element', () => {
const inputEl = wrapper.find('input.dropdown-input-field');
expect(inputEl.exists()).toBe(true);
expect(inputEl.attributes('placeholder')).toBe('Search');
});
it('renders search input icons', () => {
expect(wrapper.find('.dropdown-input-search').exists()).toBe(true);
expect(wrapper.find('.dropdown-input-clear').exists()).toBe(true);
});
});
});
/* global List */
import Vue from 'vue';
import '~/boards/models/list';
export const mockLabel = {
id: 'gid://gitlab/GroupLabel/121',
title: 'To Do',
......@@ -67,10 +62,6 @@ export const mockLists = [
},
];
export const mockListsWithModel = mockLists.map((listMock) =>
Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
);
const defaultDescendantCounts = {
openedIssues: 0,
closedIssues: 0,
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Issue from 'ee/boards/models/issue';
import List from 'ee/boards/models/list';
import { listObj } from 'jest/boards/mock_data';
import { ListType } from '~/boards/constants';
import CeList from '~/boards/models/list';
describe('List model', () => {
let list;
let issue;
let axiosMock;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
// We need to mock axios since `new List` below makes a network request
axiosMock.onGet().replyOnce(200);
});
afterEach(() => {
list = null;
issue = null;
axiosMock.restore();
});
describe('label lists', () => {
beforeEach(() => {
list = new List(listObj);
issue = new Issue({
title: 'Testing',
id: 2,
iid: 2,
labels: [],
assignees: [],
weight: 5,
});
});
it('inits totalWeight', () => {
expect(list.totalWeight).toBe(0);
});
describe('getIssues', () => {
it('calls CE getIssues', () => {
const ceGetIssues = jest
.spyOn(CeList.prototype, 'getIssues')
.mockReturnValue(Promise.resolve({}));
return list.getIssues().then(() => {
expect(ceGetIssues).toHaveBeenCalled();
});
});
it('sets total weight', () => {
jest.spyOn(CeList.prototype, 'getIssues').mockReturnValue(
Promise.resolve({
total_weight: 11,
}),
);
return list.getIssues().then(() => {
expect(list.totalWeight).toBe(11);
});
});
});
describe('addIssue', () => {
it('updates totalWeight', () => {
list.addIssue(issue);
expect(list.totalWeight).toBe(5);
});
it('calls CE addIssue with all args', () => {
const ceAddIssue = jest.spyOn(CeList.prototype, 'addIssue');
list.addIssue(issue, list, 2);
expect(ceAddIssue).toHaveBeenCalledWith(issue, list, 2);
});
});
describe('removeIssue', () => {
beforeEach(() => {
list.addIssue(issue);
});
it('updates totalWeight', () => {
list.removeIssue(issue);
expect(list.totalWeight).toBe(0);
});
it('calls CE removeIssue', () => {
const ceRemoveIssue = jest.spyOn(CeList.prototype, 'removeIssue');
list.removeIssue(issue);
expect(ceRemoveIssue).toHaveBeenCalledWith(issue);
});
});
});
describe('iteration lists', () => {
const iteration = {
id: 1000,
title: 'Sprint 1',
webUrl: 'https://gitlab.com/h5bp/-/iterations/1',
};
beforeEach(() => {
list = new List({ list_type: ListType.iteration, iteration });
});
it('sets the iteration and title', () => {
expect(list.iteration.id).toBe(iteration.id);
expect(list.title).toBe(iteration.title);
});
});
});
......@@ -5,7 +5,6 @@ import Vuex from 'vuex';
import { BoardType, GroupByParamType, listsQuery, issuableTypes } from 'ee/boards/constants';
import epicCreateMutation from 'ee/boards/graphql/epic_create.mutation.graphql';
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 setWindowLocation from 'helpers/set_window_location_helper';
......@@ -462,16 +461,7 @@ describe('setShowLabels', () => {
});
describe('updateListWipLimit', () => {
let storeMock;
beforeEach(() => {
storeMock = {
state: { endpoints: { listsEndpoint: '/test' } },
create: () => {},
setCurrentBoard: () => {},
};
boardsStoreEE.initEESpecific(storeMock);
jest.mock('axios');
axios.put = jest.fn();
axios.put.mockResolvedValue({ data: {} });
......
import AxiosMockAdapter from 'axios-mock-adapter';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { TEST_HOST } from 'helpers/test_constants';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
describe('BoardsStoreEE', () => {
let setCurrentBoard;
let axiosMock;
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
setCurrentBoard = jest.fn();
// mock CE store
const storeMock = {
state: {},
create() {},
setCurrentBoard,
};
BoardsStoreEE.initEESpecific(storeMock);
});
describe('loadList', () => {
const listPath = `${TEST_HOST}/list/path`;
const listType = 'D-negative';
it('fetches from listPath and stores the result', () => {
const dummyResponse = { uni: 'corn' };
axiosMock.onGet(listPath).replyOnce(200, dummyResponse);
const { state } = BoardsStoreEE.store;
state[listType] = [];
return BoardsStoreEE.loadList(listPath, listType).then(() => {
expect(state[listType]).toEqual(dummyResponse);
});
});
it('displays error if fetching fails', () => {
axiosMock.onGet(listPath).replyOnce(500);
const { state } = BoardsStoreEE.store;
state[listType] = [];
return BoardsStoreEE.loadList(listPath, listType).then(() => {
expect(state[listType]).toEqual([]);
expect(createFlash).toHaveBeenCalled();
});
});
it('does not make a request if response is cached', () => {
const { state } = BoardsStoreEE.store;
state[listType] = ['something'];
return BoardsStoreEE.loadList(listPath, listType).then(() => {
expect(axiosMock.history.get).toHaveLength(0);
});
});
});
describe('setCurrentBoard', () => {
const dummyBoard = 'skateboard';
it('calls setCurrentBoard() of the base store', () => {
BoardsStoreEE.store.setCurrentBoard(dummyBoard);
expect(setCurrentBoard).toHaveBeenCalledWith(dummyBoard);
});
it('resets assignees', () => {
const { state } = BoardsStoreEE.store;
state.assignees = 'some assignees';
BoardsStoreEE.store.setCurrentBoard(dummyBoard);
expect(state.assignees).toEqual([]);
});
it('resets milestones', () => {
const { state } = BoardsStoreEE.store;
state.milestones = 'some milestones';
BoardsStoreEE.store.setCurrentBoard(dummyBoard);
expect(state.milestones).toEqual([]);
});
});
describe('updateWeight', () => {
const dummyEndpoint = `${TEST_HOST}/update/weight`;
const dummyResponse = 'just another response in the network';
const weight = 'elephant';
const expectedRequest = expect.objectContaining({ data: JSON.stringify({ weight }) });
let requestSpy;
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPut(dummyEndpoint).replyOnce((config) => requestSpy(config));
});
it('makes a request to update the weight', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(BoardsStoreEE.store.updateWeight(dummyEndpoint, weight))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(BoardsStoreEE.store.updateWeight(dummyEndpoint, weight))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
});
});
......@@ -4,7 +4,6 @@ import * as types from 'ee/vue_shared/components/sidebar/epics_select/store/muta
import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state';
import { noneEpic } from 'ee/vue_shared/constants';
import testAction from 'helpers/vuex_action_helper';
import boardsStore from '~/boards/stores/boards_store';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -259,35 +258,6 @@ describe('EpicsSelect', () => {
);
});
it('should update the epic associated with the issue in BoardsStore if the update happened in Boards', (done) => {
boardsStore.detail.issue.updateEpic = jest.fn(() => {});
state.issueId = mockIssue.id;
const mockApiData = { ...mockAssignRemoveRes };
mockApiData.epic.web_url = '';
testAction(
actions.receiveIssueUpdateSuccess,
{
data: mockApiData,
epic: normalizedEpics[0],
},
state,
[
{
type: types.RECEIVE_ISSUE_UPDATE_SUCCESS,
payload: {
selectedEpic: normalizedEpics[0],
selectedEpicIssueId: mockApiData.id,
},
},
],
[],
done,
);
expect(boardsStore.detail.issue.updateEpic).toHaveBeenCalled();
});
it('should set updated selectedEpic with noneEpic to state when payload has matching Epic and Issue IDs and isRemoval param is true', (done) => {
state.issueId = mockIssue.id;
......
......@@ -3517,12 +3517,6 @@ msgstr ""
msgid "An error occurred when removing the label."
msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
msgid "An error occurred when updating the issue weight"
msgstr ""
msgid "An error occurred when updating the title"
msgstr ""
......@@ -3619,9 +3613,6 @@ msgstr ""
msgid "An error occurred while fetching reference"
msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
msgid "An error occurred while fetching tags. Retry the search."
msgstr ""
......@@ -31254,9 +31245,6 @@ msgstr ""
msgid "Something went wrong while exporting requirements"
msgstr ""
msgid "Something went wrong while fetching %{listType} list"
msgstr ""
msgid "Something went wrong while fetching branches"
msgstr ""
......@@ -31311,9 +31299,6 @@ msgstr ""
msgid "Something went wrong while merging this merge request. Please try again."
msgstr ""
msgid "Something went wrong while moving issues."
msgstr ""
msgid "Something went wrong while obtaining the Let's Encrypt certificate."
msgstr ""
......
This diff is collapsed.
......@@ -8,7 +8,7 @@ import getters from 'ee_else_ce/boards/stores/getters';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import { mockLists, mockListsWithModel } from '../mock_data';
import { mockLists } from '../mock_data';
Vue.use(Vuex);
......@@ -42,7 +42,7 @@ describe('BoardContent', () => {
});
wrapper = shallowMount(BoardContent, {
propsData: {
lists: mockListsWithModel,
lists: mockLists,
disabled: false,
...props,
},
......@@ -63,7 +63,7 @@ describe('BoardContent', () => {
});
it('renders a BoardColumn component per list', () => {
expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockListsWithModel.length);
expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockLists.length);
});
it('renders BoardContentSidebar', () => {
......
/* global ListIssue */
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import boardsStore from '~/boards/stores/boards_store';
import { setMockEndpoints, mockIssue } from './mock_data';
describe('Issue model', () => {
let issue;
beforeEach(() => {
setMockEndpoints();
boardsStore.create();
issue = new ListIssue(mockIssue);
});
it('has label', () => {
expect(issue.labels.length).toBe(1);
});
it('add new label', () => {
issue.addLabel({
id: 2,
title: 'bug',
color: 'blue',
description: 'bugs!',
});
expect(issue.labels.length).toBe(2);
});
it('does not add label if label id exists', () => {
issue.addLabel({
id: 1,
title: 'test 2',
color: 'blue',
description: 'testing',
});
expect(issue.labels.length).toBe(1);
expect(issue.labels[0].color).toBe('#F0AD4E');
});
it('adds other label with same title', () => {
issue.addLabel({
id: 2,
title: 'test',
color: 'blue',
description: 'other test',
});
expect(issue.labels.length).toBe(2);
});
it('finds label', () => {
const label = issue.findLabel(issue.labels[0]);
expect(label).toBeDefined();
});
it('removes label', () => {
const label = issue.findLabel(issue.labels[0]);
issue.removeLabel(label);
expect(issue.labels.length).toBe(0);
});
it('removes multiple labels', () => {
issue.addLabel({
id: 2,
title: 'bug',
color: 'blue',
description: 'bugs!',
});
expect(issue.labels.length).toBe(2);
issue.removeLabels([issue.labels[0], issue.labels[1]]);
expect(issue.labels.length).toBe(0);
});
it('adds assignee', () => {
issue.addAssignee({
id: 2,
name: 'Bruce Wayne',
username: 'batman',
avatar_url: 'http://batman',
});
expect(issue.assignees.length).toBe(2);
});
it('finds assignee', () => {
const assignee = issue.findAssignee(issue.assignees[0]);
expect(assignee).toBeDefined();
});
it('removes assignee', () => {
const assignee = issue.findAssignee(issue.assignees[0]);
issue.removeAssignee(assignee);
expect(issue.assignees.length).toBe(0);
});
it('removes all assignees', () => {
issue.removeAllAssignees();
expect(issue.assignees.length).toBe(0);
});
it('sets position to infinity if no position is stored', () => {
expect(issue.position).toBe(Infinity);
});
it('sets position', () => {
const relativePositionIssue = new ListIssue({
title: 'Testing',
iid: 1,
confidential: false,
relative_position: 1,
labels: [],
assignees: [],
});
expect(relativePositionIssue.position).toBe(1);
});
it('updates data', () => {
issue.updateData({ subscribed: true });
expect(issue.subscribed).toBe(true);
});
it('sets fetching state', () => {
expect(issue.isFetching.subscriptions).toBe(true);
issue.setFetchingState('subscriptions', false);
expect(issue.isFetching.subscriptions).toBe(false);
});
it('sets loading state', () => {
issue.setLoadingState('foo', true);
expect(issue.isLoading.foo).toBe(true);
});
describe('update', () => {
it('passes update to boardsStore', () => {
jest.spyOn(boardsStore, 'updateIssue').mockImplementation();
issue.update();
expect(boardsStore.updateIssue).toHaveBeenCalledWith(issue);
});
});
});
/* global List */
/* global ListAssignee */
/* global ListIssue */
/* global ListLabel */
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
describe('List model', () => {
let list;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onAny().reply(boardsMockInterceptor);
boardsStore.create();
boardsStore.setEndpoints({
listsEndpoint: '/test/-/boards/1/lists',
});
list = new List(listObj);
return waitForPromises();
});
afterEach(() => {
mock.restore();
});
describe('list type', () => {
const notExpandableList = ['blank'];
const table = Object.keys(ListType).map((k) => {
const value = ListType[k];
return [value, !notExpandableList.includes(value)];
});
it.each(table)(`when list_type is %s boards isExpandable is %p`, (type, result) => {
expect(new List({ id: 1, list_type: type }).isExpandable).toBe(result);
});
});
it('gets issues when created', () => {
expect(list.issues.length).toBe(1);
});
it('saves list and returns ID', () => {
list = new List({
title: 'test',
label: {
id: 1,
title: 'test',
color: '#ff0000',
text_color: 'white',
},
});
return list.save().then(() => {
expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label');
expect(list.position).toBe(0);
expect(list.label).toEqual(listObj.label);
});
});
it('destroys the list', () => {
boardsStore.addList(listObj);
list = boardsStore.findList('id', listObj.id);
expect(boardsStore.state.lists.length).toBe(1);
list.destroy();
return waitForPromises().then(() => {
expect(boardsStore.state.lists.length).toBe(0);
});
});
it('gets issue from list', () => {
const issue = list.findIssue(1);
expect(issue).toBeDefined();
});
it('removes issue', () => {
const issue = list.findIssue(1);
expect(list.issues.length).toBe(1);
list.removeIssue(issue);
expect(list.issues.length).toBe(0);
});
it('sends service request to update issue label', () => {
const listDup = new List(listObjDuplicate);
const issue = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [list.label, listDup.label],
assignees: [],
});
list.issues.push(issue);
listDup.issues.push(issue);
jest.spyOn(boardsStore, 'moveIssue');
listDup.updateIssueLabel(issue, list);
expect(boardsStore.moveIssue).toHaveBeenCalledWith(
issue.id,
list.id,
listDup.id,
undefined,
undefined,
);
});
describe('page number', () => {
beforeEach(() => {
jest.spyOn(list, 'getIssues').mockImplementation(() => {});
list.issues = [];
});
it('increase page number if current issue count is more than the page size', () => {
for (let i = 0; i < 30; i += 1) {
list.issues.push(
new ListIssue({
title: 'Testing',
id: i,
iid: i,
confidential: false,
labels: [list.label],
assignees: [],
}),
);
}
list.issuesSize = 50;
expect(list.issues.length).toBe(30);
list.nextPage();
expect(list.page).toBe(2);
expect(list.getIssues).toHaveBeenCalled();
});
it('does not increase page number if issue count is less than the page size', () => {
list.issues.push(
new ListIssue({
title: 'Testing',
id: 1,
confidential: false,
labels: [list.label],
assignees: [],
}),
);
list.issuesSize = 2;
list.nextPage();
expect(list.page).toBe(1);
expect(list.getIssues).toHaveBeenCalled();
});
});
describe('newIssue', () => {
beforeEach(() => {
jest.spyOn(boardsStore, 'newIssue').mockReturnValue(
Promise.resolve({
data: {
id: 42,
subscribed: false,
assignable_labels_endpoint: '/issue/42/labels',
toggle_subscription_endpoint: '/issue/42/subscriptions',
issue_sidebar_endpoint: '/issue/42/sidebar_info',
},
}),
);
list.issues = [];
});
it('adds new issue to top of list', (done) => {
const user = new ListAssignee({
id: 1,
name: 'testing 123',
username: 'test',
avatar: 'test_image',
});
list.issues.push(
new ListIssue({
title: 'Testing',
id: 1,
confidential: false,
labels: [new ListLabel(list.label)],
assignees: [],
}),
);
const dummyIssue = new ListIssue({
title: 'new issue',
id: 2,
confidential: false,
labels: [new ListLabel(list.label)],
assignees: [user],
subscribed: false,
});
list
.newIssue(dummyIssue)
.then(() => {
expect(list.issues.length).toBe(2);
expect(list.issues[0]).toBe(dummyIssue);
expect(list.issues[0].subscribed).toBe(false);
expect(list.issues[0].assignableLabelsEndpoint).toBe('/issue/42/labels');
expect(list.issues[0].toggleSubscriptionEndpoint).toBe('/issue/42/subscriptions');
expect(list.issues[0].sidebarInfoEndpoint).toBe('/issue/42/sidebar_info');
expect(list.issues[0].labels).toBe(dummyIssue.labels);
expect(list.issues[0].assignees).toBe(dummyIssue.assignees);
})
.then(done)
.catch(done.fail);
});
});
});
/* global List */
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import Vue from 'vue';
import '~/boards/models/list';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
import { __ } from '~/locale';
import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
......@@ -290,20 +285,6 @@ export const boardsMockInterceptor = (config) => {
return [200, body];
};
export const setMockEndpoints = (opts = {}) => {
const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/-/boards.json';
const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists';
const bulkUpdatePath = opts.bulkUpdatePath || '';
const boardId = opts.boardId || '1';
boardsStore.setEndpoints({
boardsEndpoint,
listsEndpoint,
bulkUpdatePath,
boardId,
});
};
export const mockList = {
id: 'gid://gitlab/List/1',
title: 'Open',
......@@ -356,10 +337,6 @@ export const mockLists = [mockList, mockLabelList];
export const mockListsById = keyBy(mockLists, 'id');
export const mockListsWithModel = mockLists.map((listMock) =>
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),
......
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