summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/boards
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /app/assets/javascripts/boards
parent4b1de649d0168371549608993deac953eb692019 (diff)
downloadgitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/boards_util.js77
-rw-r--r--app/assets/javascripts/boards/components/board_assignee_dropdown.vue106
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue17
-rw-r--r--app/assets/javascripts/boards/components/board_column_new.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_configuration_options.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue66
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue228
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue14
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_new.vue109
-rw-r--r--app/assets/javascripts/boards/components/board_list_new.vue145
-rw-r--r--app/assets/javascripts/boards/components/board_promotion_state.js1
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue2
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue83
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue25
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js1
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue15
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue161
-rw-r--r--app/assets/javascripts/boards/constants.js6
-rw-r--r--app/assets/javascripts/boards/ee_functions.js2
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js25
-rw-r--r--app/assets/javascripts/boards/graphql/board.fragment.graphql (renamed from app/assets/javascripts/boards/queries/board.fragment.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/board.mutation.graphql (renamed from app/assets/javascripts/boards/queries/board.mutation.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/board_labels.query.graphql (renamed from app/assets/javascripts/boards/queries/board_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/board_list.fragment.graphql (renamed from app/assets/javascripts/boards/queries/board_list.fragment.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql (renamed from app/assets/javascripts/boards/queries/board_list_create.mutation.graphql)2
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql (renamed from app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql (renamed from app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql (renamed from app/assets/javascripts/boards/queries/board_list_update.mutation.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists.query.graphql (renamed from app/assets/javascripts/boards/queries/board_lists.query.graphql)2
-rw-r--r--app/assets/javascripts/boards/graphql/group_boards.query.graphql (renamed from app/assets/javascripts/boards/queries/group_boards.query.graphql)2
-rw-r--r--app/assets/javascripts/boards/graphql/group_milestones.query.graphql17
-rw-r--r--app/assets/javascripts/boards/graphql/issue.fragment.graphql (renamed from app/assets/javascripts/boards/queries/issue.fragment.graphql)4
-rw-r--r--app/assets/javascripts/boards/graphql/issue_create.mutation.graphql (renamed from app/assets/javascripts/boards/queries/issue_create.mutation.graphql)2
-rw-r--r--app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql (renamed from app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql)2
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql (renamed from app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql (renamed from app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql12
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql (renamed from app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql)0
-rw-r--r--app/assets/javascripts/boards/graphql/lists_issues.query.graphql (renamed from app/assets/javascripts/boards/queries/lists_issues.query.graphql)2
-rw-r--r--app/assets/javascripts/boards/graphql/project_boards.query.graphql (renamed from app/assets/javascripts/boards/queries/project_boards.query.graphql)2
-rw-r--r--app/assets/javascripts/boards/graphql/users_search.query.graphql (renamed from app/assets/javascripts/boards/queries/users_search.query.graphql)0
-rw-r--r--app/assets/javascripts/boards/index.js49
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js5
-rw-r--r--app/assets/javascripts/boards/stores/actions.js126
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js82
-rw-r--r--app/assets/javascripts/boards/stores/getters.js9
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js12
-rw-r--r--app/assets/javascripts/boards/stores/state.js3
53 files changed, 906 insertions, 553 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 6b7b0c2e28d..e5ff41dab74 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,31 +1,39 @@
import { sortBy } from 'lodash';
-import ListIssue from 'ee_else_ce/boards/models/issue';
+import axios from '~/lib/utils/axios_utils';
import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import boardsStore from '~/boards/stores/boards_store';
export function getMilestone() {
return null;
}
+export function updateListPosition(listObj) {
+ const { listType } = listObj;
+ let { position } = listObj;
+ if (listType === ListType.closed) {
+ position = Infinity;
+ } else if (listType === ListType.backlog) {
+ position = -Infinity;
+ }
+
+ return { ...listObj, position };
+}
+
export function formatBoardLists(lists) {
- const formattedLists = lists.nodes.map(list =>
- boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }),
- );
- return formattedLists.reduce((map, list) => {
+ return lists.nodes.reduce((map, list) => {
return {
...map,
- [list.id]: list,
+ [list.id]: updateListPosition(list),
};
}, {});
}
export function formatIssue(issue) {
- return new ListIssue({
+ return {
...issue,
labels: issue.labels?.nodes || [],
assignees: issue.assignees?.nodes || [],
- });
+ };
}
export function formatListIssues(listIssues) {
@@ -44,12 +52,12 @@ export function formatListIssues(listIssues) {
[list.id]: sortedIssues.map(i => {
const id = getIdFromGraphQLId(i.id);
- const listIssue = new ListIssue({
+ const listIssue = {
...i,
id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
- });
+ };
issues[id] = listIssue;
@@ -83,21 +91,48 @@ export function fullLabelId(label) {
}
export function moveIssueListHelper(issue, fromList, toList) {
- if (toList.type === ListType.label) {
- issue.addLabel(toList.label);
+ const updatedIssue = issue;
+ if (
+ toList.listType === ListType.label &&
+ !updatedIssue.labels.find(label => label.id === toList.label.id)
+ ) {
+ updatedIssue.labels.push(toList.label);
}
- if (fromList && fromList.type === ListType.label) {
- issue.removeLabel(fromList.label);
+ if (fromList?.label && fromList.listType === ListType.label) {
+ updatedIssue.labels = updatedIssue.labels.filter(label => fromList.label.id !== label.id);
}
- if (toList.type === ListType.assignee) {
- issue.addAssignee(toList.assignee);
+ if (
+ toList.listType === ListType.assignee &&
+ !updatedIssue.assignees.find(assignee => assignee.id === toList.assignee.id)
+ ) {
+ updatedIssue.assignees.push(toList.assignee);
+ }
+ if (fromList?.assignee && fromList.listType === ListType.assignee) {
+ updatedIssue.assignees = updatedIssue.assignees.filter(
+ assignee => assignee.id !== fromList.assignee.id,
+ );
}
- if (fromList && fromList.type === ListType.assignee) {
- issue.removeAssignee(fromList.assignee);
+
+ return updatedIssue;
+}
+
+export function getBoardsPath(endpoint, board) {
+ const path = `${endpoint}${board.id ? `/${board.id}` : ''}.json`;
+
+ if (board.id) {
+ return axios.put(path, { board });
}
+ return axios.post(path, { board });
+}
+
+export function isListDraggable(list) {
+ return list.listType !== ListType.backlog && list.listType !== ListType.closed;
+}
- return issue;
+// EE-specific feature. Find the implementation in the `ee/`-folder
+export function transformBoardConfig() {
+ return '';
}
export default {
@@ -106,4 +141,6 @@ export default {
formatListIssues,
fullBoardId,
fullLabelId,
+ getBoardsPath,
+ isListDraggable,
};
diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
index c81f171af2b..1469efae5a6 100644
--- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
+++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
@@ -1,18 +1,20 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import { cloneDeep } from 'lodash';
import {
GlDropdownItem,
GlDropdownDivider,
GlAvatarLabeled,
GlAvatarLink,
GlSearchBoxByType,
+ GlLoadingIcon,
} from '@gitlab/ui';
import { __, n__ } from '~/locale';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
-import searchUsers from '~/boards/queries/users_search.query.graphql';
+import searchUsers from '~/boards/graphql/users_search.query.graphql';
export default {
noSearchDelay: 0,
@@ -32,12 +34,13 @@ export default {
GlAvatarLabeled,
GlAvatarLink,
GlSearchBoxByType,
+ GlLoadingIcon,
},
data() {
return {
search: '',
participants: [],
- selected: this.$store.getters.activeIssue.assignees,
+ selected: [],
};
},
apollo: {
@@ -72,6 +75,7 @@ export default {
},
computed: {
...mapGetters(['activeIssue']),
+ ...mapState(['isSettingAssignees']),
assigneeText() {
return n__('Assignee', '%d Assignees', this.selected.length);
},
@@ -89,9 +93,20 @@ export default {
isSearchEmpty() {
return this.search === '';
},
+ currentUser() {
+ return gon?.current_username;
+ },
+ },
+ created() {
+ this.selected = cloneDeep(this.activeIssue.assignees);
},
methods: {
...mapActions(['setAssignees']),
+ async assignSelf() {
+ const [currentUserObject] = await this.setAssignees(this.currentUser);
+
+ this.selectAssignee(currentUserObject);
+ },
clearSelected() {
this.selected = [];
},
@@ -117,9 +132,9 @@ export default {
</script>
<template>
- <board-editable-item :title="assigneeText" @close="saveAssignees">
+ <board-editable-item :loading="isSettingAssignees" :title="assigneeText" @close="saveAssignees">
<template #collapsed>
- <issuable-assignees :users="activeIssue.assignees" />
+ <issuable-assignees :users="activeIssue.assignees" @assign-self="assignSelf" />
</template>
<template #default>
@@ -132,45 +147,48 @@ export default {
<gl-search-box-by-type v-model.trim="search" />
</template>
<template #items>
- <gl-dropdown-item
- :is-checked="selectedIsEmpty"
- data-testid="unassign"
- class="mt-2"
- @click="selectAssignee()"
- >{{ $options.i18n.unassigned }}</gl-dropdown-item
- >
- <gl-dropdown-divider data-testid="unassign-divider" />
- <gl-dropdown-item
- v-for="item in selected"
- :key="item.id"
- :is-checked="isChecked(item.username)"
- @click="unselect(item.username)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="item.name"
- :sub-label="item.username"
- :src="item.avatarUrl || item.avatar"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
- <gl-dropdown-item
- v-for="unselectedUser in unSelectedFiltered"
- :key="unselectedUser.id"
- :data-testid="`item_${unselectedUser.name}`"
- @click="selectAssignee(unselectedUser)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="unselectedUser.name"
- :sub-label="unselectedUser.username"
- :src="unselectedUser.avatarUrl || unselectedUser.avatar"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
+ <gl-loading-icon v-if="$apollo.queries.participants.loading" size="lg" />
+ <template v-else>
+ <gl-dropdown-item
+ :is-checked="selectedIsEmpty"
+ data-testid="unassign"
+ class="mt-2"
+ @click="selectAssignee()"
+ >{{ $options.i18n.unassigned }}</gl-dropdown-item
+ >
+ <gl-dropdown-divider data-testid="unassign-divider" />
+ <gl-dropdown-item
+ v-for="item in selected"
+ :key="item.id"
+ :is-checked="isChecked(item.username)"
+ @click="unselect(item.username)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="item.name"
+ :sub-label="item.username"
+ :src="item.avatarUrl || item.avatar"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
+ <gl-dropdown-item
+ v-for="unselectedUser in unSelectedFiltered"
+ :key="unselectedUser.id"
+ :data-testid="`item_${unselectedUser.name}`"
+ @click="selectAssignee(unselectedUser)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="unselectedUser.name"
+ :sub-label="unselectedUser.username"
+ :src="unselectedUser.avatarUrl || unselectedUser.avatar"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ </template>
</template>
</multi-select-dropdown>
</template>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index cb93340bcf8..753e6941c43 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -2,15 +2,12 @@
// This component is being replaced in favor of './board_column_new.vue' for GraphQL boards
import Sortable from 'sortablejs';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
-import EmptyComponent from '~/vue_shared/components/empty_component';
import BoardList from './board_list.vue';
import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
-import { ListType } from '../constants';
export default {
components: {
- BoardPromotionState: EmptyComponent,
BoardListHeader,
BoardList,
},
@@ -42,9 +39,6 @@ export default {
};
},
computed: {
- showBoardListAndBoardInfo() {
- return this.list.type !== ListType.promotion;
- },
listIssues() {
return this.list.issues;
},
@@ -105,16 +99,7 @@ export default {
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
>
<board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
- <board-list
- v-if="showBoardListAndBoardInfo"
- ref="board-list"
- :disabled="disabled"
- :issues="listIssues"
- :list="list"
- />
-
- <!-- Will be only available in EE -->
- <board-promotion-state v-if="list.id === 'promotion'" />
+ <board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_column_new.vue b/app/assets/javascripts/boards/components/board_column_new.vue
index 8a59355eb83..7839f45c48b 100644
--- a/app/assets/javascripts/boards/components/board_column_new.vue
+++ b/app/assets/javascripts/boards/components/board_column_new.vue
@@ -1,13 +1,11 @@
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_new.vue';
-import BoardPromotionState from 'ee_else_ce/boards/components/board_promotion_state';
import BoardList from './board_list_new.vue';
-import { ListType } from '../constants';
+import { isListDraggable } from '../boards_util';
export default {
components: {
- BoardPromotionState,
BoardListHeader,
BoardList,
},
@@ -35,22 +33,17 @@ export default {
computed: {
...mapState(['filterParams']),
...mapGetters(['getIssuesByList']),
- showBoardListAndBoardInfo() {
- return this.list.type !== ListType.promotion;
- },
listIssues() {
return this.getIssuesByList(this.list.id);
},
- shouldFetchIssues() {
- return this.list.type !== ListType.blank;
+ isListDraggable() {
+ return isListDraggable(this.list);
},
},
watch: {
filterParams: {
handler() {
- if (this.shouldFetchIssues) {
- this.fetchIssuesForList({ listId: this.list.id });
- }
+ this.fetchIssuesForList({ listId: this.list.id });
},
deep: true,
immediate: true,
@@ -58,7 +51,6 @@ export default {
},
methods: {
...mapActions(['fetchIssuesForList']),
- // TODO: Reordering of lists https://gitlab.com/gitlab-org/gitlab/-/issues/280515
},
};
</script>
@@ -66,13 +58,12 @@ export default {
<template>
<div
:class="{
- 'is-draggable': !list.preset,
- 'is-expandable': list.isExpandable,
- 'is-collapsed': !list.isExpanded,
- 'board-type-assignee': list.type === 'assignee',
+ 'is-draggable': isListDraggable,
+ 'is-collapsed': list.collapsed,
+ 'board-type-assignee': list.listType === 'assignee',
}"
:data-id="list.id"
- class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
+ 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"
>
<div
@@ -80,15 +71,12 @@ export default {
>
<board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list
- v-if="showBoardListAndBoardInfo"
ref="board-list"
:disabled="disabled"
:issues="listIssues"
:list="list"
+ :can-admin-list="canAdminList"
/>
-
- <!-- Will be only available in EE -->
- <board-promotion-state v-if="list.id === 'promotion'" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue
index 754b00b54e0..99d1e4a2611 100644
--- a/app/assets/javascripts/boards/components/board_configuration_options.vue
+++ b/app/assets/javascripts/boards/components/board_configuration_options.vue
@@ -42,7 +42,7 @@ export default {
</script>
<template>
- <div class="append-bottom-20">
+ <div class="gl-mb-5">
<label class="label-bold gl-font-lg" for="board-new-name">
{{ __('List options') }}
</label>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 92976574efb..b366aa6fdb3 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,10 +1,13 @@
<script>
+import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
import { sortBy } from 'lodash';
import { GlAlert } from '@gitlab/ui';
-import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
+import BoardColumn from './board_column.vue';
import BoardColumnNew from './board_column_new.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import defaultSortableConfig from '~/sortable/sortable_config';
+import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
export default {
components: {
@@ -32,18 +35,51 @@ export default {
...mapState(['boardLists', 'error']),
...mapGetters(['isSwimlanesOn']),
boardListsToUse() {
- const lists =
- this.glFeatures.graphqlBoardLists || this.isSwimlanesOn ? this.boardLists : this.lists;
- return sortBy([...Object.values(lists)], 'position');
+ return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn
+ ? sortBy([...Object.values(this.boardLists)], 'position')
+ : this.lists;
+ },
+ canDragColumns() {
+ return this.glFeatures.graphqlBoardLists && this.canAdminList;
+ },
+ boardColumnWrapper() {
+ return this.canDragColumns ? Draggable : 'div';
+ },
+ draggableOptions() {
+ const options = {
+ ...defaultSortableConfig,
+ disabled: this.disabled,
+ draggable: '.is-draggable',
+ fallbackOnBody: false,
+ group: 'boards-list',
+ tag: 'div',
+ value: this.lists,
+ };
+
+ return this.canDragColumns ? options : {};
},
- },
- mounted() {
- if (this.glFeatures.graphqlBoardLists) {
- this.showPromotionList();
- }
},
methods: {
- ...mapActions(['showPromotionList']),
+ ...mapActions(['moveList']),
+ handleDragOnStart() {
+ sortableStart();
+ },
+
+ handleDragOnEnd(params) {
+ sortableEnd();
+
+ 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>
@@ -53,10 +89,14 @@ export default {
<gl-alert v-if="error" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
- <div
+ <component
+ :is="boardColumnWrapper"
v-if="!isSwimlanesOn"
+ ref="list"
+ v-bind="draggableOptions"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
- data-qa-selector="boards_list"
+ @start="handleDragOnStart"
+ @end="handleDragOnEnd"
>
<board-column
v-for="list in boardListsToUse"
@@ -66,7 +106,7 @@ export default {
:list="list"
:disabled="disabled"
/>
- </div>
+ </component>
<template v-else>
<epics-swimlanes
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index e4ef3600ff9..dab934352ca 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,11 +1,14 @@
<script>
-import { __ } from '~/locale';
+import { GlModal } from '@gitlab/ui';
+import { pick } from 'lodash';
+import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
+import { fullBoardId, getBoardsPath } from '../boards_util';
import BoardConfigurationOptions from './board_configuration_options.vue';
+import createBoardMutation from '../graphql/board.mutation.graphql';
const boardDefaults = {
id: false,
@@ -19,10 +22,28 @@ const boardDefaults = {
hide_closed_list: false,
};
+const formType = {
+ new: 'new',
+ delete: 'delete',
+ edit: 'edit',
+};
+
export default {
+ i18n: {
+ [formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') },
+ [formType.delete]: { title: s__('Board|Delete board'), btnText: __('Delete') },
+ [formType.edit]: { title: s__('Board|Edit board'), btnText: __('Save changes') },
+ scopeModalTitle: s__('Board|Board scope'),
+ cancelButtonText: __('Cancel'),
+ deleteErrorMessage: s__('Board|Failed to delete board. Please try again.'),
+ saveErrorMessage: __('Unable to save your changes. Please try again.'),
+ deleteConfirmationMessage: s__('Board|Are you sure you want to delete this board?'),
+ titleFieldLabel: __('Title'),
+ titleFieldPlaceholder: s__('Board|Enter board name'),
+ },
components: {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
- DeprecatedModal,
+ GlModal,
BoardConfigurationOptions,
},
props: {
@@ -63,36 +84,35 @@ export default {
required: false,
default: false,
},
+ currentBoard: {
+ type: Object,
+ required: true,
+ },
+ },
+ inject: {
+ endpoints: {
+ default: {},
+ },
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
- currentBoard: boardsStore.state.currentBoard,
currentPage: boardsStore.state.currentPage,
isLoading: false,
};
},
computed: {
isNewForm() {
- return this.currentPage === 'new';
+ return this.currentPage === formType.new;
},
isDeleteForm() {
- return this.currentPage === 'delete';
+ return this.currentPage === formType.delete;
},
isEditForm() {
- return this.currentPage === 'edit';
- },
- isVisible() {
- return this.currentPage !== '';
+ return this.currentPage === formType.edit;
},
buttonText() {
- if (this.isNewForm) {
- return __('Create board');
- }
- if (this.isDeleteForm) {
- return __('Delete');
- }
- return __('Save changes');
+ return this.$options.i18n[this.currentPage].btnText;
},
buttonKind() {
if (this.isNewForm) {
@@ -104,16 +124,11 @@ export default {
return 'info';
},
title() {
- if (this.isNewForm) {
- return __('Create new board');
- }
- if (this.isDeleteForm) {
- return __('Delete board');
- }
if (this.readonly) {
- return __('Board scope');
+ return this.$options.i18n.scopeModalTitle;
}
- return __('Edit board');
+
+ return this.$options.i18n[this.currentPage].title;
},
readonly() {
return !this.canAdminBoard;
@@ -121,6 +136,33 @@ export default {
submitDisabled() {
return this.isLoading || this.board.name.length === 0;
},
+ primaryProps() {
+ return {
+ text: this.buttonText,
+ attributes: [
+ {
+ variant: this.buttonKind,
+ disabled: this.submitDisabled,
+ loading: this.isLoading,
+ 'data-qa-selector': 'save_changes_button',
+ },
+ ],
+ };
+ },
+ cancelProps() {
+ return {
+ text: this.$options.i18n.cancelButtonText,
+ };
+ },
+ boardPayload() {
+ const { assignee, milestone, labels } = this.board;
+ return {
+ ...this.board,
+ assignee_id: assignee?.id,
+ milestone_id: milestone?.id,
+ label_ids: labels.length ? labels.map(b => b.id) : [''],
+ };
+ },
},
mounted() {
this.resetFormState();
@@ -129,6 +171,31 @@ export default {
}
},
methods: {
+ callBoardMutation(id) {
+ return this.$apollo.mutate({
+ mutation: createBoardMutation,
+ variables: {
+ ...pick(this.boardPayload, ['hideClosedList', 'hideBacklogList']),
+ id,
+ },
+ });
+ },
+ async updateBoard() {
+ const responses = await Promise.all([
+ // Remove unnecessary REST API call when https://gitlab.com/gitlab-org/gitlab/-/issues/282299#note_462996301 is resolved
+ getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload),
+ this.callBoardMutation(fullBoardId(this.boardPayload.id)),
+ ]);
+
+ return responses[0].data;
+ },
+ async createBoard() {
+ // TODO: change this to use `createBoard` mutation https://gitlab.com/gitlab-org/gitlab/-/issues/292466 is resolved
+ const boardData = await getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload);
+ this.callBoardMutation(fullBoardId(boardData.data.id));
+
+ return boardData.data || boardData;
+ },
submit() {
if (this.board.name.length === 0) return;
this.isLoading = true;
@@ -136,31 +203,21 @@ export default {
boardsStore
.deleteBoard(this.currentBoard)
.then(() => {
+ this.isLoading = false;
visitUrl(boardsStore.rootPath);
})
.catch(() => {
- Flash(__('Failed to delete board. Please try again.'));
+ Flash(this.$options.i18n.deleteErrorMessage);
this.isLoading = false;
});
} else {
- boardsStore
- .createBoard(this.board)
- .then(resp => {
- // This handles 2 use cases
- // - In create call we only get one parameter, the new board
- // - In update call, due to Promise.all, we get REST response in
- // array index 0
-
- if (Array.isArray(resp)) {
- return resp[0].data;
- }
- return resp.data ? resp.data : resp;
- })
+ const boardAction = this.boardPayload.id ? this.updateBoard : this.createBoard;
+ boardAction()
.then(data => {
visitUrl(data.board_path);
})
.catch(() => {
- Flash(__('Unable to save your changes. Please try again.'));
+ Flash(this.$options.i18n.saveErrorMessage);
this.isLoading = false;
});
}
@@ -181,53 +238,58 @@ export default {
</script>
<template>
- <deprecated-modal
- v-show="isVisible"
+ <gl-modal
+ modal-id="board-config-modal"
+ modal-class="board-config-modal"
+ content-class="gl-absolute gl-top-7"
+ visible
:hide-footer="readonly"
:title="title"
- :primary-button-label="buttonText"
- :kind="buttonKind"
- :submit-disabled="submitDisabled"
- modal-dialog-class="board-config-modal"
+ :action-primary="primaryProps"
+ :action-cancel="cancelProps"
+ @primary="submit"
@cancel="cancel"
- @submit="submit"
+ @close="cancel"
+ @hide.prevent
>
- <template #body>
- <p v-if="isDeleteForm">{{ __('Are you sure you want to delete this board?') }}</p>
- <form v-else class="js-board-config-modal" @submit.prevent>
- <div v-if="!readonly" class="append-bottom-20">
- <label class="label-bold gl-font-lg" for="board-new-name">{{ __('Title') }}</label>
- <input
- id="board-new-name"
- ref="name"
- v-model="board.name"
- class="form-control"
- data-qa-selector="board_name_field"
- type="text"
- :placeholder="__('Enter board name')"
- @keyup.enter="submit"
- />
- </div>
-
- <board-configuration-options
- :is-new-form="isNewForm"
- :board="board"
- :current-board="currentBoard"
+ <p v-if="isDeleteForm" data-testid="delete-confirmation-message">
+ {{ $options.i18n.deleteConfirmationMessage }}
+ </p>
+ <form v-else class="js-board-config-modal" data-testid="board-form-wrapper" @submit.prevent>
+ <div v-if="!readonly" class="gl-mb-5" data-testid="board-form">
+ <label class="gl-font-weight-bold gl-font-lg" for="board-new-name">
+ {{ $options.i18n.titleFieldLabel }}
+ </label>
+ <input
+ id="board-new-name"
+ ref="name"
+ v-model="board.name"
+ class="form-control"
+ data-qa-selector="board_name_field"
+ type="text"
+ :placeholder="$options.i18n.titleFieldPlaceholder"
+ @keyup.enter="submit"
/>
+ </div>
- <board-scope
- v-if="scopedIssueBoardFeatureEnabled"
- :collapse-scope="isNewForm"
- :board="board"
- :can-admin-board="canAdminBoard"
- :labels-path="labelsPath"
- :labels-web-url="labelsWebUrl"
- :enable-scoped-labels="enableScopedLabels"
- :project-id="projectId"
- :group-id="groupId"
- :weights="weights"
- />
- </form>
- </template>
- </deprecated-modal>
+ <board-configuration-options
+ :is-new-form="isNewForm"
+ :board="board"
+ :current-board="currentBoard"
+ />
+
+ <board-scope
+ v-if="scopedIssueBoardFeatureEnabled"
+ :collapse-scope="isNewForm"
+ :board="board"
+ :can-admin-board="canAdminBoard"
+ :labels-path="labelsPath"
+ :labels-web-url="labelsWebUrl"
+ :enable-scoped-labels="enableScopedLabels"
+ :project-id="projectId"
+ :group-id="groupId"
+ :weights="weights"
+ />
+ </form>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 53989e2d9de..1f87b563e73 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,7 +6,6 @@ import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getBoardSortableDefaultOptions,
@@ -25,7 +24,6 @@ export default {
boardNewIssue,
GlLoadingIcon,
},
- mixins: [glFeatureFlagMixin()],
props: {
disabled: {
type: Boolean,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index d85ba2038a7..3db5c2e0830 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -72,12 +72,7 @@ export default {
return this.list?.label?.description || this.list.title || '';
},
showListHeaderButton() {
- return (
- !this.disabled &&
- this.listType !== ListType.closed &&
- this.listType !== ListType.blank &&
- this.listType !== ListType.promotion
- );
+ return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
return (
@@ -109,9 +104,6 @@ export default {
this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded
);
},
- showBoardListAndBoardInfo() {
- return this.listType !== ListType.blank && this.listType !== ListType.promotion;
- },
uniqueKey() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.listType}.${this.list.id}`;
@@ -190,7 +182,8 @@ export default {
:title="chevronTooltip"
:icon="chevronIcon"
class="board-title-caret no-drag gl-cursor-pointer"
- variant="link"
+ category="tertiary"
+ size="small"
@click="toggleExpanded"
/>
<!-- The following is only true in EE and if it is a milestone -->
@@ -288,7 +281,6 @@ export default {
</gl-tooltip>
<div
- v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary"
:class="{
'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
diff --git a/app/assets/javascripts/boards/components/board_list_header_new.vue b/app/assets/javascripts/boards/components/board_list_header_new.vue
index 99347a4cd4d..44eb2aa34c2 100644
--- a/app/assets/javascripts/boards/components/board_list_header_new.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_new.vue
@@ -9,15 +9,22 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
-import { n__, s__ } from '~/locale';
+import { n__, s__, __ } from '~/locale';
import AccessorUtilities from '../../lib/utils/accessor';
import IssueCount from './issue_count.vue';
import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import { inactiveId, LIST, ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import { isListDraggable } from '~/boards/boards_util';
export default {
+ i18n: {
+ newIssue: __('New issue'),
+ listSettings: __('List settings'),
+ expand: s__('Boards|Expand'),
+ collapse: s__('Boards|Collapse'),
+ },
components: {
GlButtonGroup,
GlButton,
@@ -66,57 +73,49 @@ export default {
return Boolean(this.currentUserId);
},
listType() {
- return this.list.type;
+ return this.list.listType;
},
listAssignee() {
return this.list?.assignee?.username || '';
},
listTitle() {
- return this.list?.label?.description || this.list.title || '';
+ return this.list?.label?.description || this.list?.assignee?.name || this.list.title || '';
},
showListHeaderButton() {
- return (
- !this.disabled &&
- this.listType !== ListType.closed &&
- this.listType !== ListType.blank &&
- this.listType !== ListType.promotion
- );
+ return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
return (
- this.list.type === ListType.milestone &&
+ this.listType === ListType.milestone &&
this.list.milestone &&
- (this.list.isExpanded || !this.isSwimlanesHeader)
+ (!this.list.collapsed || !this.isSwimlanesHeader)
);
},
showAssigneeListDetails() {
return (
- this.list.type === ListType.assignee && (this.list.isExpanded || !this.isSwimlanesHeader)
+ this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader)
);
},
issuesCount() {
- return this.list.issuesSize;
+ return this.list.issuesCount;
},
issuesTooltipLabel() {
return n__(`%d issue`, `%d issues`, this.issuesCount);
},
chevronTooltip() {
- return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
+ return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse;
},
chevronIcon() {
- return this.list.isExpanded ? 'chevron-right' : 'chevron-down';
+ return this.list.collapsed ? 'chevron-down' : 'chevron-right';
},
isNewIssueShown() {
return this.listType === ListType.backlog || this.showListHeaderButton;
},
isSettingsShown() {
return (
- this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded
+ this.listType !== ListType.backlog && this.showListHeaderButton && !this.list.collapsed
);
},
- showBoardListAndBoardInfo() {
- return this.listType !== ListType.blank && this.listType !== ListType.promotion;
- },
uniqueKey() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.listType}.${this.list.id}`;
@@ -127,6 +126,9 @@ export default {
headerStyle() {
return { borderTopColor: this.list?.label?.color };
},
+ userCanDrag() {
+ return !this.disabled && isListDraggable(this.list);
+ },
},
methods: {
...mapActions(['updateList', 'setActiveId']),
@@ -145,7 +147,7 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
- this.list.isExpanded = !this.list.isExpanded;
+ this.list.collapsed = !this.list.collapsed;
if (!this.isLoggedIn) {
this.addToLocalStorage();
@@ -159,11 +161,11 @@ export default {
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
+ localStorage.setItem(`${this.uniqueKey}.expanded`, !this.list.collapsed);
}
},
updateListFunction() {
- this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded });
+ this.updateList({ listId: this.list.id, collapsed: this.list.collapsed });
},
},
};
@@ -173,7 +175,7 @@ export default {
<header
:class="{
'has-border': list.label && list.label.color,
- 'gl-h-full': !list.isExpanded,
+ 'gl-h-full': list.collapsed,
'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader,
}"
:style="headerStyle"
@@ -183,22 +185,22 @@ export default {
>
<h3
:class="{
- 'user-can-drag': !disabled && !list.preset,
- 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
- 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
- 'gl-py-2': !list.isExpanded && isSwimlanesHeader,
- 'gl-flex-direction-column': !list.isExpanded,
+ 'user-can-drag': userCanDrag,
+ 'gl-py-3 gl-h-full': list.collapsed && !isSwimlanesHeader,
+ 'gl-border-b-0': list.collapsed || isSwimlanesHeader,
+ 'gl-py-2': list.collapsed && isSwimlanesHeader,
+ 'gl-flex-direction-column': list.collapsed,
}"
class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle"
>
<gl-button
- v-if="list.isExpandable"
v-gl-tooltip.hover
:aria-label="chevronTooltip"
:title="chevronTooltip"
:icon="chevronIcon"
class="board-title-caret no-drag gl-cursor-pointer"
- variant="link"
+ category="tertiary"
+ size="small"
@click="toggleExpanded"
/>
<!-- EE start -->
@@ -207,8 +209,8 @@ export default {
aria-hidden="true"
class="milestone-icon"
:class="{
- 'gl-mt-3 gl-rotate-90': !list.isExpanded,
- 'gl-mr-2': list.isExpanded,
+ 'gl-mt-3 gl-rotate-90': list.collapsed,
+ 'gl-mr-2': !list.collapsed,
}"
>
<gl-icon name="timer" />
@@ -216,17 +218,17 @@ export default {
<a
v-if="showAssigneeListDetails"
- :href="list.assignee.path"
+ :href="list.assignee.webUrl"
class="user-avatar-link js-no-trigger"
:class="{
- 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ 'gl-mt-3 gl-rotate-90': list.collapsed,
}"
>
<img
v-gl-tooltip.hover.bottom
:title="listAssignee"
:alt="list.assignee.name"
- :src="list.assignee.avatar"
+ :src="list.assignee.avatarUrl"
class="avatar s20"
height="20"
width="20"
@@ -236,9 +238,9 @@ export default {
<div
class="board-title-text"
:class="{
- 'gl-display-none': !list.isExpanded && isSwimlanesHeader,
- 'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded,
- 'gl-flex-grow-1': list.isExpanded,
+ 'gl-display-none': list.collapsed && isSwimlanesHeader,
+ 'gl-flex-grow-0 gl-my-3 gl-mx-0': list.collapsed,
+ 'gl-flex-grow-1': !list.collapsed,
}"
>
<!-- EE start -->
@@ -246,16 +248,16 @@ export default {
v-if="listType !== 'label'"
v-gl-tooltip.hover
:class="{
- 'gl-display-block': !list.isExpanded || listType === 'milestone',
+ 'gl-display-block': list.collapsed || listType === 'milestone',
}"
:title="listTitle"
class="board-title-main-text gl-text-truncate"
>
- {{ list.title }}
+ {{ listTitle }}
</span>
<span
v-if="listType === 'assignee'"
- v-show="list.isExpanded"
+ v-show="!list.collapsed"
class="gl-ml-2 gl-font-weight-normal gl-text-gray-500"
>
@{{ listAssignee }}
@@ -267,21 +269,21 @@ export default {
:background-color="list.label.color"
:description="list.label.description"
:scoped="showScopedLabels(list.label)"
- :size="!list.isExpanded ? 'sm' : ''"
+ :size="list.collapsed ? 'sm' : ''"
:title="list.label.title"
/>
</div>
<!-- EE start -->
<span
- v-if="isSwimlanesHeader && !list.isExpanded"
+ v-if="isSwimlanesHeader && list.collapsed"
ref="collapsedInfo"
aria-hidden="true"
- class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-500"
+ class="board-header-collapsed-info-icon gl-cursor-pointer gl-text-gray-500"
>
<gl-icon name="information" />
</span>
- <gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo">
+ <gl-tooltip v-if="isSwimlanesHeader && list.collapsed" :target="() => $refs.collapsedInfo">
<div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div>
<div v-if="list.maxIssueCount !== 0">
•
@@ -301,11 +303,10 @@ export default {
<!-- EE end -->
<div
- v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
:class="{
- 'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
- 'gl-p-0': !list.isExpanded,
+ 'gl-display-none!': list.collapsed && isSwimlanesHeader,
+ 'gl-p-0': list.collapsed,
}"
>
<span class="gl-display-inline-flex">
@@ -331,11 +332,11 @@ export default {
>
<gl-button
v-if="isNewIssueShown"
- v-show="list.isExpanded"
+ v-show="!list.collapsed"
ref="newIssueBtn"
v-gl-tooltip.hover
- :aria-label="__('New issue')"
- :title="__('New issue')"
+ :aria-label="$options.i18n.newIssue"
+ :title="$options.i18n.newIssue"
class="issue-count-badge-add-button no-drag"
icon="plus"
@click="showNewIssueForm"
@@ -345,13 +346,13 @@ export default {
v-if="isSettingsShown"
ref="settingsBtn"
v-gl-tooltip.hover
- :aria-label="__('List settings')"
+ :aria-label="$options.i18n.listSettings"
class="no-drag js-board-settings-button"
- :title="__('List settings')"
+ :title="$options.i18n.listSettings"
icon="settings"
@click="openSidebarSettings"
/>
- <gl-tooltip :target="() => $refs.settingsBtn">{{ __('List settings') }}</gl-tooltip>
+ <gl-tooltip :target="() => $refs.settingsBtn">{{ $options.i18n.listSettings }}</gl-tooltip>
</gl-button-group>
</h3>
</header>
diff --git a/app/assets/javascripts/boards/components/board_list_new.vue b/app/assets/javascripts/boards/components/board_list_new.vue
index 396aedcc557..92a381a8f57 100644
--- a/app/assets/javascripts/boards/components/board_list_new.vue
+++ b/app/assets/javascripts/boards/components/board_list_new.vue
@@ -1,21 +1,26 @@
<script>
+import Draggable from 'vuedraggable';
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import defaultSortableConfig from '~/sortable/sortable_config';
+import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
import BoardNewIssue from './board_new_issue_new.vue';
import BoardCard from './board_card.vue';
import eventHub from '../eventhub';
-import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'BoardList',
+ i18n: {
+ loadingIssues: __('Loading issues'),
+ loadingMoreissues: __('Loading more issues'),
+ showingAllIssues: __('Showing all issues'),
+ },
components: {
BoardCard,
BoardNewIssue,
GlLoadingIcon,
},
- mixins: [glFeatureFlagMixin()],
props: {
disabled: {
type: Boolean,
@@ -29,11 +34,15 @@ export default {
type: Array,
required: true,
},
+ canAdminList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
scrollOffset: 250,
- filters: boardsStore.state.filters,
showCount: false,
showIssueForm: false,
};
@@ -43,11 +52,11 @@ export default {
paginatedIssueText() {
return sprintf(__('Showing %{pageSize} of %{total} issues'), {
pageSize: this.issues.length,
- total: this.list.issuesSize,
+ total: this.list.issuesCount,
});
},
issuesSizeExceedsMax() {
- return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount;
+ return this.list.maxIssueCount > 0 && this.list.issuesCount > this.list.maxIssueCount;
},
hasNextPage() {
return this.pageInfoByListId[this.list.id].hasNextPage;
@@ -55,15 +64,34 @@ export default {
loading() {
return this.listsFlags[this.list.id]?.isLoading;
},
+ loadingMore() {
+ return this.listsFlags[this.list.id]?.isLoadingMore;
+ },
+ 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;
+ },
+ showingAllIssues() {
+ return this.issues.length === this.list.issuesCount;
+ },
+ treeRootWrapper() {
+ return this.canAdminList ? Draggable : 'ul';
+ },
+ treeRootOptions() {
+ const options = {
+ ...defaultSortableConfig,
+ fallbackOnBody: false,
+ group: 'board-list',
+ tag: 'ul',
+ 'ghost-class': 'board-card-drag-active',
+ 'data-list-id': this.list.id,
+ value: this.issues,
+ };
+
+ return this.canAdminList ? options : {};
+ },
},
watch: {
- filters: {
- handler() {
- this.list.loadingMore = false;
- this.$refs.list.scrollTop = 0;
- },
- deep: true,
- },
issues() {
this.$nextTick(() => {
this.showCount = this.scrollHeight() > Math.ceil(this.listHeight());
@@ -76,35 +104,29 @@ export default {
},
mounted() {
// Scroll event on list to load more
- this.$refs.list.addEventListener('scroll', this.onScroll);
+ this.listRef.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- this.$refs.list.removeEventListener('scroll', this.onScroll);
+ this.listRef.removeEventListener('scroll', this.onScroll);
},
methods: {
- ...mapActions(['fetchIssuesForList']),
+ ...mapActions(['fetchIssuesForList', 'moveIssue']),
listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
+ return this.listRef.getBoundingClientRect().height;
},
scrollHeight() {
- return this.$refs.list.scrollHeight;
+ return this.listRef.scrollHeight;
},
scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
+ return this.listRef.scrollTop + this.listHeight();
},
scrollToTop() {
- this.$refs.list.scrollTop = 0;
+ this.listRef.scrollTop = 0;
},
loadNextPage() {
- const loadingDone = () => {
- this.list.loadingMore = false;
- };
- this.list.loadingMore = true;
- this.fetchIssuesForList({ listId: this.list.id, fetchNext: true })
- .then(loadingDone)
- .catch(loadingDone);
+ this.fetchIssuesForList({ listId: this.list.id, fetchNext: true });
},
toggleForm() {
this.showIssueForm = !this.showIssueForm;
@@ -112,7 +134,7 @@ export default {
onScroll() {
window.requestAnimationFrame(() => {
if (
- !this.list.loadingMore &&
+ !this.loadingMore &&
this.scrollTop() > this.scrollHeight() - this.scrollOffset &&
this.hasNextPage
) {
@@ -120,32 +142,83 @@ export default {
}
});
},
+ handleDragOnStart() {
+ sortableStart();
+ },
+ handleDragOnEnd(params) {
+ sortableEnd();
+ const { newIndex, oldIndex, from, to, item } = params;
+ const { issueId, issueIid, issuePath } = item.dataset;
+ const { children } = to;
+ let moveBeforeId;
+ let moveAfterId;
+
+ const getIssueId = el => Number(el.dataset.issueId);
+
+ // If issue is being moved within the same list
+ if (from === to) {
+ if (newIndex > oldIndex && children.length > 1) {
+ // If issue is being moved down we look for the issue that ends up before
+ moveBeforeId = getIssueId(children[newIndex]);
+ } else if (newIndex < oldIndex && children.length > 1) {
+ // If issue is being moved up we look for the issue that ends up after
+ moveAfterId = getIssueId(children[newIndex]);
+ } else {
+ // If issue remains in the same list at the same position we do nothing
+ return;
+ }
+ } else {
+ // We look for the issue that ends up before the moved issue if it exists
+ if (children[newIndex - 1]) {
+ moveBeforeId = getIssueId(children[newIndex - 1]);
+ }
+ // We look for the issue that ends up after the moved issue if it exists
+ if (children[newIndex]) {
+ moveAfterId = getIssueId(children[newIndex]);
+ }
+ }
+
+ this.moveIssue({
+ issueId,
+ issueIid,
+ issuePath,
+ fromListId: from.dataset.listId,
+ toListId: to.dataset.listId,
+ moveBeforeId,
+ moveAfterId,
+ });
+ },
},
};
</script>
<template>
<div
- v-show="list.isExpanded"
+ v-show="!list.collapsed"
class="board-list-component gl-relative gl-h-full gl-display-flex gl-flex-direction-column"
data-qa-selector="board_list_cards_area"
>
<div
v-if="loading"
class="gl-mt-4 gl-text-center"
- :aria-label="__('Loading issues')"
+ :aria-label="$options.i18n.loadingIssues"
data-testid="board_list_loading"
>
<gl-loading-icon />
</div>
- <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
- <ul
+ <board-new-issue v-if="list.listType !== 'closed' && showIssueForm" :list="list" />
+ <component
+ :is="treeRootWrapper"
v-show="!loading"
ref="list"
+ v-bind="treeRootOptions"
:data-board="list.id"
- :data-board-type="list.type"
+ :data-board-type="list.listType"
:class="{ 'bg-danger-100': issuesSizeExceedsMax }"
class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list"
+ data-testid="tree-root-wrapper"
+ @start="handleDragOnStart"
+ @end="handleDragOnEnd"
>
<board-card
v-for="(issue, index) in issues"
@@ -157,10 +230,10 @@ export default {
:disabled="disabled"
/>
<li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
- <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" />
- <span v-if="issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
+ <gl-loading-icon v-if="loadingMore" :label="$options.i18n.loadingMoreissues" />
+ <span v-if="showingAllIssues">{{ $options.i18n.showingAllIssues }}</span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
- </ul>
+ </component>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_promotion_state.js b/app/assets/javascripts/boards/components/board_promotion_state.js
deleted file mode 100644
index ff8b4c56321..00000000000
--- a/app/assets/javascripts/boards/components/board_promotion_state.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 80070b25bd0..60db8fefe82 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -53,7 +53,7 @@ export default {
return this.activeList.label;
},
boardListType() {
- return this.activeList.type || null;
+ return this.activeList.type || this.activeList.listType || null;
},
listTypeTitle() {
return this.$options.labelListText;
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 0b079c78209..4f23c38d0f7 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -3,17 +3,18 @@ import { throttle } from 'lodash';
import {
GlLoadingIcon,
GlSearchBoxByType,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownDivider,
- GlDeprecatedDropdownHeader,
- GlDeprecatedDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlModalDirective,
} from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import projectQuery from '../queries/project_boards.query.graphql';
-import groupQuery from '../queries/group_boards.query.graphql';
+import projectQuery from '../graphql/project_boards.query.graphql';
+import groupQuery from '../graphql/group_boards.query.graphql';
import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
@@ -26,10 +27,13 @@ export default {
BoardForm,
GlLoadingIcon,
GlSearchBoxByType,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownDivider,
- GlDeprecatedDropdownHeader,
- GlDeprecatedDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ },
+ directives: {
+ GlModalDirective,
},
props: {
currentBoard: {
@@ -108,7 +112,7 @@ export default {
return this.groupId ? 'group' : 'project';
},
loading() {
- return this.loadingRecentBoards && this.loadingBoards;
+ return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
currentPage() {
return this.state.currentPage;
@@ -235,22 +239,17 @@ export default {
<template>
<div class="boards-switcher js-boards-selector gl-mr-3">
<span class="boards-selector-wrapper js-boards-selector-wrapper">
- <gl-deprecated-dropdown
+ <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"
>
- <div>
- <div class="dropdown-title mb-0" @mousedown.prevent>
- {{ s__('IssueBoards|Switch board') }}
- </div>
- </div>
-
- <gl-deprecated-dropdown-header class="mt-0">
- <gl-search-box-by-type ref="searchBox" v-model="filterTerm" />
- </gl-deprecated-dropdown-header>
+ <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"
@@ -259,49 +258,50 @@ export default {
class="dropdown-content flex-fill"
@scroll.passive="throttledSetScrollFade"
>
- <gl-deprecated-dropdown-item
+ <gl-dropdown-item
v-show="filteredBoards.length === 0"
class="gl-pointer-events-none text-secondary"
>
{{ s__('IssueBoards|No matching boards found') }}
- </gl-deprecated-dropdown-item>
+ </gl-dropdown-item>
- <h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
+ <gl-dropdown-section-header v-if="showRecentSection">
{{ __('Recent') }}
- </h6>
+ </gl-dropdown-section-header>
<template v-if="showRecentSection">
- <gl-deprecated-dropdown-item
+ <gl-dropdown-item
v-for="recentBoard in recentBoards"
:key="`recent-${recentBoard.id}`"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${recentBoard.id}`"
>
{{ recentBoard.name }}
- </gl-deprecated-dropdown-item>
+ </gl-dropdown-item>
</template>
- <hr v-if="showRecentSection" class="my-1" />
+ <gl-dropdown-divider v-if="showRecentSection" />
- <h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
+ <gl-dropdown-section-header v-if="showRecentSection">
{{ __('All') }}
- </h6>
+ </gl-dropdown-section-header>
- <gl-deprecated-dropdown-item
+ <gl-dropdown-item
v-for="otherBoard in filteredBoards"
:key="otherBoard.id"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${otherBoard.id}`"
>
{{ otherBoard.name }}
- </gl-deprecated-dropdown-item>
- <gl-deprecated-dropdown-item v-if="hasMissingBoards" class="small unclickable">
+ </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-deprecated-dropdown-item>
+ </gl-dropdown-item>
</div>
<div
@@ -313,25 +313,27 @@ export default {
<gl-loading-icon v-if="loading" />
<div v-if="canAdminBoard">
- <gl-deprecated-dropdown-divider />
+ <gl-dropdown-divider />
- <gl-deprecated-dropdown-item
+ <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-deprecated-dropdown-item>
+ </gl-dropdown-item>
- <gl-deprecated-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-deprecated-dropdown-item>
+ </gl-dropdown-item>
</div>
- </gl-deprecated-dropdown>
+ </gl-dropdown>
<board-form
v-if="currentPage"
@@ -343,6 +345,7 @@ export default {
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
+ :current-board="currentBoard"
/>
</span>
</div>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 45ce1e51489..ddd20ff281c 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -10,6 +10,7 @@ import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import { ListType } from '../constants';
export default {
components: {
@@ -122,7 +123,13 @@ export default {
return true;
},
isNonListLabel(label) {
- return label.id && !(this.list.type === 'label' && this.list.title === label.title);
+ return (
+ label.id &&
+ !(
+ (this.list.type || this.list.listType) === ListType.label &&
+ this.list.title === label.title
+ )
+ );
},
filterByLabel(label) {
if (!this.updateFilters) return;
@@ -158,9 +165,13 @@ export default {
class="confidential-icon gl-mr-2"
:aria-label="__('Confidential')"
/>
- <a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{
- issue.title
- }}</a>
+ <a
+ :href="issue.path || issue.webUrl || ''"
+ :title="issue.title"
+ class="js-no-trigger"
+ @mousemove.stop
+ >{{ issue.title }}</a
+ >
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
@@ -196,7 +207,11 @@ export default {
#{{ issue.iid }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
- <issue-due-date v-if="issue.dueDate" :date="issue.dueDate" :closed="issue.closed" />
+ <issue-due-date
+ v-if="issue.dueDate"
+ :date="issue.dueDate"
+ :closed="issue.closed || Boolean(issue.closedAt)"
+ />
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
v-if="validIssueWeight"
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 47eee5306da..d1011c24977 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -15,6 +15,7 @@ function shouldCreateListGraphQL(label) {
return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
}
+// eslint-disable-next-line @gitlab/no-global-event-off
$(document)
.off('created.label')
.on('created.label', (e, label, addNewList) => {
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index f90fe582566..9c90938fc52 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -7,6 +7,7 @@ import eventHub from '../eventhub';
import Api from '../../api';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { ListType } from '../constants';
export default {
name: 'BoardProjectSelect',
@@ -53,7 +54,7 @@ export default {
this.loading = true;
const additionalAttrs = {};
- if (this.list.type && this.list.type !== 'backlog') {
+ if ((this.list.type || this.list.listType) !== ListType.backlog) {
additionalAttrs.min_access_level = featureAccessLevel.EVERYONE;
}
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 5fb7a9b210c..ce267be6d45 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -50,6 +50,13 @@ export default {
}
window.removeEventListener('click', this.collapseWhenOffClick);
},
+ toggle({ emitEvent = true } = {}) {
+ if (this.edit) {
+ this.collapse({ emitEvent });
+ } else {
+ this.expand();
+ }
+ },
},
};
</script>
@@ -64,18 +71,18 @@ export default {
<gl-button
v-if="canUpdate"
variant="link"
- class="gl-text-gray-900!"
+ class="gl-text-gray-900! js-sidebar-dropdown-toggle"
data-testid="edit-button"
- @click="expand()"
+ @click="toggle"
>
{{ __('Edit') }}
</gl-button>
</div>
- <div v-show="!edit" class="gl-text-gray-400" data-testid="collapsed-content">
+ <div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content">
<slot name="collapsed">{{ __('None') }}</slot>
</div>
<div v-show="edit" data-testid="expanded-content">
- <slot></slot>
+ <slot :edit="edit"></slot>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
index 6935ead2706..904ceaed1b3 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -79,7 +79,7 @@ export default {
<span class="gl-mx-2">-</span>
<gl-button
variant="link"
- class="gl-text-gray-400!"
+ class="gl-text-gray-500!"
data-testid="reset-button"
:disabled="loading"
@click="setDueDate(null)"
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 9d537a4ef2c..6a407bd6ba6 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
@@ -92,7 +92,7 @@ export default {
@close="removeLabel(label.id)"
/>
</template>
- <template>
+ <template #default="{ edit }">
<labels-select
ref="labelsSelect"
:allow-label-edit="false"
@@ -105,6 +105,7 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')"
+ :is-editing="edit"
variant="embedded"
class="gl-display-block labels gl-w-full"
@updateSelectedLabels="setLabels"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
new file mode 100644
index 00000000000..78c3f8acc62
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -0,0 +1,161 @@
+<script>
+import { mapGetters, mapActions } from 'vuex';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { fetchPolicies } from '~/lib/graphql';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import groupMilestones from '../../graphql/group_milestones.query.graphql';
+import createFlash from '~/flash';
+import { __, s__ } from '~/locale';
+
+export default {
+ components: {
+ BoardEditableItem,
+ GlDropdown,
+ GlLoadingIcon,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlDropdownDivider,
+ },
+ data() {
+ return {
+ milestones: [],
+ searchTitle: '',
+ loading: false,
+ edit: false,
+ };
+ },
+ apollo: {
+ milestones: {
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ query: groupMilestones,
+ debounce: 250,
+ skip() {
+ return !this.edit;
+ },
+ variables() {
+ return {
+ fullPath: this.groupFullPath,
+ searchTitle: this.searchTitle,
+ state: 'active',
+ includeDescendants: true,
+ };
+ },
+ update(data) {
+ const edges = data?.group?.milestones?.edges ?? [];
+ return edges.map(item => item.node);
+ },
+ error() {
+ createFlash({ message: this.$options.i18n.fetchMilestonesError });
+ },
+ },
+ },
+ computed: {
+ ...mapGetters({ issue: 'activeIssue' }),
+ hasMilestone() {
+ return this.issue.milestone !== null;
+ },
+ groupFullPath() {
+ const { referencePath = '' } = this.issue;
+ return referencePath.slice(0, referencePath.indexOf('/'));
+ },
+ projectPath() {
+ const { referencePath = '' } = this.issue;
+ return referencePath.slice(0, referencePath.indexOf('#'));
+ },
+ dropdownText() {
+ return this.issue.milestone?.title ?? this.$options.i18n.noMilestone;
+ },
+ },
+ mounted() {
+ this.$root.$on('bv::dropdown::hide', () => {
+ this.$refs.sidebarItem.collapse();
+ });
+ },
+ methods: {
+ ...mapActions(['setActiveIssueMilestone']),
+ handleOpen() {
+ this.edit = true;
+ this.$refs.dropdown.show();
+ },
+ async setMilestone(milestoneId) {
+ this.loading = true;
+ this.searchTitle = '';
+ this.$refs.sidebarItem.collapse();
+
+ try {
+ const input = { milestoneId, projectPath: this.projectPath };
+ await this.setActiveIssueMilestone(input);
+ } catch (e) {
+ createFlash({ message: this.$options.i18n.updateMilestoneError });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ i18n: {
+ milestone: __('Milestone'),
+ noMilestone: __('No milestone'),
+ assignMilestone: __('Assign milestone'),
+ noMilestonesFound: s__('Milestones|No milestones found'),
+ fetchMilestonesError: __('There was a problem fetching milestones.'),
+ updateMilestoneError: __('An error occurred while updating the milestone.'),
+ },
+};
+</script>
+
+<template>
+ <board-editable-item
+ ref="sidebarItem"
+ :title="$options.i18n.milestone"
+ :loading="loading"
+ @open="handleOpen()"
+ @close="edit = false"
+ >
+ <template v-if="hasMilestone" #collapsed>
+ <strong class="gl-text-gray-900">{{ issue.milestone.title }}</strong>
+ </template>
+ <template>
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropdownText"
+ :header-text="$options.i18n.assignMilestone"
+ block
+ >
+ <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
+ <gl-dropdown-item
+ data-testid="no-milestone-item"
+ :is-check-item="true"
+ :is-checked="!issue.milestone"
+ @click="setMilestone(null)"
+ >
+ {{ $options.i18n.noMilestone }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
+ <template v-else-if="milestones.length > 0">
+ <gl-dropdown-item
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ :is-check-item="true"
+ :is-checked="issue.milestone && milestone.id === issue.milestone.id"
+ data-testid="milestone-item"
+ @click="setMilestone(milestone.id)"
+ >
+ {{ milestone.title }}
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-text v-else data-testid="no-milestones-found">
+ {{ $options.i18n.noMilestonesFound }}
+ </gl-dropdown-text>
+ </gl-dropdown>
+ </template>
+ </board-editable-item>
+</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 49cb560594c..9264fac5eda 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -9,8 +9,6 @@ export const ListType = {
backlog: 'backlog',
closed: 'closed',
label: 'label',
- promotion: 'promotion',
- blank: 'blank',
};
export const inactiveId = 0;
@@ -18,11 +16,7 @@ export const inactiveId = 0;
export const ISSUABLE = 'issuable';
export const LIST = 'list';
-/* eslint-disable-next-line @gitlab/require-i18n-strings */
-export const DEFAULT_LABELS = ['to do', 'doing'];
-
export default {
BoardType,
ListType,
- DEFAULT_LABELS,
};
diff --git a/app/assets/javascripts/boards/ee_functions.js b/app/assets/javascripts/boards/ee_functions.js
index 419a640d5c5..b6b34556663 100644
--- a/app/assets/javascripts/boards/ee_functions.js
+++ b/app/assets/javascripts/boards/ee_functions.js
@@ -1,5 +1,3 @@
-export const setPromotionState = () => {};
-
export const setWeightFetchingState = () => {};
export const setEpicFetchingState = () => {};
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 4fa78ecd5a4..1667dcc9f2e 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,7 +1,10 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
+import { transformBoardConfig } from 'ee_else_ce/boards/boards_util';
import FilteredSearchContainer from '../filtered_search/container';
import boardsStore from './stores/boards_store';
+import vuexstore from './stores';
+import { updateHistory } from '~/lib/utils/url_utility';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
@@ -22,18 +25,28 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
this.isHandledAsync = true;
this.cantEdit = cantEdit.filter(i => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
+
+ if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) {
+ const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig);
+ if (boardConfigPath !== '') {
+ const filterPath = window.location.search ? `${window.location.search}&` : '?';
+ updateHistory({
+ url: `${filterPath}${transformBoardConfig(vuexstore.state.boardConfig)}`,
+ });
+ }
+ }
}
updateObject(path) {
const groupByParam = new URLSearchParams(window.location.search).get('group_by');
this.store.path = `${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`;
- if (gon.features.boardsWithSwimlanes || gon.features.graphqlBoardLists) {
- boardsStore.updateFiltersUrl();
- boardsStore.performSearch();
- }
-
- if (this.updateUrl) {
+ if (vuexstore.getters.shouldUseGraphQL) {
+ updateHistory({
+ url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`,
+ });
+ vuexstore.dispatch('performSearch');
+ } else if (this.updateUrl) {
boardsStore.updateFiltersUrl();
}
}
diff --git a/app/assets/javascripts/boards/queries/board.fragment.graphql b/app/assets/javascripts/boards/graphql/board.fragment.graphql
index 872a4c4afbc..872a4c4afbc 100644
--- a/app/assets/javascripts/boards/queries/board.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/board.fragment.graphql
diff --git a/app/assets/javascripts/boards/queries/board.mutation.graphql b/app/assets/javascripts/boards/graphql/board.mutation.graphql
index ef2b81a7939..ef2b81a7939 100644
--- a/app/assets/javascripts/boards/queries/board.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board.mutation.graphql
diff --git a/app/assets/javascripts/boards/queries/board_labels.query.graphql b/app/assets/javascripts/boards/graphql/board_labels.query.graphql
index 42a94419a97..42a94419a97 100644
--- a/app/assets/javascripts/boards/queries/board_labels.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_labels.query.graphql
diff --git a/app/assets/javascripts/boards/queries/board_list.fragment.graphql b/app/assets/javascripts/boards/graphql/board_list.fragment.graphql
index bbf3314377e..bbf3314377e 100644
--- a/app/assets/javascripts/boards/queries/board_list.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list.fragment.graphql
diff --git a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
index 48420b349ae..f78a21baa7f 100644
--- a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
+#import "ee_else_ce/boards/graphql/board_list.fragment.graphql"
mutation CreateBoardList(
$boardId: BoardID!
diff --git a/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql
index ef3fd36e980..ef3fd36e980 100644
--- a/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql
diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql
index d85b736720b..d85b736720b 100644
--- a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql
diff --git a/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql
index b474c9acb93..b474c9acb93 100644
--- a/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql
diff --git a/app/assets/javascripts/boards/queries/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
index 88425e9a9c1..eb922f162f8 100644
--- a/app/assets/javascripts/boards/queries/board_lists.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
+#import "ee_else_ce/boards/graphql/board_list.fragment.graphql"
query ListIssues(
$fullPath: ID!
diff --git a/app/assets/javascripts/boards/queries/group_boards.query.graphql b/app/assets/javascripts/boards/graphql/group_boards.query.graphql
index 74c224add7d..feafd6ae10d 100644
--- a/app/assets/javascripts/boards/queries/group_boards.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_boards.query.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/boards/queries/board.fragment.graphql"
+#import "ee_else_ce/boards/graphql/board.fragment.graphql"
query group_boards($fullPath: ID!) {
group(fullPath: $fullPath) {
diff --git a/app/assets/javascripts/boards/graphql/group_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_milestones.query.graphql
new file mode 100644
index 00000000000..f2ab12ef4a7
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/group_milestones.query.graphql
@@ -0,0 +1,17 @@
+query groupMilestones(
+ $fullPath: ID!
+ $state: MilestoneStateEnum
+ $includeDescendants: Boolean
+ $searchTitle: String
+) {
+ group(fullPath: $fullPath) {
+ milestones(state: $state, includeDescendants: $includeDescendants, searchTitle: $searchTitle) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
index 4b429f875a6..1395bef39ed 100644
--- a/app/assets/javascripts/boards/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
@@ -11,6 +11,10 @@ fragment IssueNode on Issue {
webUrl
subscribed
relativePosition
+ milestone {
+ id
+ title
+ }
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/boards/queries/issue_create.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_create.mutation.graphql
index 65be147be07..c1a2361a4e8 100644
--- a/app/assets/javascripts/boards/queries/issue_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_create.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/boards/queries/issue.fragment.graphql"
+#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
mutation CreateIssue($input: CreateIssueInput!) {
createIssue(input: $input) {
diff --git a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
index ff6aa597f48..3c574fd8c87 100644
--- a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/boards/queries/issue.fragment.graphql"
+#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
mutation IssueMoveList(
$projectPath: ID!
diff --git a/app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql
index bbea248cf85..bbea248cf85 100644
--- a/app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql
diff --git a/app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
index 3c5f4b3e3bd..3c5f4b3e3bd 100644
--- a/app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
diff --git a/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql
new file mode 100644
index 00000000000..5dc78a03a06
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql
@@ -0,0 +1,12 @@
+mutation issueSetMilestone($input: UpdateIssueInput!) {
+ updateIssue(input: $input) {
+ issue {
+ milestone {
+ id
+ title
+ description
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql
index 1f383245ac2..1f383245ac2 100644
--- a/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql
diff --git a/app/assets/javascripts/boards/queries/lists_issues.query.graphql b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
index 5dbfe4675c6..43af7d2b2f1 100644
--- a/app/assets/javascripts/boards/queries/lists_issues.query.graphql
+++ b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/boards/queries/issue.fragment.graphql"
+#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
query ListIssues(
$fullPath: ID!
diff --git a/app/assets/javascripts/boards/queries/project_boards.query.graphql b/app/assets/javascripts/boards/graphql/project_boards.query.graphql
index a1326bd5eff..f98d25ba671 100644
--- a/app/assets/javascripts/boards/queries/project_boards.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_boards.query.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/boards/queries/board.fragment.graphql"
+#import "ee_else_ce/boards/graphql/board.fragment.graphql"
query project_boards($fullPath: ID!) {
project(fullPath: $fullPath) {
diff --git a/app/assets/javascripts/boards/queries/users_search.query.graphql b/app/assets/javascripts/boards/graphql/users_search.query.graphql
index ca016495d79..ca016495d79 100644
--- a/app/assets/javascripts/boards/queries/users_search.query.graphql
+++ b/app/assets/javascripts/boards/graphql/users_search.query.graphql
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index d3e40299d8d..64a4f246735 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
@@ -9,7 +9,6 @@ import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import {
- setPromotionState,
setWeightFetchingState,
setEpicFetchingState,
getMilestoneTitle,
@@ -41,7 +40,6 @@ import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
- urlParamsToObject,
} from '~/lib/utils/common_utils';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
@@ -77,7 +75,6 @@ export default () => {
el: $boardApp,
components: {
BoardContent,
- Board: () => import('ee_else_ce/boards/components/board_column.vue'),
BoardSidebar,
BoardAddIssuesModal,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
@@ -114,7 +111,6 @@ export default () => {
};
},
computed: {
- ...mapState(['isShowingEpicsSwimlanes']),
...mapGetters(['shouldUseGraphQL']),
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
@@ -133,7 +129,17 @@ export default () => {
...endpoints,
boardType: this.parent,
disabled: this.disabled,
- showPromotion: parseBoolean($boardApp.getAttribute('data-show-promotion')),
+ boardConfig: {
+ milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
+ milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
+ iterationId: parseInt($boardApp.dataset.boardIterationId, 10),
+ iterationTitle: $boardApp.dataset.boardIterationTitle || '',
+ assigneeUsername: $boardApp.dataset.boardAssigneeUsername,
+ labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels || []) : [],
+ weight: $boardApp.dataset.boardWeight
+ ? parseInt($boardApp.dataset.boardWeight, 10)
+ : null,
+ },
});
boardsStore.setEndpoints(endpoints);
boardsStore.rootPath = this.boardsEndpoint;
@@ -142,7 +148,6 @@ export default () => {
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
- eventHub.$on('performSearch', this.performSearch);
eventHub.$on('initialBoardLoad', this.initialBoardLoad);
},
beforeDestroy() {
@@ -150,7 +155,6 @@ export default () => {
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
- eventHub.$off('performSearch', this.performSearch);
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
},
mounted() {
@@ -166,22 +170,13 @@ export default () => {
}
},
methods: {
- ...mapActions([
- 'setInitialBoardData',
- 'setFilters',
- 'fetchEpicsSwimlanes',
- 'resetIssues',
- 'resetEpics',
- 'fetchLists',
- ]),
+ ...mapActions(['setInitialBoardData', 'performSearch']),
initialBoardLoad() {
boardsStore
.all()
.then(res => res.data)
.then(lists => {
lists.forEach(list => boardsStore.addList(list));
- boardsStore.addBlankState();
- setPromotionState(boardsStore);
this.loading = false;
})
.catch(() => {
@@ -191,17 +186,6 @@ export default () => {
updateTokens() {
this.filterManager.updateTokens();
},
- performSearch() {
- this.setFilters(convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)));
- if (gon.features.boardsWithSwimlanes && this.isShowingEpicsSwimlanes) {
- this.resetEpics();
- this.resetIssues();
- this.fetchEpicsSwimlanes({});
- } else if (gon.features.graphqlBoardLists && !this.isShowingEpicsSwimlanes) {
- this.fetchLists();
- this.resetIssues();
- }
- },
updateDetailIssue(newIssue, multiSelect = false) {
const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
@@ -303,7 +287,7 @@ export default () => {
const issueBoardsModal = document.getElementById('js-add-issues-btn');
- if (issueBoardsModal) {
+ if (issueBoardsModal && gon.features.addIssuesButton) {
// eslint-disable-next-line no-new
new Vue({
el: issueBoardsModal,
@@ -350,5 +334,8 @@ export default () => {
toggleEpicsSwimlanes();
}
- mountMultipleBoardsSwitcher();
+ mountMultipleBoardsSwitcher({
+ boardsEndpoint: $boardApp.dataset.boardsEndpoint,
+ recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
+ });
};
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 51bb72b7657..df65ebb7526 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -10,7 +10,7 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
-export default () => {
+export default (endpoints = {}) => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
return new Vue({
el: boardsSwitcherElement,
@@ -35,6 +35,9 @@ export default () => {
return { boardsSelectorProps };
},
+ provide: {
+ endpoints,
+ },
render(createElement) {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index dd950a45076..59b97eba9fe 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,9 +1,10 @@
import { pick } from 'lodash';
-import boardListsQuery from 'ee_else_ce/boards/queries/board_lists.query.graphql';
+import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { BoardType, ListType, inactiveId, DEFAULT_LABELS } from '~/boards/constants';
+import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
+import { BoardType, ListType, inactiveId } from '~/boards/constants';
import * as types from './mutation_types';
import {
formatBoardLists,
@@ -12,19 +13,20 @@ import {
formatListsPageInfo,
formatIssue,
} from '../boards_util';
-import boardStore from '~/boards/stores/boards_store';
-
-import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
-import listsIssuesQuery from '../queries/lists_issues.query.graphql';
-import boardLabelsQuery from '../queries/board_labels.query.graphql';
-import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
-import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
-import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
-import destroyBoardListMutation from '../queries/board_list_destroy.mutation.graphql';
-import issueCreateMutation from '../queries/issue_create.mutation.graphql';
-import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
-import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
-import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
+import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
+import boardLabelsQuery from '../graphql/board_labels.query.graphql';
+import createBoardListMutation from '../graphql/board_list_create.mutation.graphql';
+import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
+import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
+import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
+import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
+import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
+import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
+import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
+import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -63,6 +65,18 @@ export default {
commit(types.SET_FILTERS, filterParams);
},
+ performSearch({ dispatch }) {
+ dispatch(
+ 'setFilters',
+ convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)),
+ );
+
+ if (gon.features.graphqlBoardLists) {
+ dispatch('fetchLists');
+ dispatch('resetIssues');
+ }
+ },
+
fetchLists: ({ commit, state, dispatch }) => {
const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
@@ -87,7 +101,6 @@ export default {
if (!lists.nodes.find(l => l.listType === ListType.backlog) && !hideBacklogList) {
dispatch('createList', { backlog: true });
}
- dispatch('generateDefaultLists');
})
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
},
@@ -118,15 +131,9 @@ export default {
},
addList: ({ commit }, list) => {
- // Temporarily using positioning logic from boardStore
- commit(
- types.RECEIVE_ADD_LIST_SUCCESS,
- boardStore.updateListPosition({ ...list, doNotFetchIssues: true }),
- );
+ commit(types.RECEIVE_ADD_LIST_SUCCESS, list);
},
- showPromotionList: () => {},
-
fetchLabels: ({ state, commit }, searchTerm) => {
const { endpoints, boardType } = state;
const { fullPath } = endpoints;
@@ -150,35 +157,14 @@ export default {
.catch(() => commit(types.RECEIVE_LABELS_FAILURE));
},
- generateDefaultLists: async ({ state, commit, dispatch }) => {
- if (state.disabled) {
- return;
- }
- if (
- Object.entries(state.boardLists).find(
- ([, list]) => list.type !== ListType.backlog && list.type !== ListType.closed,
- )
- ) {
- return;
- }
-
- const fetchLabelsAndCreateList = label => {
- return dispatch('fetchLabels', label)
- .then(res => {
- if (res.length > 0) {
- dispatch('createList', { labelId: res[0].id });
- }
- })
- .catch(() => commit(types.GENERATE_DEFAULT_LISTS_FAILURE));
- };
-
- await Promise.all(DEFAULT_LABELS.map(fetchLabelsAndCreateList));
- },
-
moveList: (
{ state, commit, dispatch },
{ listId, replacedListId, newIndex, adjustmentValue },
) => {
+ if (listId === replacedListId) {
+ return;
+ }
+
const { boardLists } = state;
const backupList = { ...boardLists };
const movedList = boardLists[listId];
@@ -315,9 +301,11 @@ export default {
},
setAssignees: ({ commit, getters }, assigneeUsernames) => {
+ commit(types.SET_ASSIGNEE_LOADING, true);
+
return gqlClient
.mutate({
- mutation: updateAssignees,
+ mutation: updateAssigneesMutation,
variables: {
iid: getters.activeIssue.iid,
projectPath: getters.activeIssue.referencePath.split('#')[0],
@@ -325,14 +313,48 @@ export default {
},
})
.then(({ data }) => {
+ const { nodes } = data.issueSetAssignees?.issue?.assignees || [];
+
commit('UPDATE_ISSUE_BY_ID', {
issueId: getters.activeIssue.id,
prop: 'assignees',
- value: data.issueSetAssignees.issue.assignees.nodes,
+ value: nodes,
});
+
+ return nodes;
+ })
+ .catch(() => {
+ createFlash({ message: __('An error occurred while updating assignees.') });
+ })
+ .finally(() => {
+ commit(types.SET_ASSIGNEE_LOADING, false);
});
},
+ setActiveIssueMilestone: async ({ commit, getters }, input) => {
+ const { activeIssue } = getters;
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetMilestoneMutation,
+ variables: {
+ input: {
+ iid: String(activeIssue.iid),
+ milestoneId: getIdFromGraphQLId(input.milestoneId),
+ projectPath: input.projectPath,
+ },
+ },
+ });
+
+ if (data.updateIssue.errors?.length > 0) {
+ throw new Error(data.updateIssue.errors);
+ }
+
+ commit(types.UPDATE_ISSUE_BY_ID, {
+ issueId: activeIssue.id,
+ prop: 'milestone',
+ value: data.updateIssue.issue.milestone,
+ });
+ },
+
createNewIssue: ({ commit, state }, issueInput) => {
const input = issueInput;
const { boardType, endpoints } = state;
@@ -378,7 +400,7 @@ export default {
setActiveIssueLabels: async ({ commit, getters }, input) => {
const { activeIssue } = getters;
const { data } = await gqlClient.mutate({
- mutation: issueSetLabels,
+ mutation: issueSetLabelsMutation,
variables: {
input: {
iid: String(activeIssue.iid),
@@ -403,7 +425,7 @@ export default {
setActiveIssueDueDate: async ({ commit, getters }, input) => {
const { activeIssue } = getters;
const { data } = await gqlClient.mutate({
- mutation: issueSetDueDate,
+ mutation: issueSetDueDateMutation,
variables: {
input: {
iid: String(activeIssue.iid),
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 337b2897fe9..36702b6ca5f 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -1,9 +1,8 @@
/* eslint-disable no-shadow, no-param-reassign,consistent-return */
/* global List */
/* global ListIssue */
-import { sortBy, pick } from 'lodash';
+import { sortBy } from 'lodash';
import Vue from 'vue';
-import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import {
urlParamsToObject,
@@ -22,8 +21,6 @@ import ListLabel from '../models/label';
import ListAssignee from '../models/assignee';
import ListMilestone from '../models/milestone';
-import createBoardMutation from '../queries/board.mutation.graphql';
-
const PER_PAGE = 20;
export const gqlClient = createDefaultClient();
@@ -125,20 +122,6 @@ const boardsStore = {
.querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
?.classList.remove('is-active');
},
- shouldAddBlankState() {
- // Decide whether to add the blank state
- return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0];
- },
- addBlankState() {
- if (!this.shouldAddBlankState() || this.welcomeIsHidden()) return;
-
- this.generateDefaultLists()
- .then(res => res.data)
- .then(data => Promise.all(data.map(list => this.addList(list))))
- .catch(() => {
- this.removeList(undefined, 'label');
- });
- },
findIssueLabel(issue, findLabel) {
return issue.labels.find(label => label.id === findLabel.id);
@@ -202,9 +185,6 @@ const boardsStore = {
return list.issues.find(issue => issue.id === id);
},
- welcomeIsHidden() {
- return parseBoolean(Cookies.get('issue_board_welcome_hidden'));
- },
removeList(id, type = 'blank') {
const list = this.findList('id', id, type);
@@ -302,11 +282,7 @@ const boardsStore = {
onNewListIssueResponse(list, issue, data) {
issue.refreshData(data);
- if (
- !gon.features.boardsWithSwimlanes &&
- !gon.features.graphqlBoardLists &&
- list.issues.length > 1
- ) {
+ if (list.issues.length > 1) {
const moveBeforeId = list.issues[1].id;
this.moveIssue(issue.id, null, null, null, moveBeforeId);
}
@@ -516,10 +492,6 @@ const boardsStore = {
eventHub.$emit('updateTokens');
},
- performSearch() {
- eventHub.$emit('performSearch');
- },
-
setListDetail(newList) {
this.detail.list = newList;
},
@@ -566,10 +538,6 @@ const boardsStore = {
return axios.get(this.state.endpoints.listsEndpoint);
},
- generateDefaultLists() {
- return axios.post(this.state.endpoints.listsEndpointGenerate, {});
- },
-
createList(entityId, entityType) {
const list = {
[entityType]: entityId,
@@ -785,52 +753,6 @@ const boardsStore = {
return axios.get(this.state.endpoints.recentBoardsEndpoint);
},
- createBoard(board) {
- const boardPayload = { ...board };
- boardPayload.label_ids = (board.labels || []).map(b => b.id);
-
- if (boardPayload.label_ids.length === 0) {
- boardPayload.label_ids = [''];
- }
-
- if (boardPayload.assignee) {
- boardPayload.assignee_id = boardPayload.assignee.id;
- }
-
- if (boardPayload.milestone) {
- boardPayload.milestone_id = boardPayload.milestone.id;
- }
-
- if (boardPayload.id) {
- const input = {
- ...pick(boardPayload, ['hideClosedList', 'hideBacklogList']),
- id: this.generateBoardGid(boardPayload.id),
- };
-
- return Promise.all([
- axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload }),
- gqlClient.mutate({
- mutation: createBoardMutation,
- variables: input,
- }),
- ]);
- }
-
- return axios
- .post(this.generateBoardsPath(), { board: boardPayload })
- .then(resp => resp.data)
- .then(data => {
- gqlClient.mutate({
- mutation: createBoardMutation,
- variables: {
- ...pick(boardPayload, ['hideClosedList', 'hideBacklogList']),
- id: this.generateBoardGid(data.id),
- },
- });
- return data;
- });
- },
-
deleteBoard({ id }) {
return axios.delete(this.generateBoardsPath(id));
},
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index cd28b4a0ff7..ca6887b6f45 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -2,15 +2,8 @@ import { find } from 'lodash';
import { inactiveId } from '../constants';
export default {
- labelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
isSidebarOpen: state => state.activeId !== inactiveId,
- isSwimlanesOn: state => {
- if (!gon?.features?.boardsWithSwimlanes && !gon?.features?.swimlanes) {
- return false;
- }
-
- return state.isShowingEpicsSwimlanes;
- },
+ isSwimlanesOn: () => false,
getIssueById: state => id => {
return state.issues[id] || {};
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 3a57cb9b5e1..2b2c2bee51c 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -34,4 +34,5 @@ export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID';
+export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index bb083158c8f..8c4e514710f 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -13,7 +13,7 @@ const notImplemented = () => {
export const removeIssueFromList = ({ state, listId, issueId }) => {
Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
const list = state.boardLists[listId];
- Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize - 1 });
+ Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount - 1 });
};
export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
@@ -27,16 +27,16 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
listIssues.splice(newIndex, 0, issueId);
Vue.set(state.issuesByListId, listId, listIssues);
const list = state.boardLists[listId];
- Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize + 1 });
+ Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount + 1 });
};
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
- const { boardType, disabled, showPromotion, ...endpoints } = data;
+ const { boardType, disabled, boardConfig, ...endpoints } = data;
state.endpoints = endpoints;
state.boardType = boardType;
state.disabled = disabled;
- state.showPromotion = showPromotion;
+ state.boardConfig = boardConfig;
},
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
@@ -143,6 +143,10 @@ export default {
Vue.set(state.issues[issueId], prop, value);
},
+ [mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
+ state.isSettingAssignees = isLoading;
+ },
+
[mutationTypes.REQUEST_ADD_ISSUE]: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index b91c09f8051..573e98e56e0 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -4,16 +4,17 @@ export default () => ({
endpoints: {},
boardType: null,
disabled: false,
- showPromotion: false,
isShowingLabels: true,
activeId: inactiveId,
sidebarType: '',
boardLists: {},
listsFlags: {},
issuesByListId: {},
+ isSettingAssignees: false,
pageInfoByListId: {},
issues: {},
filterParams: {},
+ boardConfig: {},
error: undefined,
// TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes: false,