Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 12:08:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 12:08:42 +0300
commitb76ae638462ab0f673e5915986070518dd3f9ad3 (patch)
treebdab0533383b52873be0ec0eb4d3c66598ff8b91 /app/assets/javascripts/boards
parent434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff)
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/boards_util.js3
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue77
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue48
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue109
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_item.vue95
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue57
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue5
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue21
-rw-r--r--app/assets/javascripts/boards/constants.js7
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_members.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql10
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js7
-rw-r--r--app/assets/javascripts/boards/stores/actions.js128
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js6
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js6
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js33
-rw-r--r--app/assets/javascripts/boards/stores/state.js2
25 files changed, 515 insertions, 175 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 46f97e09385..3219d74f85f 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -204,6 +204,9 @@ export const FiltersInfo = {
releaseTag: {
negatedSupport: true,
},
+ types: {
+ negatedSupport: true,
+ },
search: {
negatedSupport: false,
},
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 05b64ddc773..5658a34e9a6 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -65,7 +65,7 @@ export default {
},
computed: {
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
- ...mapGetters(['isEpicBoard']),
+ ...mapGetters(['isEpicBoard', 'isProjectBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
@@ -144,6 +144,9 @@ export default {
totalProgress() {
return Math.round((this.item.descendantWeightSum.closedIssues / this.totalWeight) * 100);
},
+ showReferencePath() {
+ return !this.isProjectBoard && this.itemReferencePath;
+ },
},
methods: {
...mapActions(['performSearch', 'setError']),
@@ -247,7 +250,7 @@ export default {
:class="{ 'gl-font-base': isEpicBoard }"
>
<tooltip-on-truncate
- v-if="itemReferencePath"
+ v-if="showReferencePath"
:title="itemReferencePath"
placement="bottom"
class="board-item-path gl-text-truncate gl-font-weight-bold"
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 69abf886ad7..bcf5b12b209 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -79,7 +79,7 @@ export default {
'is-collapsed': list.collapsed,
'board-type-assignee': list.listType === 'assignee',
}"
- :data-id="list.id"
+ :data-list-id="list.id"
class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable"
data-qa-selector="board_list"
>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 53b071aaed1..4df6ff75249 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -6,10 +6,12 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import defaultSortableConfig from '~/sortable/sortable_config';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { DraggableItemTypes } from '../constants';
import BoardColumn from './board_column.vue';
import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
+ draggableItemTypes: DraggableItemTypes,
components: {
BoardAddNewColumn,
BoardColumn,
@@ -76,19 +78,6 @@ export default {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
},
- handleDragOnEnd(params) {
- const { item, newIndex, oldIndex, to } = params;
-
- const listId = item.dataset.id;
- const replacedListId = to.children[newIndex].dataset.id;
-
- this.moveList({
- listId,
- replacedListId,
- newIndex,
- adjustmentValue: newIndex < oldIndex ? 1 : -1,
- });
- },
},
};
</script>
@@ -104,7 +93,7 @@ export default {
ref="list"
v-bind="draggableOptions"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
- @end="handleDragOnEnd"
+ @end="moveList"
>
<component
:is="boardColumnComponent"
@@ -112,6 +101,7 @@ export default {
:key="index"
ref="board"
:list="list"
+ :data-draggable-item-type="$options.draggableItemTypes.list"
:disabled="disabled"
/>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index e014b82d362..7a936e75676 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -87,6 +87,7 @@ export default {
v-bind="$attrs"
:open="isSidebarOpen"
class="boards-sidebar gl-absolute"
+ variant="sidebar"
@close="handleClose"
>
<template #title>
@@ -159,7 +160,7 @@ export default {
:issuable-type="issuableType"
data-testid="sidebar-due-date"
/>
- <board-sidebar-labels-select class="labels" />
+ <board-sidebar-labels-select class="block labels" />
<sidebar-weight-widget
v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid"
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index cfd6b21fa66..7f242dea644 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -27,7 +27,15 @@ export default {
},
computed: {
urlParams() {
- const { authorUsername, labelName, assigneeUsername, search } = this.filterParams;
+ const {
+ authorUsername,
+ labelName,
+ assigneeUsername,
+ search,
+ milestoneTitle,
+ types,
+ weight,
+ } = this.filterParams;
let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
@@ -36,6 +44,9 @@ export default {
'not[label_name][]': this.filterParams.not.labelName,
'not[author_username]': this.filterParams.not.authorUsername,
'not[assignee_username]': this.filterParams.not.assigneeUsername,
+ 'not[types]': this.filterParams.not.types,
+ 'not[milestone_title]': this.filterParams.not.milestoneTitle,
+ 'not[weight]': this.filterParams.not.weight,
},
undefined,
);
@@ -46,7 +57,10 @@ export default {
author_username: authorUsername,
'label_name[]': labelName,
assignee_username: assigneeUsername,
+ milestone_title: milestoneTitle,
search,
+ types,
+ weight,
};
},
},
@@ -64,7 +78,15 @@ export default {
this.performSearch();
},
getFilteredSearchValue() {
- const { authorUsername, labelName, assigneeUsername, search } = this.filterParams;
+ const {
+ authorUsername,
+ labelName,
+ assigneeUsername,
+ search,
+ milestoneTitle,
+ types,
+ weight,
+ } = this.filterParams;
const filteredSearchValue = [];
if (authorUsername) {
@@ -81,6 +103,13 @@ export default {
});
}
+ if (types) {
+ filteredSearchValue.push({
+ type: 'types',
+ value: { data: types, operator: '=' },
+ });
+ }
+
if (labelName?.length) {
filteredSearchValue.push(
...labelName.map((label) => ({
@@ -90,6 +119,20 @@ export default {
);
}
+ if (milestoneTitle) {
+ filteredSearchValue.push({
+ type: 'milestone_title',
+ value: { data: milestoneTitle, operator: '=' },
+ });
+ }
+
+ if (weight) {
+ filteredSearchValue.push({
+ type: 'weight',
+ value: { data: weight, operator: '=' },
+ });
+ }
+
if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({
type: 'author_username',
@@ -97,6 +140,20 @@ export default {
});
}
+ if (this.filterParams['not[milestoneTitle]']) {
+ filteredSearchValue.push({
+ type: 'milestone_title',
+ value: { data: this.filterParams['not[milestoneTitle]'], operator: '!=' },
+ });
+ }
+
+ if (this.filterParams['not[weight]']) {
+ filteredSearchValue.push({
+ type: 'weight',
+ value: { data: this.filterParams['not[weight]'], operator: '!=' },
+ });
+ }
+
if (this.filterParams['not[assigneeUsername]']) {
filteredSearchValue.push({
type: 'assignee_username',
@@ -113,6 +170,13 @@ export default {
);
}
+ if (this.filterParams['not[types]']) {
+ filteredSearchValue.push({
+ type: 'types',
+ value: { data: this.filterParams['not[types]'], operator: '!=' },
+ });
+ }
+
if (search) {
filteredSearchValue.push(search);
}
@@ -140,9 +204,18 @@ export default {
case 'assignee_username':
filterParams.assigneeUsername = filter.value.data;
break;
+ case 'types':
+ filterParams.types = filter.value.data;
+ break;
case 'label_name':
labels.push(filter.value.data);
break;
+ case 'milestone_title':
+ filterParams.milestoneTitle = filter.value.data;
+ break;
+ case 'weight':
+ filterParams.weight = filter.value.data;
+ break;
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 386ed6bd0a1..a89f71504a9 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -2,7 +2,7 @@
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
-import { TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants';
+import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -18,10 +18,9 @@ const boardDefaults = {
id: false,
name: '',
labels: [],
- milestone_id: undefined,
+ milestone: {},
iteration_id: undefined,
assignee: {},
- assignee_id: undefined,
weight: null,
hide_backlog_list: false,
hide_closed_list: false,
@@ -190,13 +189,10 @@ export default {
issueBoardScopeMutationVariables() {
return {
weight: this.board.weight,
- assigneeId: this.board.assignee?.id
- ? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
+ assigneeId: this.board.assignee?.id || null,
+ milestoneId: this.board.milestone?.id
+ ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
: null,
- milestoneId:
- this.board.milestone?.id || this.board.milestone?.id === 0
- ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
- : null,
iterationId: this.board.iteration_id
? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
: null,
@@ -306,6 +302,19 @@ export default {
}
});
},
+ setAssignee(assigneeId) {
+ this.$set(this.board, 'assignee', {
+ id: assigneeId,
+ });
+ },
+ setMilestone(milestoneId) {
+ this.$set(this.board, 'milestone', {
+ id: milestoneId,
+ });
+ },
+ setWeight(weight) {
+ this.$set(this.board, 'weight', weight);
+ },
},
};
</script>
@@ -373,6 +382,9 @@ export default {
:weights="weights"
@set-iteration="setIteration"
@set-board-labels="setBoardLabels"
+ @set-assignee="setAssignee"
+ @set-milestone="setMilestone"
+ @set-weight="setWeight"
/>
</form>
</gl-modal>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 8dca6be853f..849492effab 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,12 +6,13 @@ import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_opt
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
import Tracking from '~/tracking';
-import { toggleFormEventPrefix } from '../constants';
+import { toggleFormEventPrefix, DraggableItemTypes } from '../constants';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
export default {
+ draggableItemTypes: DraggableItemTypes,
name: 'BoardList',
i18n: {
loading: __('Loading'),
@@ -27,11 +28,6 @@ export default {
GlIntersectionObserver,
},
mixins: [Tracking.mixin()],
- inject: {
- canAdminList: {
- default: false,
- },
- },
props: {
disabled: {
type: Boolean,
@@ -89,8 +85,8 @@ export default {
return !this.isEpicBoard && this.list.listType !== 'closed' && this.showIssueForm;
},
listRef() {
- // When list is draggable, the reference to the list needs to be accessed differently
- return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
+ // When list is draggable, the reference to the list needs to be accessed differently
+ return this.canMoveIssue ? this.$refs.list.$el : this.$refs.list;
},
showingAllItems() {
return this.boardItems.length === this.listItemsCount;
@@ -100,8 +96,11 @@ export default {
? this.$options.i18n.showingAllEpics
: this.$options.i18n.showingAllIssues;
},
+ canMoveIssue() {
+ return !this.disabled;
+ },
treeRootWrapper() {
- return this.canAdminList && !this.listsFlags[this.list.id]?.addItemToListInProgress
+ return this.canMoveIssue && !this.listsFlags[this.list.id]?.addItemToListInProgress
? Draggable
: 'ul';
},
@@ -116,7 +115,7 @@ export default {
value: this.boardItems,
};
- return this.canAdminList ? options : {};
+ return this.canMoveIssue ? options : {};
},
},
watch: {
@@ -172,15 +171,33 @@ export default {
this.loadNextPage();
}
},
- handleDragOnStart() {
+ handleDragOnStart({
+ item: {
+ dataset: { draggableItemType },
+ },
+ }) {
+ if (draggableItemType !== DraggableItemTypes.card) {
+ return;
+ }
+
sortableStart();
this.track('drag_card', { label: 'board' });
},
- handleDragOnEnd(params) {
+ handleDragOnEnd({
+ newIndex: originalNewIndex,
+ oldIndex,
+ from,
+ to,
+ item: {
+ dataset: { draggableItemType, itemId, itemIid, itemPath },
+ },
+ }) {
+ if (draggableItemType !== DraggableItemTypes.card) {
+ return;
+ }
+
sortableEnd();
- const { oldIndex, from, to, item } = params;
- let { newIndex } = params;
- const { itemId, itemIid, itemPath } = item.dataset;
+ let newIndex = originalNewIndex;
let { children } = to;
let moveBeforeId;
let moveAfterId;
@@ -267,6 +284,7 @@ export default {
:index="index"
:list="list"
:item="item"
+ :data-draggable-item-type="$options.draggableItemTypes.card"
:disabled="disabled"
/>
<gl-intersection-observer @appear="onReachingListBottom">
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index caeecb25227..84c9191975e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,21 +1,19 @@
<script>
-import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
-import { __ } from '~/locale';
+
import { toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
+
+import BoardNewItem from './board_new_item.vue';
import ProjectSelect from './project_select.vue';
export default {
name: 'BoardNewIssue',
- i18n: {
- cancel: __('Cancel'),
- },
components: {
+ BoardNewItem,
ProjectSelect,
- GlButton,
},
mixins: [BoardNewIssueMixin],
inject: ['groupId'],
@@ -25,106 +23,55 @@ export default {
required: true,
},
},
- data() {
- return {
- title: '',
- };
- },
computed: {
- ...mapState(['selectedProject']),
- ...mapGetters(['isGroupBoard', 'isEpicBoard']),
- /**
- * We've extended this component in EE where
- * submitButtonTitle returns a different string
- * hence this is kept as a computed prop.
- */
- submitButtonTitle() {
- return __('Create issue');
+ ...mapState(['selectedProject', 'fullPath']),
+ ...mapGetters(['isGroupBoard']),
+ formEventPrefix() {
+ return toggleFormEventPrefix.issue;
},
- disabled() {
- if (this.isGroupBoard) {
- return this.title === '' || !this.selectedProject.name;
- }
- return this.title === '';
+ disableSubmit() {
+ return this.isGroupBoard ? !this.selectedProject.name : false;
},
- inputFieldId() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `${this.list.id}-title`;
+ projectPath() {
+ return this.isGroupBoard ? this.selectedProject.fullPath : this.fullPath;
},
},
- mounted() {
- this.$refs.input.focus();
- eventHub.$on('setSelectedProject', this.setSelectedProject);
- },
methods: {
...mapActions(['addListNewIssue']),
- submit() {
- const { title } = this;
+ submit({ title }) {
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
- eventHub.$emit(`scroll-board-list-${this.list.id}`);
-
return this.addListNewIssue({
+ list: this.list,
issueInput: {
title,
labelIds: labels?.map((l) => l.id),
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
- projectPath: this.selectedProject.fullPath,
- ...this.extraIssueInput(),
+ projectPath: this.projectPath,
},
- list: this.list,
}).then(() => {
- this.reset();
+ this.cancel();
});
},
- reset() {
- this.title = '';
- eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
+ cancel() {
+ eventHub.$emit(`${this.formEventPrefix}${this.list.id}`);
},
},
};
</script>
<template>
- <div class="board-new-issue-form">
- <div class="board-card position-relative p-3 rounded">
- <form ref="submitForm" @submit.prevent="submit">
- <label :for="inputFieldId" class="label-bold">{{ __('Title') }}</label>
- <input
- :id="inputFieldId"
- ref="input"
- v-model="title"
- class="form-control"
- type="text"
- name="issue_title"
- autocomplete="off"
- />
- <project-select v-if="isGroupBoard && !isEpicBoard" :group-id="groupId" :list="list" />
- <div class="clearfix gl-mt-3">
- <gl-button
- ref="submitButton"
- :disabled="disabled"
- class="float-left js-no-auto-disable"
- variant="confirm"
- category="primary"
- type="submit"
- >
- {{ submitButtonTitle }}
- </gl-button>
- <gl-button
- ref="cancelButton"
- class="float-right"
- type="button"
- variant="default"
- @click="reset"
- >
- {{ $options.i18n.cancel }}
- </gl-button>
- </div>
- </form>
- </div>
- </div>
+ <board-new-item
+ :list="list"
+ :form-event-prefix="formEventPrefix"
+ :submit-button-title="__('Create issue')"
+ :disable-submit="disableSubmit"
+ @form-submit="submit"
+ @form-cancel="cancel"
+ >
+ <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
+ </board-new-item>
</template>
diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
index 1218941065f..a25b436b8de 100644
--- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
@@ -11,7 +11,7 @@ import ProjectSelect from './project_select_deprecated.vue';
// This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards
export default {
- name: 'BoardNewIssue',
+ name: 'BoardNewIssueDeprecated',
components: {
ProjectSelect,
GlButton,
diff --git a/app/assets/javascripts/boards/components/board_new_item.vue b/app/assets/javascripts/boards/components/board_new_item.vue
new file mode 100644
index 00000000000..44574de17d7
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_item.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+import eventHub from '../eventhub';
+
+export default {
+ i18n: {
+ cancel: __('Cancel'),
+ },
+ components: {
+ GlForm,
+ GlFormInput,
+ GlButton,
+ },
+ props: {
+ list: {
+ type: Object,
+ required: true,
+ },
+ formEventPrefix: {
+ type: String,
+ required: true,
+ },
+ disableSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ submitButtonTitle: {
+ type: String,
+ required: false,
+ default: __('Create issue'),
+ },
+ },
+ data() {
+ return {
+ title: '',
+ };
+ },
+ computed: {
+ inputFieldId() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `${this.list.id}-title`;
+ },
+ },
+ methods: {
+ handleFormCancel() {
+ this.title = '';
+ this.$emit('form-cancel');
+ },
+ handleFormSubmit() {
+ const { title, list } = this;
+
+ eventHub.$emit(`scroll-board-list-${this.list.id}`);
+ this.$emit('form-submit', {
+ title,
+ list,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="board-new-issue-form">
+ <div class="board-card position-relative gl-p-5 rounded">
+ <gl-form @submit.prevent="handleFormSubmit" @reset="handleFormCancel">
+ <label :for="inputFieldId" class="gl-font-weight-bold">{{ __('Title') }}</label>
+ <gl-form-input
+ :id="inputFieldId"
+ v-model.trim="title"
+ :autofocus="true"
+ autocomplete="off"
+ type="text"
+ name="issue_title"
+ />
+ <slot></slot>
+ <div class="gl-clearfix gl-mt-4">
+ <gl-button
+ :disabled="!title || disableSubmit"
+ class="gl-float-left js-no-auto-disable"
+ variant="confirm"
+ type="submit"
+ >
+ {{ submitButtonTitle }}
+ </gl-button>
+ <gl-button class="gl-float-right js-no-auto-disable" type="reset">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index d8dac17d326..5206db05410 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -1,4 +1,6 @@
<script>
+import { GlFilteredSearchToken } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants';
@@ -6,13 +8,24 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
export default {
+ types: {
+ ISSUE: 'ISSUE',
+ INCIDENT: 'INCIDENT',
+ },
i18n: {
search: __('Search'),
label: __('Label'),
author: __('Author'),
assignee: __('Assignee'),
+ type: __('Type'),
+ incident: __('Incident'),
+ issue: __('Issue'),
+ milestone: __('Milestone'),
+ weight: __('Weight'),
is: __('is'),
isNot: __('is not'),
},
@@ -29,7 +42,19 @@ export default {
},
computed: {
tokens() {
- const { label, is, isNot, author, assignee } = this.$options.i18n;
+ const {
+ label,
+ is,
+ isNot,
+ author,
+ assignee,
+ issue,
+ incident,
+ type,
+ milestone,
+ weight,
+ } = this.$options.i18n;
+ const { types } = this.$options;
const { fetchAuthors, fetchLabels } = issueBoardFilters(
this.$apollo,
this.fullPath,
@@ -77,10 +102,40 @@ export default {
fetchAuthors,
preloadedAuthors: this.preloadedAuthors(),
},
+ {
+ icon: 'issues',
+ title: type,
+ type: 'types',
+ operators: [{ value: '=', description: is }],
+ token: GlFilteredSearchToken,
+ unique: true,
+ options: [
+ { icon: 'issue-type-issue', value: types.ISSUE, title: issue },
+ { icon: 'issue-type-incident', value: types.INCIDENT, title: incident },
+ ],
+ },
+ {
+ type: 'milestone_title',
+ title: milestone,
+ icon: 'clock',
+ symbol: '%',
+ token: MilestoneToken,
+ unique: true,
+ defaultMilestones: [], // todo: https://gitlab.com/gitlab-org/gitlab/-/issues/337044#note_640010094
+ fetchMilestones: this.fetchMilestones,
+ },
+ {
+ type: 'weight',
+ title: weight,
+ icon: 'weight',
+ token: WeightToken,
+ unique: true,
+ },
];
},
},
methods: {
+ ...mapActions(['fetchMilestones']),
preloadedAuthors() {
return gon?.current_user_id
? [
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
index 84802650dad..e7696b8d31b 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -87,7 +87,7 @@ export default {
<div>
<header
v-show="showHeader"
- class="gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-mb-3"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-mb-2"
>
<span class="gl-vertical-align-middle">
<slot name="title">
@@ -97,7 +97,8 @@ export default {
</span>
<gl-button
v-if="canUpdate"
- variant="link"
+ category="tertiary"
+ size="small"
class="gl-text-gray-900! gl-ml-5 js-sidebar-dropdown-toggle edit-link"
data-testid="edit-button"
@click="toggle"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index 29febd0fa51..e74463825c5 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -25,6 +25,8 @@ export default {
data() {
return {
loading: false,
+ oldIid: null,
+ isEditing: false,
};
},
computed: {
@@ -72,6 +74,15 @@ export default {
return this.labelsFetchPath || projectLabelsFetchPath;
},
},
+ watch: {
+ activeBoardItem(_, oldVal) {
+ if (this.isEditing) {
+ this.oldIid = oldVal.iid;
+ } else {
+ this.oldIid = null;
+ }
+ },
+ },
methods: {
...mapActions(['setActiveBoardItemLabels', 'setError']),
async setLabels(payload) {
@@ -84,8 +95,14 @@ export default {
.filter((label) => !payload.find((selected) => selected.id === label.id))
.map((label) => label.id);
- const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue };
+ const input = {
+ addLabelIds,
+ removeLabelIds,
+ projectPath: this.projectPathForActiveIssue,
+ iid: this.oldIid,
+ };
await this.setActiveBoardItemLabels(input);
+ this.oldIid = null;
} catch (e) {
this.setError({ error: e, message: __('An error occurred while updating labels.') });
} finally {
@@ -115,6 +132,8 @@ export default {
:title="__('Labels')"
:loading="loading"
data-testid="sidebar-labels"
+ @open="isEditing = true"
+ @close="isEditing = false"
>
<template #collapsed>
<gl-label
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 21ef70582a4..16fb4596726 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -109,9 +109,16 @@ export const FilterFields = {
'myReactionEmoji',
'releaseTag',
'search',
+ 'types',
+ 'weight',
],
};
+export const DraggableItemTypes = {
+ card: 'card',
+ list: 'list',
+};
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/graphql/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
index eb922f162f8..734867c77e9 100644
--- a/app/assets/javascripts/boards/graphql/board_lists.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
@@ -9,6 +9,7 @@ query ListIssues(
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
board(id: $boardId) {
+ hideBacklogList
lists(issueFilters: $filters) {
nodes {
...BoardListFragment
@@ -18,6 +19,7 @@ query ListIssues(
}
project(fullPath: $fullPath) @include(if: $isProject) {
board(id: $boardId) {
+ hideBacklogList
lists(issueFilters: $filters) {
nodes {
...BoardListFragment
diff --git a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
index 3b8c5389725..d3251c2aa12 100644
--- a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
@@ -3,7 +3,7 @@
query GroupBoardMembers($fullPath: ID!, $search: String) {
workspace: group(fullPath: $fullPath) {
__typename
- assignees: groupMembers(search: $search) {
+ assignees: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
__typename
nodes {
id
diff --git a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
new file mode 100644
index 00000000000..73aa9137dec
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
@@ -0,0 +1,10 @@
+query GroupBoardMilestones($fullPath: ID!, $searchTerm: String) {
+ group(fullPath: $fullPath) {
+ milestones(includeAncestors: true, searchTitle: $searchTerm) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
new file mode 100644
index 00000000000..8dd4d256caa
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
@@ -0,0 +1,10 @@
+query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String) {
+ project(fullPath: $fullPath) {
+ milestones(searchTitle: $searchTerm, includeAncestors: true) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 7f655091cd0..7d6179a8547 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -11,7 +11,12 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
});
export default (params = {}) => {
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 0f1b72146c9..970d00841bd 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,4 +1,5 @@
import * as Sentry from '@sentry/browser';
+import { sortBy } from 'lodash';
import {
BoardType,
ListType,
@@ -13,14 +14,14 @@ import {
issuableTypes,
FilterFields,
ListTypeTitles,
+ DraggableItemTypes,
} from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-// eslint-disable-next-line import/no-deprecated
-import { urlParamsToObject } from '~/lib/utils/url_utility';
+import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
formatBoardLists,
@@ -35,10 +36,13 @@ import {
filterVariables,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
+import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
+import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
+
import * as types from './mutation_types';
export const gqlClient = createGqClient(
@@ -76,8 +80,7 @@ export default {
performSearch({ dispatch }) {
dispatch(
'setFilters',
- // eslint-disable-next-line import/no-deprecated
- convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)),
+ convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })),
);
if (gon.features.graphqlBoardLists) {
@@ -215,34 +218,99 @@ export default {
});
},
+ fetchMilestones({ state, commit }, searchTerm) {
+ commit(types.RECEIVE_MILESTONES_REQUEST);
+
+ const { fullPath, boardType } = state;
+
+ const variables = {
+ fullPath,
+ searchTerm,
+ };
+
+ let query;
+ if (boardType === BoardType.project) {
+ query = projectBoardMilestonesQuery;
+ }
+ if (boardType === BoardType.group) {
+ query = groupBoardMilestonesQuery;
+ }
+
+ if (!query) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Unknown board type');
+ }
+
+ return gqlClient
+ .query({
+ query,
+ variables,
+ })
+ .then(({ data }) => {
+ const errors = data[boardType]?.errors;
+ const milestones = data[boardType]?.milestones.nodes;
+
+ if (errors?.[0]) {
+ throw new Error(errors[0]);
+ }
+
+ commit(types.RECEIVE_MILESTONES_SUCCESS, milestones);
+
+ return milestones;
+ })
+ .catch((e) => {
+ commit(types.RECEIVE_MILESTONES_FAILURE);
+ throw e;
+ });
+ },
+
moveList: (
- { state, commit, dispatch },
- { listId, replacedListId, newIndex, adjustmentValue },
+ { state: { boardLists }, commit, dispatch },
+ {
+ item: {
+ dataset: { listId: movedListId, draggableItemType },
+ },
+ newIndex,
+ to: { children },
+ },
) => {
- if (listId === replacedListId) {
+ if (draggableItemType !== DraggableItemTypes.list) {
return;
}
- const { boardLists } = state;
- const backupList = { ...boardLists };
- const movedList = boardLists[listId];
+ const displacedListId = children[newIndex].dataset.listId;
+ if (movedListId === displacedListId) {
+ return;
+ }
- const newPosition = newIndex - 1;
- const listAtNewIndex = boardLists[replacedListId];
+ const listIds = sortBy(
+ Object.keys(boardLists).filter(
+ (listId) =>
+ listId !== movedListId &&
+ boardLists[listId].listType !== ListType.backlog &&
+ boardLists[listId].listType !== ListType.closed,
+ ),
+ (i) => boardLists[i].position,
+ );
- movedList.position = newPosition;
- listAtNewIndex.position += adjustmentValue;
- commit(types.MOVE_LIST, {
- movedList,
- listAtNewIndex,
- });
+ const targetPosition = boardLists[displacedListId].position;
+ // When the dragged list moves left, displaced list should shift right.
+ const shiftOffset = Number(boardLists[movedListId].position < targetPosition);
+ const displacedListIndex = listIds.findIndex((listId) => listId === displacedListId);
- dispatch('updateList', { listId, position: newPosition, backupList });
+ commit(
+ types.MOVE_LISTS,
+ listIds
+ .slice(0, displacedListIndex + shiftOffset)
+ .concat([movedListId], listIds.slice(displacedListIndex + shiftOffset))
+ .map((listId, index) => ({ listId, position: index })),
+ );
+ dispatch('updateList', { listId: movedListId, position: targetPosition });
},
updateList: (
- { commit, state: { issuableType, boardItemsByListId = {} }, dispatch },
- { listId, position, collapsed, backupList },
+ { state: { issuableType, boardItemsByListId = {} }, dispatch },
+ { listId, position, collapsed },
) => {
gqlClient
.mutate({
@@ -255,8 +323,7 @@ export default {
})
.then(({ data }) => {
if (data?.updateBoardList?.errors.length) {
- commit(types.UPDATE_LIST_FAILURE, backupList);
- return;
+ throw new Error();
}
// Only fetch when board items havent been fetched on a collapsed list
@@ -265,10 +332,19 @@ export default {
}
})
.catch(() => {
- commit(types.UPDATE_LIST_FAILURE, backupList);
+ dispatch('handleUpdateListFailure');
});
},
+ handleUpdateListFailure: ({ dispatch, commit }) => {
+ dispatch('fetchLists');
+
+ commit(
+ types.SET_ERROR,
+ s__('Boards|An error occurred while updating the board list. Please try again.'),
+ );
+ },
+
toggleListCollapsed: ({ commit }, { listId, collapsed }) => {
commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed });
},
@@ -551,7 +627,7 @@ export default {
mutation: issueSetLabelsMutation,
variables: {
input: {
- iid: String(activeBoardItem.iid),
+ iid: input.iid || String(activeBoardItem.iid),
addLabelIds: input.addLabelIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
projectPath: input.projectPath,
@@ -564,7 +640,7 @@ export default {
}
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: activeBoardItem.id,
+ itemId: getIdFromGraphQLId(data.updateIssue?.issue?.id) || activeBoardItem.id,
prop: 'labels',
value: data.updateIssue.issue.labels.nodes,
});
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 49c40c7776a..857b0912c57 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,8 +8,7 @@ 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';
-// eslint-disable-next-line import/no-deprecated
-import { mergeUrlParams, urlParamsToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
+import { mergeUrlParams, queryToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub';
import ListAssignee from '../models/assignee';
@@ -597,8 +596,7 @@ const boardsStore = {
getListIssues(list, emptyIssues = true) {
const data = {
- // eslint-disable-next-line import/no-deprecated
- ...urlParamsToObject(this.filter.path),
+ ...queryToObject(this.filter.path, { gatherArrays: true }),
page: list.page,
};
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 38c54bc8c5d..31b78014525 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -10,8 +10,7 @@ export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
-export const MOVE_LIST = 'MOVE_LIST';
-export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
+export const MOVE_LISTS = 'MOVE_LISTS';
export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED';
export const REMOVE_LIST = 'REMOVE_LIST';
export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
@@ -19,6 +18,9 @@ export const RESET_ITEMS_FOR_LIST = 'RESET_ITEMS_FOR_LIST';
export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
+export const RECEIVE_MILESTONES_REQUEST = 'RECEIVE_MILESTONES_REQUEST';
+export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
+export const RECEIVE_MILESTONES_FAILURE = 'RECEIVE_MILESTONES_FAILURE';
export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM';
export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM';
export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index a32a100fa11..668a3dbaa7e 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,7 +1,7 @@
-import { pull, union } from 'lodash';
+import { cloneDeep, pull, union } from 'lodash';
import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types';
@@ -103,15 +103,12 @@ export default {
Vue.set(state.boardLists, list.id, list);
},
- [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => {
- const { boardLists } = state;
- Vue.set(boardLists, movedList.id, movedList);
- Vue.set(boardLists, listAtNewIndex.id, listAtNewIndex);
- },
-
- [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => {
- state.error = s__('Boards|An error occurred while updating the list. Please try again.');
- Vue.set(state, 'boardLists', backupList);
+ [mutationTypes.MOVE_LISTS]: (state, movedLists) => {
+ const updatedBoardList = movedLists.reduce((acc, { listId, position }) => {
+ acc[listId].position = position;
+ return acc;
+ }, cloneDeep(state.boardLists));
+ Vue.set(state, 'boardLists', updatedBoardList);
},
[mutationTypes.TOGGLE_LIST_COLLAPSED]: (state, { listId, collapsed }) => {
@@ -136,6 +133,20 @@ export default {
Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
},
+ [mutationTypes.RECEIVE_MILESTONES_SUCCESS](state, milestones) {
+ state.milestones = milestones;
+ state.milestonesLoading = false;
+ },
+
+ [mutationTypes.RECEIVE_MILESTONES_REQUEST](state) {
+ state.milestonesLoading = true;
+ },
+
+ [mutationTypes.RECEIVE_MILESTONES_FAILURE](state) {
+ state.milestonesLoading = false;
+ state.error = __('Failed to load milestones.');
+ },
+
[mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listItems, listPageInfo, listId }) => {
const { listData, boardItems } = listItems;
Vue.set(state, 'boardItems', { ...state.boardItems, ...boardItems });
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 7be5ae8b583..264a03ff39d 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -19,6 +19,8 @@ export default () => ({
boardConfig: {},
labelsLoading: false,
labels: [],
+ milestones: [],
+ milestonesLoading: false,
highlightedLists: [],
selectedBoardItems: [],
groupProjects: [],