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 { ...@@ -21,11 +21,6 @@ export default {
}, },
inject: ['canAdminList'], inject: ['canAdminList'],
props: { props: {
lists: {
type: Array,
required: false,
default: () => [],
},
disabled: { disabled: {
type: Boolean, type: Boolean,
required: true, required: true,
......
<script> <script>
import { GlModal, GlAlert } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex'; import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants'; import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility'; import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
...@@ -289,14 +288,10 @@ export default { ...@@ -289,14 +288,10 @@ export default {
setBoardLabels(labels) { setBoardLabels(labels) {
labels.forEach((label) => { labels.forEach((label) => {
if (label.set && !this.board.labels.find((l) => l.id === label.id)) { if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
this.board.labels.push( this.board.labels.push({
new ListLabel({ ...label,
id: label.id,
title: label.title,
color: label.color,
textColor: label.text_color, textColor: label.text_color,
}), });
);
} else if (!label.set) { } else if (!label.set) {
this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id); 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'; ...@@ -4,33 +4,19 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { mapActions } from 'vuex'; 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 toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels'; import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.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 '~/boards/filters/due_date_filters';
import { issuableTypes } from '~/boards/constants'; import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards'; import FilteredSearchBoards from '~/boards/filtered_search_boards';
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards'; import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores'; import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import toggleFocusMode from '~/boards/toggle_focus'; import toggleFocusMode from '~/boards/toggle_focus';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import introspectionQueryResultData from '~/sidebar/fragmentTypes.json'; import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
import { fullBoardId } from './boards_util'; import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle'; import boardConfigToggle from './config_toggle';
...@@ -77,8 +63,6 @@ export default () => { ...@@ -77,8 +63,6 @@ export default () => {
initBoardsFilteredSearch(apolloProvider); initBoardsFilteredSearch(apolloProvider);
} }
boardsStore.create();
// eslint-disable-next-line @gitlab/no-runtime-template-compiler // eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({ issueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
...@@ -116,22 +100,13 @@ export default () => { ...@@ -116,22 +100,13 @@ export default () => {
apolloProvider, apolloProvider,
data() { data() {
return { return {
state: boardsStore.state,
loading: 0, loading: 0,
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
disabled: parseBoolean($boardApp.dataset.disabled), disabled: parseBoolean($boardApp.dataset.disabled),
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: boardsStore.detail,
parent: $boardApp.dataset.parent, parent: $boardApp.dataset.parent,
detailIssueVisible: false,
}; };
}, },
computed: {
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
},
created() { created() {
this.setInitialBoardData({ this.setInitialBoardData({
boardId: $boardApp.dataset.boardId, boardId: $boardApp.dataset.boardId,
...@@ -154,129 +129,29 @@ export default () => { ...@@ -154,129 +129,29 @@ export default () => {
: null, : 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('updateTokens', this.updateTokens);
eventHub.$on('newDetailIssue', this.updateDetailIssue); eventHub.$on('toggleDetailIssue', this.toggleDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens); eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue); eventHub.$off('toggleDetailIssue', this.toggleDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
}, },
mounted() { mounted() {
if (!gon?.features?.issueBoardsFilteredSearch) { if (!gon?.features?.issueBoardsFilteredSearch) {
this.filterManager = new FilteredSearchBoards( this.filterManager = new FilteredSearchBoards({ path: '' }, true, []);
boardsStore.filter,
true,
boardsStore.cantEdit,
);
this.filterManager.setup(); this.filterManager.setup();
} }
this.performSearch(); this.performSearch();
boardsStore.disabled = this.disabled;
}, },
methods: { methods: {
...mapActions(['setInitialBoardData', 'performSearch', 'setError']), ...mapActions(['setInitialBoardData', 'performSearch', 'setError']),
updateTokens() { updateTokens() {
this.filterManager.updateTokens(); this.filterManager.updateTokens();
}, },
updateDetailIssue(newIssue, multiSelect = false) { toggleDetailIssue(hasSidebar) {
const { sidebarInfoEndpoint } = newIssue; this.detailIssueVisible = hasSidebar;
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;
}, },
}, },
}); });
......
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 { ...@@ -18,6 +18,7 @@ import {
} from 'ee_else_ce/boards/constants'; } from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql'; 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 issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import eventHub from '~/boards/eventhub';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -61,10 +62,12 @@ export default { ...@@ -61,10 +62,12 @@ export default {
setActiveId({ commit }, { id, sidebarType }) { setActiveId({ commit }, { id, sidebarType }) {
commit(types.SET_ACTIVE_ID, { id, sidebarType }); commit(types.SET_ACTIVE_ID, { id, sidebarType });
eventHub.$emit('toggleDetailIssue', true);
}, },
unsetActiveId({ dispatch }) { unsetActiveId({ dispatch }) {
dispatch('setActiveId', { id: inactiveId, sidebarType: '' }); dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
eventHub.$emit('toggleDetailIssue', false);
}, },
setFilters: ({ commit, state: { issuableType } }, filters) => { setFilters: ({ commit, state: { issuableType } }, filters) => {
......
/* eslint-disable no-shadow, no-param-reassign, consistent-return */
/* global List */
/* global ListIssue */
import { sortBy } from 'lodash';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { mergeUrlParams, queryToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub';
import ListAssignee from '../models/assignee';
import ListLabel from '../models/label';
import ListMilestone from '../models/milestone';
import IssueProject from '../models/project';
const PER_PAGE = 20;
export const gqlClient = createDefaultClient();
const boardsStore = {
disabled: false,
timeTracking: {
limitToHours: false,
},
scopedLabels: {
enabled: false,
},
filter: {
path: '',
},
state: {
currentBoard: {
labels: [],
},
currentPage: '',
endpoints: {},
},
detail: {
issue: {},
list: {},
},
moving: {
issue: {},
list: {},
},
multiSelect: { list: [] },
setEndpoints({
boardsEndpoint,
listsEndpoint,
bulkUpdatePath,
boardId,
recentBoardsEndpoint,
fullPath,
}) {
const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.state.endpoints = {
boardsEndpoint,
boardId,
listsEndpoint,
listsEndpointGenerate,
bulkUpdatePath,
fullPath,
recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
};
},
create() {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
this.detail = {
issue: {},
list: {},
};
},
showPage(page) {
this.state.currentPage = page;
},
updateListPosition(listObj) {
const listType = listObj.listType || listObj.list_type;
let { position } = listObj;
if (listType === ListType.closed) {
position = Infinity;
} else if (listType === ListType.backlog) {
position = -1;
}
const list = new List({ ...listObj, position });
return list;
},
addList(listObj) {
const list = this.updateListPosition(listObj);
this.state.lists = sortBy([...this.state.lists, list], 'position');
return list;
},
new(listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog');
list
.save()
.then(() => {
list.highlighted = true;
setTimeout(() => {
list.highlighted = false;
}, flashAnimationDuration);
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = sortBy(this.state.lists, 'position');
})
.catch(() => {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
});
},
updateNewListDropdown(listId) {
document
.querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
?.classList.remove('is-active');
},
findIssueLabel(issue, findLabel) {
return issue.labels.find((label) => label.id === findLabel.id);
},
goToNextPage(list) {
if (list.issuesSize > list.issues.length) {
if (list.issues.length / PER_PAGE >= 1) {
list.page += 1;
}
return list.getIssues(false);
}
},
addListIssue(list, issue, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
if (!list.findIssue(issue.id)) {
if (newIndex !== undefined) {
list.issues.splice(newIndex, 0, issue);
if (list.issues[newIndex - 1]) {
moveBeforeId = list.issues[newIndex - 1].id;
}
if (list.issues[newIndex + 1]) {
moveAfterId = list.issues[newIndex + 1].id;
}
} else {
list.issues.push(issue);
}
if (list.label) {
issue.addLabel(list.label);
}
if (list.assignee) {
if (listFrom && listFrom.type === 'assignee') {
issue.removeAssignee(listFrom.assignee);
}
issue.addAssignee(list.assignee);
}
if (IS_EE && list.milestone) {
if (listFrom && listFrom.type === 'milestone') {
issue.removeMilestone(listFrom.milestone);
}
issue.addMilestone(list.milestone);
}
if (listFrom) {
list.issuesSize += 1;
list.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
}
}
},
findListIssue(list, id) {
return list.issues.find((issue) => issue.id === id);
},
removeList(id) {
const list = this.findList('id', id);
if (!list) return;
this.state.lists = this.state.lists.filter((list) => list.id !== id);
},
moveList(listFrom, orderLists) {
orderLists.forEach((id, i) => {
const list = this.findList('id', parseInt(id, 10));
list.position = i;
});
listFrom.update();
},
addMultipleListIssues(list, issues, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
const listHasIssues = issues.every((issue) => list.findIssue(issue.id));
if (!listHasIssues) {
if (newIndex !== undefined) {
if (list.issues[newIndex - 1]) {
moveBeforeId = list.issues[newIndex - 1].id;
}
if (list.issues[newIndex]) {
moveAfterId = list.issues[newIndex].id;
}
list.issues.splice(newIndex, 0, ...issues);
} else {
list.issues.push(...issues);
}
if (list.label) {
issues.forEach((issue) => issue.addLabel(list.label));
}
if (list.assignee) {
if (listFrom && listFrom.type === 'assignee') {
issues.forEach((issue) => issue.removeAssignee(listFrom.assignee));
}
issues.forEach((issue) => issue.addAssignee(list.assignee));
}
if (IS_EE && list.milestone) {
if (listFrom && listFrom.type === 'milestone') {
issues.forEach((issue) => issue.removeMilestone(listFrom.milestone));
}
issues.forEach((issue) => issue.addMilestone(list.milestone));
}
if (listFrom) {
list.issuesSize += issues.length;
list.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
}
}
},
removeListIssues(list, removeIssue) {
list.issues = list.issues.filter((issue) => {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
list.issuesSize -= 1;
issue.removeLabel(list.label);
}
return !matchesRemove;
});
},
removeListMultipleIssues(list, removeIssues) {
const ids = removeIssues.map((issue) => issue.id);
list.issues = list.issues.filter((issue) => {
const matchesRemove = ids.includes(issue.id);
if (matchesRemove) {
list.issuesSize -= 1;
issue.removeLabel(list.label);
}
return !matchesRemove;
});
},
startMoving(list, issue) {
Object.assign(this.moving, { list, issue });
},
onNewListIssueResponse(list, issue, data) {
issue.refreshData(data);
if (list.issues.length > 1) {
const moveBeforeId = list.issues[1].id;
this.moveIssue(issue.id, null, null, null, moveBeforeId);
}
},
moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) {
const issueTo = issues.map((issue) => listTo.findIssue(issue.id));
const issueLists = issues.map((issue) => issue.getLists()).flat();
const listLabels = issueLists.map((list) => list.label);
const hasMoveableIssues = issueTo.filter(Boolean).length > 0;
if (!hasMoveableIssues) {
// Check if target list assignee is already present in this issue
if (
listTo.type === ListType.assignee &&
listFrom.type === ListType.assignee &&
issues.some((issue) => issue.findAssignee(listTo.assignee))
) {
const targetIssues = issues.map((issue) => listTo.findIssue(issue.id));
targetIssues.forEach((targetIssue) => targetIssue.removeAssignee(listFrom.assignee));
} else if (listTo.type === 'milestone') {
const currentMilestones = issues.map((issue) => issue.milestone);
const currentLists = this.state.lists
.filter((list) => list.type === 'milestone' && list.id !== listTo.id)
.filter((list) =>
list.issues.some((listIssue) => issues.some((issue) => listIssue.id === issue.id)),
);
issues.forEach((issue) => {
currentMilestones.forEach((milestone) => {
issue.removeMilestone(milestone);
});
});
issues.forEach((issue) => {
issue.addMilestone(listTo.milestone);
});
currentLists.forEach((currentList) => {
issues.forEach((issue) => {
currentList.removeIssue(issue);
});
});
listTo.addMultipleIssues(issues, listFrom, newIndex);
} else {
// Add to new lists issues if it doesn't already exist
listTo.addMultipleIssues(issues, listFrom, newIndex);
}
} else {
listTo.updateMultipleIssues(issues, listFrom);
issues.forEach((issue) => {
issue.removeLabel(listFrom.label);
});
}
if (listTo.type === ListType.closed && listFrom.type !== ListType.backlog) {
issueLists.forEach((list) => {
issues.forEach((issue) => {
list.removeIssue(issue);
});
});
issues.forEach((issue) => {
issue.removeLabels(listLabels);
});
} else if (listTo.type === ListType.backlog && listFrom.type === ListType.assignee) {
issues.forEach((issue) => {
issue.removeAssignee(listFrom.assignee);
});
issueLists.forEach((list) => {
issues.forEach((issue) => {
list.removeIssue(issue);
});
});
} else if (listTo.type === ListType.backlog && listFrom.type === ListType.milestone) {
issues.forEach((issue) => {
issue.removeMilestone(listFrom.milestone);
});
issueLists.forEach((list) => {
issues.forEach((issue) => {
list.removeIssue(issue);
});
});
} else if (
this.shouldRemoveIssue(listFrom, listTo) &&
this.issuesAreContiguous(listFrom, issues)
) {
listFrom.removeMultipleIssues(issues);
}
},
issuesAreContiguous(list, issues) {
// When there's only 1 issue selected, we can return early.
if (issues.length === 1) return true;
// Create list of ids for issues involved.
const listIssueIds = list.issues.map((issue) => issue.id);
const movedIssueIds = issues.map((issue) => issue.id);
// Check if moved issue IDs is sub-array
// of source list issue IDs (i.e. contiguous selection).
return listIssueIds.join('|').includes(movedIssueIds.join('|'));
},
moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
const listLabels = issueLists.map((listIssue) => listIssue.label);
if (!issueTo) {
// Check if target list assignee is already present in this issue
if (
listTo.type === 'assignee' &&
listFrom.type === 'assignee' &&
issue.findAssignee(listTo.assignee)
) {
const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee);
} else if (listTo.type === 'milestone') {
const currentMilestone = issue.milestone;
const currentLists = this.state.lists
.filter((list) => list.type === 'milestone' && list.id !== listTo.id)
.filter((list) => list.issues.some((listIssue) => issue.id === listIssue.id));
issue.removeMilestone(currentMilestone);
issue.addMilestone(listTo.milestone);
currentLists.forEach((currentList) => currentList.removeIssue(issue));
listTo.addIssue(issue, listFrom, newIndex);
} else {
// Add to new lists issues if it doesn't already exist
listTo.addIssue(issue, listFrom, newIndex);
}
} else {
listTo.updateIssueLabel(issue, listFrom);
issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
} else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
issue.removeAssignee(listFrom.assignee);
listFrom.removeIssue(issue);
} else if (listTo.type === 'backlog' && listFrom.type === 'milestone') {
issue.removeMilestone(listFrom.milestone);
listFrom.removeIssue(issue);
} else if (this.shouldRemoveIssue(listFrom, listTo)) {
listFrom.removeIssue(issue);
}
},
shouldRemoveIssue(listFrom, listTo) {
return (
(listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') ||
listFrom.type === 'backlog' ||
listFrom.type === 'closed'
);
},
moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
moveMultipleIssuesInList({ list, issues, oldIndicies, newIndex, idArray }) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + issues.length], 10) || null;
list.moveMultipleIssues({
issues,
oldIndicies,
newIndex,
moveBeforeId: beforeId,
moveAfterId: afterId,
});
},
findList(key, val) {
return this.state.lists.find((list) => list[key] === val);
},
findListByLabelId(id) {
return this.state.lists.find((list) => list.type === 'label' && list.label.id === id);
},
toggleFilter(filter) {
const filterPath = this.filter.path.split('&');
const filterIndex = filterPath.indexOf(filter);
if (filterIndex === -1) {
filterPath.push(filter);
} else {
filterPath.splice(filterIndex, 1);
}
this.filter.path = filterPath.join('&');
this.updateFiltersUrl();
eventHub.$emit('updateTokens');
},
setListDetail(newList) {
this.detail.list = newList;
},
updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
},
clearDetailIssue() {
this.setIssueDetail({});
},
setIssueDetail(issueDetail) {
this.detail.issue = issueDetail;
},
setTimeTrackingLimitToHours(limitToHours) {
this.timeTracking.limitToHours = parseBoolean(limitToHours);
},
generateBoardGid(boardId) {
return `gid://gitlab/Board/${boardId}`;
},
generateBoardsPath(id) {
return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`;
},
generateIssuesPath(id) {
return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`;
},
generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
id ? `/${id}` : ''
}`;
},
generateMultiDragPath(boardId) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues/bulk_move`;
},
all() {
return axios.get(this.state.endpoints.listsEndpoint);
},
createList(entityId, entityType) {
const list = {
[entityType]: entityId,
};
return axios.post(this.state.endpoints.listsEndpoint, {
list,
});
},
updateList(id, position, collapsed) {
return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, {
list: {
position,
collapsed,
},
});
},
updateListFunc(list) {
const collapsed = !list.isExpanded;
return this.updateList(list.id, list.position, collapsed).catch(() => {
// TODO: handle request error
});
},
destroyList(id) {
return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`);
},
destroy(list) {
const index = this.state.lists.indexOf(list);
this.state.lists.splice(index, 1);
this.updateNewListDropdown(list.id);
this.destroyList(list.id).catch(() => {
// TODO: handle request error
});
},
saveList(list) {
const entity = list.label || list.assignee || list.milestone || list.iteration;
let entityType = '';
if (list.label) {
entityType = 'label_id';
} else if (list.assignee) {
entityType = 'assignee_id';
} else if (IS_EE && list.milestone) {
entityType = 'milestone_id';
} else if (IS_EE && list.iteration) {
entityType = 'iteration_id';
}
return this.createList(entity.id, entityType)
.then((res) => res.data)
.then((data) => {
list.id = data.id;
list.type = data.list_type;
list.position = data.position;
list.label = data.label;
return list.getIssues();
});
},
getListIssues(list, emptyIssues = true) {
const data = {
...queryToObject(this.filter.path, { gatherArrays: true }),
page: list.page,
};
if (list.label && data.label_name) {
data.label_name = data.label_name.filter((label) => label !== list.label.title);
}
if (emptyIssues) {
list.loading = true;
}
return this.getIssuesForList(list.id, data)
.then((res) => res.data)
.then((data) => {
list.loading = false;
list.issuesSize = data.size;
if (emptyIssues) {
list.issues = [];
}
data.issues.forEach((issueObj) => {
list.addIssue(new ListIssue(issueObj));
});
return data;
});
},
getIssuesForList(id, filter = {}) {
const data = { id };
Object.keys(filter).forEach((key) => {
data[key] = filter[key];
});
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
},
moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), {
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
});
},
moveListIssues(list, issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
list.issues.splice(oldIndex, 1);
list.issues.splice(newIndex, 0, issue);
this.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
// TODO: handle request error
});
},
moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }) {
return axios.put(this.generateMultiDragPath(this.state.endpoints.boardId), {
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
ids,
});
},
moveListMultipleIssues({ list, issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) {
oldIndicies.reverse().forEach((index) => {
list.issues.splice(index, 1);
});
list.issues.splice(newIndex, 0, ...issues);
return this.moveMultipleIssues({
ids: issues.map((issue) => issue.id),
fromListId: null,
toListId: null,
moveBeforeId,
moveAfterId,
});
},
newIssue(id, issue) {
if (typeof id === 'string') {
id = getIdFromGraphQLId(id);
}
return axios.post(this.generateIssuesPath(id), {
issue,
});
},
newListIssue(list, issue) {
list.addIssue(issue, null, 0);
list.issuesSize += 1;
let listId = list.id;
if (typeof listId === 'string') {
listId = getIdFromGraphQLId(listId);
}
return this.newIssue(list.id, issue)
.then((res) => res.data)
.then((data) => list.onNewIssueResponse(issue, data));
},
getBacklog(data) {
return axios.get(
mergeUrlParams(
data,
`${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`,
),
);
},
removeIssueLabel(issue, removeLabel) {
if (removeLabel) {
issue.labels = issue.labels.filter((label) => removeLabel.id !== label.id);
}
},
addIssueAssignee(issue, assignee) {
if (!issue.findAssignee(assignee)) {
issue.assignees.push(new ListAssignee(assignee));
}
},
setIssueAssignees(issue, assignees) {
issue.assignees = [...assignees];
},
removeIssueLabels(issue, labels) {
labels.forEach(issue.removeLabel.bind(issue));
},
bulkUpdate(issueIds, extraData = {}) {
const data = {
update: Object.assign(extraData, {
issuable_ids: issueIds.join(','),
}),
};
return axios.post(this.state.endpoints.bulkUpdatePath, data);
},
getIssueInfo(endpoint) {
return axios.get(endpoint);
},
toggleIssueSubscription(endpoint) {
return axios.post(endpoint);
},
recentBoards() {
return axios.get(this.state.endpoints.recentBoardsEndpoint);
},
setCurrentBoard(board) {
this.state.currentBoard = board;
},
toggleMultiSelect(issue) {
const selectedIssueIds = this.multiSelect.list.map((issue) => issue.id);
const index = selectedIssueIds.indexOf(issue.id);
if (index === -1) {
this.multiSelect.list.push(issue);
return;
}
this.multiSelect.list = [
...this.multiSelect.list.slice(0, index),
...this.multiSelect.list.slice(index + 1),
];
},
removeIssueAssignee(issue, removeAssignee) {
if (removeAssignee) {
issue.assignees = issue.assignees.filter((assignee) => assignee.id !== removeAssignee.id);
}
},
findIssueAssignee(issue, findAssignee) {
return issue.assignees.find((assignee) => assignee.id === findAssignee.id);
},
clearMultiSelect() {
this.multiSelect.list = [];
},
removeAllIssueAssignees(issue) {
issue.assignees = [];
},
addIssueMilestone(issue, milestone) {
const miletoneId = issue.milestone ? issue.milestone.id : null;
if (IS_EE && milestone.id !== miletoneId) {
issue.milestone = new ListMilestone(milestone);
}
},
setIssueLoadingState(issue, key, value) {
issue.isLoading[key] = value;
},
updateIssueData(issue, newData) {
Object.assign(issue, newData);
},
setIssueFetchingState(issue, key, value) {
issue.isFetching[key] = value;
},
removeIssueMilestone(issue, removeMilestone) {
if (IS_EE && removeMilestone && removeMilestone.id === issue.milestone.id) {
issue.milestone = {};
}
},
refreshIssueData(issue, obj) {
const convertedObj = convertObjectPropsToCamelCase(obj, {
dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'],
});
convertedObj.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
issue.path = obj.real_path || obj.webUrl;
issue.project_id = obj.project_id;
Object.assign(issue, convertedObj);
if (obj.project) {
issue.project = new IssueProject(obj.project);
}
if (obj.milestone) {
issue.milestone = new ListMilestone(obj.milestone);
issue.milestone_id = obj.milestone.id;
}
if (obj.labels) {
issue.labels = obj.labels.map((label) => new ListLabel(label));
}
if (obj.assignees) {
issue.assignees = obj.assignees.map((a) => new ListAssignee(a));
}
},
addIssueLabel(issue, label) {
if (!issue.findLabel(label)) {
issue.labels.push(new ListLabel(label));
}
},
updateIssue(issue) {
const data = {
issue: {
milestone_id: issue.milestone ? issue.milestone.id : null,
due_date: issue.dueDate,
assignee_ids: issue.assignees.length > 0 ? issue.assignees.map(({ id }) => id) : [0],
label_ids: issue.labels.length > 0 ? issue.labels.map(({ id }) => id) : [''],
},
};
return axios.patch(`${issue.path}.json`, data).then(({ data: body = {} } = {}) => {
/**
* Since post implementation of Scoped labels, server can reject
* same key-ed labels. To keep the UI and server Model consistent,
* we're just assigning labels that server echo's back to us when we
* PATCH the said object.
*/
if (body) {
issue.labels = convertObjectPropsToCamelCase(body.labels, { deep: true });
}
});
},
};
BoardsStoreEE.initEESpecific(boardsStore);
export default boardsStore;
// 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 @@ ...@@ -18,5 +18,5 @@
= render 'shared/issuable/search_bar', type: :boards, board: board = 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-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 %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'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default class ListLabel { export default class ListLabel {
......
...@@ -92,11 +92,8 @@ export default () => { ...@@ -92,11 +92,8 @@ export default () => {
state: {}, state: {},
loading: 0, loading: 0,
allowSubEpics: parseBoolean($boardApp.dataset.subEpicsFeatureAvailable), allowSubEpics: parseBoolean($boardApp.dataset.subEpicsFeatureAvailable),
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
disabled: parseBoolean($boardApp.dataset.disabled), disabled: parseBoolean($boardApp.dataset.disabled),
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
parent: $boardApp.dataset.parent, parent: $boardApp.dataset.parent,
detailIssueVisible: false, detailIssueVisible: false,
}; };
......
import Api from 'ee/api'; import Api from 'ee/api';
import { noneEpic } from 'ee/vue_shared/constants'; import { noneEpic } from 'ee/vue_shared/constants';
import boardsStore from '~/boards/stores/boards_store';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; 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'; import * as types from './mutation_types';
...@@ -59,43 +57,8 @@ export const fetchEpics = ({ state, dispatch }, search = '') => { ...@@ -59,43 +57,8 @@ export const fetchEpics = ({ state, dispatch }, search = '') => {
export const requestIssueUpdate = ({ commit }) => commit(types.REQUEST_ISSUE_UPDATE); export const requestIssueUpdate = ({ commit }) => commit(types.REQUEST_ISSUE_UPDATE);
export const receiveIssueUpdateSuccess = ({ state, commit }, { data, epic, isRemoval = false }) => { 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 // Verify if update was successful
if (data.epic.id === epic.id && data.issue.id === state.issueId) { 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, { commit(types.RECEIVE_ISSUE_UPDATE_SUCCESS, {
selectedEpic: isRemoval ? noneEpic : epic, selectedEpic: isRemoval ? noneEpic : epic,
selectedEpicIssueId: data.id, 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 = { export const mockLabel = {
id: 'gid://gitlab/GroupLabel/121', id: 'gid://gitlab/GroupLabel/121',
title: 'To Do', title: 'To Do',
...@@ -67,10 +62,6 @@ export const mockLists = [ ...@@ -67,10 +62,6 @@ export const mockLists = [
}, },
]; ];
export const mockListsWithModel = mockLists.map((listMock) =>
Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
);
const defaultDescendantCounts = { const defaultDescendantCounts = {
openedIssues: 0, openedIssues: 0,
closedIssues: 0, closedIssues: 0,
......
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'; ...@@ -5,7 +5,6 @@ import Vuex from 'vuex';
import { BoardType, GroupByParamType, listsQuery, issuableTypes } from 'ee/boards/constants'; import { BoardType, GroupByParamType, listsQuery, issuableTypes } from 'ee/boards/constants';
import epicCreateMutation from 'ee/boards/graphql/epic_create.mutation.graphql'; import epicCreateMutation from 'ee/boards/graphql/epic_create.mutation.graphql';
import actions, { gqlClient } from 'ee/boards/stores/actions'; 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 * as types from 'ee/boards/stores/mutation_types';
import mutations from 'ee/boards/stores/mutations'; import mutations from 'ee/boards/stores/mutations';
import setWindowLocation from 'helpers/set_window_location_helper'; import setWindowLocation from 'helpers/set_window_location_helper';
...@@ -462,16 +461,7 @@ describe('setShowLabels', () => { ...@@ -462,16 +461,7 @@ describe('setShowLabels', () => {
}); });
describe('updateListWipLimit', () => { describe('updateListWipLimit', () => {
let storeMock;
beforeEach(() => { beforeEach(() => {
storeMock = {
state: { endpoints: { listsEndpoint: '/test' } },
create: () => {},
setCurrentBoard: () => {},
};
boardsStoreEE.initEESpecific(storeMock);
jest.mock('axios'); jest.mock('axios');
axios.put = jest.fn(); axios.put = jest.fn();
axios.put.mockResolvedValue({ data: {} }); 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 ...@@ -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 createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state';
import { noneEpic } from 'ee/vue_shared/constants'; import { noneEpic } from 'ee/vue_shared/constants';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import boardsStore from '~/boards/stores/boards_store';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -259,35 +258,6 @@ describe('EpicsSelect', () => { ...@@ -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) => { 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; state.issueId = mockIssue.id;
......
...@@ -3517,12 +3517,6 @@ msgstr "" ...@@ -3517,12 +3517,6 @@ msgstr ""
msgid "An error occurred when removing the label." msgid "An error occurred when removing the label."
msgstr "" 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" msgid "An error occurred when updating the title"
msgstr "" msgstr ""
...@@ -3619,9 +3613,6 @@ msgstr "" ...@@ -3619,9 +3613,6 @@ msgstr ""
msgid "An error occurred while fetching reference" msgid "An error occurred while fetching reference"
msgstr "" msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
msgid "An error occurred while fetching tags. Retry the search." msgid "An error occurred while fetching tags. Retry the search."
msgstr "" msgstr ""
...@@ -31254,9 +31245,6 @@ msgstr "" ...@@ -31254,9 +31245,6 @@ msgstr ""
msgid "Something went wrong while exporting requirements" msgid "Something went wrong while exporting requirements"
msgstr "" msgstr ""
msgid "Something went wrong while fetching %{listType} list"
msgstr ""
msgid "Something went wrong while fetching branches" msgid "Something went wrong while fetching branches"
msgstr "" msgstr ""
...@@ -31311,9 +31299,6 @@ msgstr "" ...@@ -31311,9 +31299,6 @@ msgstr ""
msgid "Something went wrong while merging this merge request. Please try again." msgid "Something went wrong while merging this merge request. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong while moving issues."
msgstr ""
msgid "Something went wrong while obtaining the Let's Encrypt certificate." msgid "Something went wrong while obtaining the Let's Encrypt certificate."
msgstr "" msgstr ""
......
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import eventHub from '~/boards/eventhub';
import ListIssue from '~/boards/models/issue';
import List from '~/boards/models/list';
import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
import { listObj, listObjDuplicate } from './mock_data';
jest.mock('js-cookie');
const createTestIssue = () => ({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [],
assignees: [],
});
describe('boardsStore', () => {
const dummyResponse = "without type checking this doesn't matter";
const boardId = 'dummy-board-id';
const endpoints = {
boardsEndpoint: `${TEST_HOST}/boards`,
listsEndpoint: `${TEST_HOST}/lists`,
bulkUpdatePath: `${TEST_HOST}/bulk/update`,
recentBoardsEndpoint: `${TEST_HOST}/recent/boards`,
};
let axiosMock;
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
boardsStore.setEndpoints({
...endpoints,
boardId,
});
});
afterEach(() => {
axiosMock.restore();
});
const setupDefaultResponses = () => {
axiosMock
.onGet(`${endpoints.listsEndpoint}/${listObj.id}/issues?id=${listObj.id}&page=1`)
.reply(200, { issues: [createTestIssue()] });
axiosMock.onPost(endpoints.listsEndpoint).reply(200, listObj);
axiosMock.onPut();
};
describe('all', () => {
it('makes a request to fetch lists', () => {
axiosMock.onGet(endpoints.listsEndpoint).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.all()).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(endpoints.listsEndpoint).replyOnce(500);
return expect(boardsStore.all()).rejects.toThrow();
});
});
describe('createList', () => {
const entityType = 'moorhen';
const entityId = 'quack';
const expectedRequest = expect.objectContaining({
data: JSON.stringify({ list: { [entityType]: entityId } }),
});
let requestSpy;
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPost(endpoints.listsEndpoint).replyOnce((config) => requestSpy(config));
});
it('makes a request to create a list', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.createList(entityId, entityType))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(boardsStore.createList(entityId, entityType))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
});
describe('updateList', () => {
const id = 'David Webb';
const position = 'unknown';
const collapsed = false;
const expectedRequest = expect.objectContaining({
data: JSON.stringify({ list: { position, collapsed } }),
});
let requestSpy;
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPut(`${endpoints.listsEndpoint}/${id}`).replyOnce((config) => requestSpy(config));
});
it('makes a request to update a list position', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.updateList(id, position, collapsed))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(boardsStore.updateList(id, position, collapsed))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
});
describe('destroyList', () => {
const id = '-42';
let requestSpy;
beforeEach(() => {
requestSpy = jest.fn();
axiosMock
.onDelete(`${endpoints.listsEndpoint}/${id}`)
.replyOnce((config) => requestSpy(config));
});
it('makes a request to delete a list', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.destroyList(id))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalled();
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(boardsStore.destroyList(id))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalled();
});
});
});
describe('saveList', () => {
let list;
beforeEach(() => {
list = new List(listObj);
setupDefaultResponses();
});
it('makes a request to save a list', () => {
const expectedResponse = expect.objectContaining({ issues: [createTestIssue()] });
const expectedListValue = {
id: listObj.id,
position: listObj.position,
type: listObj.list_type,
label: listObj.label,
};
expect(list.id).toBe(listObj.id);
expect(list.position).toBe(listObj.position);
expect(list).toMatchObject(expectedListValue);
return expect(boardsStore.saveList(list)).resolves.toEqual(expectedResponse);
});
});
describe('getListIssues', () => {
let list;
beforeEach(() => {
list = new List(listObj);
setupDefaultResponses();
});
it('makes a request to get issues', () => {
const expectedResponse = expect.objectContaining({ issues: [createTestIssue()] });
expect(list.issues).toEqual([]);
return expect(boardsStore.getListIssues(list, true)).resolves.toEqual(expectedResponse);
});
});
describe('getIssuesForList', () => {
const id = 'TOO-MUCH';
const url = `${endpoints.listsEndpoint}/${id}/issues?id=${id}`;
it('makes a request to fetch list issues', () => {
axiosMock.onGet(url).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.getIssuesForList(id)).resolves.toEqual(expectedResponse);
});
it('makes a request to fetch list issues with filter', () => {
const filter = { algal: 'scrubber' };
axiosMock.onGet(`${url}&algal=scrubber`).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(url).replyOnce(500);
return expect(boardsStore.getIssuesForList(id)).rejects.toThrow();
});
});
describe('moveIssue', () => {
const urlRoot = 'potato';
const id = 'over 9000';
const fromListId = 'left';
const toListId = 'right';
const moveBeforeId = 'up';
const moveAfterId = 'down';
const expectedRequest = expect.objectContaining({
data: JSON.stringify({
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
}),
});
let requestSpy;
beforeAll(() => {
global.gon.relative_url_root = urlRoot;
});
afterAll(() => {
delete global.gon.relative_url_root;
});
beforeEach(() => {
requestSpy = jest.fn();
axiosMock
.onPut(`${urlRoot}/-/boards/${boardId}/issues/${id}`)
.replyOnce((config) => requestSpy(config));
});
it('makes a request to move an issue between lists', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
});
describe('newIssue', () => {
const id = 1;
const issue = { some: 'issue data' };
const url = `${endpoints.listsEndpoint}/${id}/issues`;
const expectedRequest = expect.objectContaining({
data: JSON.stringify({
issue,
}),
});
let requestSpy;
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPost(url).replyOnce((config) => requestSpy(config));
});
it('makes a request to create a new issue', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.newIssue(id, issue))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(boardsStore.newIssue(id, issue))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
});
describe('getBacklog', () => {
const urlRoot = 'deep';
const url = `${urlRoot}/-/boards/${boardId}/issues.json?not=relevant`;
const requestParams = {
not: 'relevant',
};
beforeAll(() => {
global.gon.relative_url_root = urlRoot;
});
afterAll(() => {
delete global.gon.relative_url_root;
});
it('makes a request to fetch backlog', () => {
axiosMock.onGet(url).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.getBacklog(requestParams)).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(url).replyOnce(500);
return expect(boardsStore.getBacklog(requestParams)).rejects.toThrow();
});
});
describe('bulkUpdate', () => {
const issueIds = [1, 2, 3];
const extraData = { moar: 'data' };
const expectedRequest = expect.objectContaining({
data: JSON.stringify({
update: {
...extraData,
issuable_ids: '1,2,3',
},
}),
});
let requestSpy;
beforeEach(() => {
requestSpy = jest.fn();
axiosMock.onPost(endpoints.bulkUpdatePath).replyOnce((config) => requestSpy(config));
});
it('makes a request to create a list', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.bulkUpdate(issueIds, extraData))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
return expect(boardsStore.bulkUpdate(issueIds, extraData))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
});
});
});
describe('getIssueInfo', () => {
const dummyEndpoint = `${TEST_HOST}/some/where`;
it('makes a request to the given endpoint', () => {
axiosMock.onGet(dummyEndpoint).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(dummyEndpoint).replyOnce(500);
return expect(boardsStore.getIssueInfo(dummyEndpoint)).rejects.toThrow();
});
});
describe('toggleIssueSubscription', () => {
const dummyEndpoint = `${TEST_HOST}/some/where`;
it('makes a request to the given endpoint', () => {
axiosMock.onPost(dummyEndpoint).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual(
expectedResponse,
);
});
it('fails for error response', () => {
axiosMock.onPost(dummyEndpoint).replyOnce(500);
return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow();
});
});
describe('recentBoards', () => {
const url = `${endpoints.recentBoardsEndpoint}.json`;
it('makes a request to fetch all boards', () => {
axiosMock.onGet(url).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(boardsStore.recentBoards()).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(url).replyOnce(500);
return expect(boardsStore.recentBoards()).rejects.toThrow();
});
});
describe('when created', () => {
beforeEach(() => {
setupDefaultResponses();
jest.spyOn(boardsStore, 'moveIssue').mockReturnValue(Promise.resolve());
jest.spyOn(boardsStore, 'moveMultipleIssues').mockReturnValue(Promise.resolve());
boardsStore.create();
});
it('starts with a blank state', () => {
expect(boardsStore.state.lists.length).toBe(0);
});
describe('addList', () => {
it('sorts by position', () => {
boardsStore.addList({ position: 2 });
boardsStore.addList({ position: 1 });
expect(boardsStore.state.lists.map(({ position }) => position)).toEqual([1, 2]);
});
});
describe('toggleFilter', () => {
const dummyFilter = 'x=42';
let updateTokensSpy;
beforeEach(() => {
updateTokensSpy = jest.fn();
eventHub.$once('updateTokens', updateTokensSpy);
// prevent using window.history
jest.spyOn(boardsStore, 'updateFiltersUrl').mockReturnValue();
});
it('adds the filter if it is not present', () => {
boardsStore.filter.path = 'something';
boardsStore.toggleFilter(dummyFilter);
expect(boardsStore.filter.path).toEqual(`something&${dummyFilter}`);
expect(updateTokensSpy).toHaveBeenCalled();
expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
});
it('removes the filter if it is present', () => {
boardsStore.filter.path = `something&${dummyFilter}`;
boardsStore.toggleFilter(dummyFilter);
expect(boardsStore.filter.path).toEqual('something');
expect(updateTokensSpy).toHaveBeenCalled();
expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
});
});
describe('lists', () => {
it('creates new list without persisting to DB', () => {
expect(boardsStore.state.lists.length).toBe(0);
boardsStore.addList(listObj);
expect(boardsStore.state.lists.length).toBe(1);
});
it('finds list by ID', () => {
boardsStore.addList(listObj);
const list = boardsStore.findList('id', listObj.id);
expect(list.id).toBe(listObj.id);
});
it('finds list by type', () => {
boardsStore.addList(listObj);
const list = boardsStore.findList('type', 'label');
expect(list).toBeDefined();
});
it('finds list by label ID', () => {
boardsStore.addList(listObj);
const list = boardsStore.findListByLabelId(listObj.label.id);
expect(list.id).toBe(listObj.id);
});
it('gets issue when new list added', () => {
boardsStore.addList(listObj);
const list = boardsStore.findList('id', listObj.id);
expect(boardsStore.state.lists.length).toBe(1);
return axios.waitForAll().then(() => {
expect(list.issues.length).toBe(1);
expect(list.issues[0].id).toBe(1);
});
});
it('persists new list', () => {
boardsStore.new({
title: 'Test',
list_type: 'label',
label: {
id: 1,
title: 'Testing',
color: 'red',
description: 'testing;',
},
});
expect(boardsStore.state.lists.length).toBe(1);
return axios.waitForAll().then(() => {
const list = boardsStore.findList('id', listObj.id);
expect(list).toEqual(
expect.objectContaining({
id: listObj.id,
position: 0,
}),
);
});
});
it('removes list from state', () => {
boardsStore.addList(listObj);
expect(boardsStore.state.lists.length).toBe(1);
boardsStore.removeList(listObj.id);
expect(boardsStore.state.lists.length).toBe(0);
});
it('moves the position of lists', () => {
const listOne = boardsStore.addList(listObj);
boardsStore.addList(listObjDuplicate);
expect(boardsStore.state.lists.length).toBe(2);
boardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]);
expect(listOne.position).toBe(1);
});
it('moves an issue from one list to another', () => {
const listOne = boardsStore.addList(listObj);
const listTwo = boardsStore.addList(listObjDuplicate);
expect(boardsStore.state.lists.length).toBe(2);
return axios.waitForAll().then(() => {
expect(listOne.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
expect(listOne.issues.length).toBe(0);
expect(listTwo.issues.length).toBe(1);
});
});
it('moves an issue from backlog to a list', () => {
const backlog = boardsStore.addList({
...listObj,
list_type: 'backlog',
});
const listTwo = boardsStore.addList(listObjDuplicate);
expect(boardsStore.state.lists.length).toBe(2);
return axios.waitForAll().then(() => {
expect(backlog.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
boardsStore.moveIssueToList(backlog, listTwo, backlog.findIssue(1));
expect(backlog.issues.length).toBe(0);
expect(listTwo.issues.length).toBe(1);
});
});
it('moves issue to top of another list', () => {
const listOne = boardsStore.addList(listObj);
const listTwo = boardsStore.addList(listObjDuplicate);
expect(boardsStore.state.lists.length).toBe(2);
return axios.waitForAll().then(() => {
listOne.issues[0].id = 2;
expect(listOne.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0);
expect(listOne.issues.length).toBe(0);
expect(listTwo.issues.length).toBe(2);
expect(listTwo.issues[0].id).toBe(2);
expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1);
});
});
it('moves issue to bottom of another list', () => {
const listOne = boardsStore.addList(listObj);
const listTwo = boardsStore.addList(listObjDuplicate);
expect(boardsStore.state.lists.length).toBe(2);
return axios.waitForAll().then(() => {
listOne.issues[0].id = 2;
expect(listOne.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1);
expect(listOne.issues.length).toBe(0);
expect(listTwo.issues.length).toBe(2);
expect(listTwo.issues[1].id).toBe(2);
expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null);
});
});
it('moves issue in list', () => {
const issue = new ListIssue({
title: 'Testing',
id: 2,
iid: 2,
confidential: false,
labels: [],
assignees: [],
});
const list = boardsStore.addList(listObj);
return axios.waitForAll().then(() => {
list.addIssue(issue);
expect(list.issues.length).toBe(2);
boardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]);
expect(list.issues[0].id).toBe(2);
expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null);
});
});
});
describe('setListDetail', () => {
it('sets the list detail', () => {
boardsStore.detail.list = 'not a list';
const dummyValue = 'new list';
boardsStore.setListDetail(dummyValue);
expect(boardsStore.detail.list).toEqual(dummyValue);
});
});
describe('clearDetailIssue', () => {
it('resets issue details', () => {
boardsStore.detail.issue = 'something';
boardsStore.clearDetailIssue();
expect(boardsStore.detail.issue).toEqual({});
});
});
describe('setIssueDetail', () => {
it('sets issue details', () => {
boardsStore.detail.issue = 'some details';
const dummyValue = 'new details';
boardsStore.setIssueDetail(dummyValue);
expect(boardsStore.detail.issue).toEqual(dummyValue);
});
});
describe('startMoving', () => {
it('stores list and issue', () => {
const dummyIssue = 'some issue';
const dummyList = 'some list';
boardsStore.startMoving(dummyList, dummyIssue);
expect(boardsStore.moving.issue).toEqual(dummyIssue);
expect(boardsStore.moving.list).toEqual(dummyList);
});
});
describe('setTimeTrackingLimitToHours', () => {
it('sets the timeTracking.LimitToHours option', () => {
boardsStore.timeTracking.limitToHours = false;
boardsStore.setTimeTrackingLimitToHours('true');
expect(boardsStore.timeTracking.limitToHours).toEqual(true);
});
});
describe('setCurrentBoard', () => {
const dummyBoard = 'hoverboard';
it('sets the current board', () => {
const { state } = boardsStore;
state.currentBoard = null;
boardsStore.setCurrentBoard(dummyBoard);
expect(state.currentBoard).toEqual(dummyBoard);
});
});
describe('toggleMultiSelect', () => {
let basicIssueObj;
beforeAll(() => {
basicIssueObj = { id: 987654 };
});
afterEach(() => {
boardsStore.clearMultiSelect();
});
it('adds issue when not present', () => {
boardsStore.toggleMultiSelect(basicIssueObj);
const selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
});
it('removes issue when issue is present', () => {
boardsStore.toggleMultiSelect(basicIssueObj);
let selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
boardsStore.toggleMultiSelect(basicIssueObj);
selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
expect(selectedIds.includes(basicIssueObj.id)).toEqual(false);
});
});
describe('clearMultiSelect', () => {
it('clears all the multi selected issues', () => {
const issue1 = { id: 12345 };
const issue2 = { id: 12346 };
boardsStore.toggleMultiSelect(issue1);
boardsStore.toggleMultiSelect(issue2);
expect(boardsStore.multiSelect.list.length).toEqual(2);
boardsStore.clearMultiSelect();
expect(boardsStore.multiSelect.list.length).toEqual(0);
});
});
describe('moveMultipleIssuesToList', () => {
it('move issues on the new index', () => {
const listOne = boardsStore.addList(listObj);
const listTwo = boardsStore.addList(listObjDuplicate);
expect(boardsStore.state.lists.length).toBe(2);
return axios.waitForAll().then(() => {
expect(listOne.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
boardsStore.moveMultipleIssuesToList({
listFrom: listOne,
listTo: listTwo,
issues: listOne.issues,
newIndex: 0,
});
expect(listTwo.issues.length).toBe(1);
});
});
});
describe('moveMultipleIssuesInList', () => {
it('moves multiple issues in list', () => {
const issueObj = {
title: 'Issue #1',
id: 12345,
iid: 2,
confidential: false,
labels: [],
assignees: [],
};
const issue1 = new ListIssue(issueObj);
const issue2 = new ListIssue({
...issueObj,
title: 'Issue #2',
id: 12346,
});
const list = boardsStore.addList(listObj);
return axios.waitForAll().then(() => {
list.addIssue(issue1);
list.addIssue(issue2);
expect(list.issues.length).toBe(3);
expect(list.issues[0].id).not.toBe(issue2.id);
boardsStore.moveMultipleIssuesInList({
list,
issues: [issue1, issue2],
oldIndicies: [0],
newIndex: 1,
idArray: [1, 12345, 12346],
});
expect(list.issues[0].id).toBe(issue1.id);
expect(boardsStore.moveMultipleIssues).toHaveBeenCalledWith({
ids: [issue1.id, issue2.id],
fromListId: null,
toListId: null,
moveBeforeId: 1,
moveAfterId: null,
});
});
});
});
describe('addListIssue', () => {
let list;
const issue1 = new ListIssue({
title: 'Testing',
id: 2,
iid: 2,
confidential: false,
labels: [
{
color: '#ff0000',
description: 'testing;',
id: 5000,
priority: undefined,
textColor: 'white',
title: 'Test',
},
],
assignees: [],
});
const issue2 = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [
{
id: 1,
title: 'test',
color: 'red',
description: 'testing',
},
],
assignees: [
{
id: 1,
name: 'name',
username: 'username',
avatar_url: 'http://avatar_url',
},
],
real_path: 'path/to/issue',
});
beforeEach(() => {
list = new List(listObj);
list.addIssue(issue1);
setupDefaultResponses();
});
it('adds issues that are not already on the list', () => {
expect(list.findIssue(issue2.id)).toBe(undefined);
expect(list.issues).toEqual([issue1]);
boardsStore.addListIssue(list, issue2);
expect(list.findIssue(issue2.id)).toBe(issue2);
expect(list.issues.length).toBe(2);
expect(list.issues).toEqual([issue1, issue2]);
});
});
describe('updateIssue', () => {
let issue;
let patchSpy;
beforeEach(() => {
issue = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [
{
id: 1,
title: 'test',
color: 'red',
description: 'testing',
},
],
assignees: [
{
id: 1,
name: 'name',
username: 'username',
avatar_url: 'http://avatar_url',
},
],
real_path: 'path/to/issue',
});
patchSpy = jest.fn().mockReturnValue([200, { labels: [] }]);
axiosMock.onPatch(`path/to/issue.json`).reply(({ data }) => patchSpy(JSON.parse(data)));
});
it('passes assignee ids when there are assignees', () => {
boardsStore.updateIssue(issue);
return boardsStore.updateIssue(issue).then(() => {
expect(patchSpy).toHaveBeenCalledWith({
issue: {
milestone_id: null,
assignee_ids: [1],
label_ids: [1],
},
});
});
});
it('passes assignee ids of [0] when there are no assignees', () => {
issue.removeAllAssignees();
return boardsStore.updateIssue(issue).then(() => {
expect(patchSpy).toHaveBeenCalledWith({
issue: {
milestone_id: null,
assignee_ids: [0],
label_ids: [1],
},
});
});
});
});
});
});
...@@ -8,7 +8,7 @@ import getters from 'ee_else_ce/boards/stores/getters'; ...@@ -8,7 +8,7 @@ import getters from 'ee_else_ce/boards/stores/getters';
import BoardColumn from '~/boards/components/board_column.vue'; import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue'; import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import { mockLists, mockListsWithModel } from '../mock_data'; import { mockLists } from '../mock_data';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -42,7 +42,7 @@ describe('BoardContent', () => { ...@@ -42,7 +42,7 @@ describe('BoardContent', () => {
}); });
wrapper = shallowMount(BoardContent, { wrapper = shallowMount(BoardContent, {
propsData: { propsData: {
lists: mockListsWithModel, lists: mockLists,
disabled: false, disabled: false,
...props, ...props,
}, },
...@@ -63,7 +63,7 @@ describe('BoardContent', () => { ...@@ -63,7 +63,7 @@ describe('BoardContent', () => {
}); });
it('renders a BoardColumn component per list', () => { it('renders a BoardColumn component per list', () => {
expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockListsWithModel.length); expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockLists.length);
}); });
it('renders BoardContentSidebar', () => { 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 { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash'; import { keyBy } from 'lodash';
import Vue from 'vue';
import '~/boards/models/list';
import { ListType } from '~/boards/constants'; import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants'; 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'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
...@@ -290,20 +285,6 @@ export const boardsMockInterceptor = (config) => { ...@@ -290,20 +285,6 @@ export const boardsMockInterceptor = (config) => {
return [200, body]; 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 = { export const mockList = {
id: 'gid://gitlab/List/1', id: 'gid://gitlab/List/1',
title: 'Open', title: 'Open',
...@@ -356,10 +337,6 @@ export const mockLists = [mockList, mockLabelList]; ...@@ -356,10 +337,6 @@ export const mockLists = [mockList, mockLabelList];
export const mockListsById = keyBy(mockLists, 'id'); export const mockListsById = keyBy(mockLists, 'id');
export const mockListsWithModel = mockLists.map((listMock) =>
Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
);
export const mockIssuesByListId = { export const mockIssuesByListId = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue3.id, mockIssue4.id], 'gid://gitlab/List/1': [mockIssue.id, mockIssue3.id, mockIssue4.id],
'gid://gitlab/List/2': mockIssues.map(({ id }) => 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