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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
commit43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch)
treedceebdc68925362117480a5d672bcff122fb625b /app/assets/javascripts/boards
parent20c84b99005abd1c82101dfeff264ac50d2df211 (diff)
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column.vue116
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue101
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue124
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue39
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue20
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue124
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue148
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue42
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue28
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue233
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue96
-rw-r--r--app/assets/javascripts/boards/components/board_top_bar.vue45
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue39
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue9
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue28
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue9
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue38
-rw-r--r--app/assets/javascripts/boards/constants.js29
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists.query.graphql5
-rw-r--r--app/assets/javascripts/boards/graphql/client/active_board_item.query.graphql7
-rw-r--r--app/assets/javascripts/boards/graphql/client/board_toggle_collapsed.mutation.graphql9
-rw-r--r--app/assets/javascripts/boards/graphql/client/set_active_board_item.mutation.graphql7
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql2
-rw-r--r--app/assets/javascripts/boards/index.js7
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js21
-rw-r--r--app/assets/javascripts/boards/stores/actions.js27
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js11
31 files changed, 939 insertions, 461 deletions
diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue
index c5411ec313a..90f7059da86 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column.vue
@@ -1,13 +1,24 @@
<script>
-import { GlFormRadio, GlFormRadioGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import {
+ GlTooltipDirective as GlTooltip,
+ GlButton,
+ GlCollapsibleListbox,
+ GlIcon,
+} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
+import { __ } from '~/locale';
export default {
+ i18n: {
+ value: __('Value'),
+ noResults: __('No matching results'),
+ },
components: {
BoardAddNewColumnForm,
- GlFormRadio,
- GlFormRadioGroup,
+ GlButton,
+ GlCollapsibleListbox,
+ GlIcon,
},
directives: {
GlTooltip,
@@ -17,6 +28,7 @@ export default {
return {
selectedId: null,
selectedLabel: null,
+ selectedIdValid: true,
};
},
computed: {
@@ -25,6 +37,15 @@ export default {
columnForSelected() {
return this.getListByLabelId(this.selectedId);
},
+ items() {
+ return (
+ this.labels.map((i) => ({
+ ...i,
+ text: i.title,
+ value: i.id,
+ })) || []
+ );
+ },
},
created() {
this.filterItems();
@@ -33,6 +54,7 @@ export default {
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
addList() {
if (!this.selectedLabel) {
+ this.selectedIdValid = false;
return;
}
@@ -61,53 +83,67 @@ export default {
this.selectedLabel = { ...label };
}
},
+ onHide() {
+ this.searchValue = '';
+ this.$emit('filter-items', '');
+ this.$emit('hide');
+ },
},
};
</script>
<template>
<board-add-new-column-form
- :loading="labelsLoading"
- :none-selected="__('Select a label')"
- :search-placeholder="__('Search labels')"
- :selected-id="selectedId"
+ :selected-id-valid="selectedIdValid"
@filter-items="filterItems"
@add-list="addList"
>
- <template #selected>
- <template v-if="selectedLabel">
- <span
- class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
- :style="{
- backgroundColor: selectedLabel.color,
- }"
- ></span>
- <div class="gl-text-truncate">{{ selectedLabel.title }}</div>
- </template>
- </template>
-
- <template #items>
- <gl-form-radio-group
- v-if="labels.length > 0"
- class="gl-overflow-y-auto gl-px-5 gl-pt-3"
- :checked="selectedId"
- @change="setSelectedItem"
+ <template #dropdown>
+ <gl-collapsible-listbox
+ class="gl-mb-3 gl-max-w-full"
+ :items="items"
+ searchable
+ :search-placeholder="__('Search labels')"
+ :searching="labelsLoading"
+ :selected="selectedId"
+ :no-results-text="$options.i18n.noResults"
+ @select="setSelectedItem"
+ @search="filterItems"
+ @hidden="onHide"
>
- <label
- v-for="label in labels"
- :key="label.id"
- class="gl-display-flex gl-mb-5 gl-font-weight-normal gl-overflow-break-word"
- >
- <gl-form-radio :value="label.id" />
- <span
- class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
- :style="{
- backgroundColor: label.color,
- }"
- ></span>
- <span>{{ label.title }}</span>
- </label>
- </gl-form-radio-group>
+ <template #toggle>
+ <gl-button
+ class="gl-max-w-full gl-display-flex gl-align-items-center gl-text-truncate"
+ :class="{ 'gl-inset-border-1-red-400!': !selectedIdValid }"
+ button-text-classes="gl-display-flex"
+ >
+ <template v-if="selectedLabel">
+ <span
+ class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
+ :style="{
+ backgroundColor: selectedLabel.color,
+ }"
+ ></span>
+ <div class="gl-text-truncate">{{ selectedLabel.title }}</div>
+ </template>
+
+ <template v-else>{{ __('Select a label') }}</template>
+ <gl-icon class="dropdown-chevron gl-ml-2" name="chevron-down" />
+ </gl-button>
+ </template>
+
+ <template #list-item="{ item }">
+ <label class="gl-display-flex gl-font-weight-normal gl-overflow-break-word gl-mb-0">
+ <span
+ class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
+ :style="{
+ backgroundColor: item.color,
+ }"
+ ></span>
+ <span>{{ item.title }}</span>
+ </label>
+ </template>
+ </gl-collapsible-listbox>
</template>
</board-add-new-column-form>
</template>
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
index 1899d42fa4d..259423df07f 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
@@ -1,12 +1,5 @@
<script>
-import {
- GlButton,
- GlDropdown,
- GlFormGroup,
- GlIcon,
- GlSearchBoxByType,
- GlSkeletonLoader,
-} from '@gitlab/ui';
+import { GlButton, GlFormGroup } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
@@ -15,81 +8,34 @@ export default {
add: __('Add to board'),
cancel: __('Cancel'),
newList: __('New list'),
- noResults: __('No matching results'),
scope: __('Scope'),
scopeDescription: __('Issues must match this scope to appear in this list.'),
- selected: __('Selected'),
requiredFieldFeedback: __('This field is required.'),
},
components: {
GlButton,
- GlDropdown,
GlFormGroup,
- GlIcon,
- GlSearchBoxByType,
- GlSkeletonLoader,
},
props: {
- loading: {
- type: Boolean,
- required: true,
- },
searchLabel: {
type: String,
required: false,
default: null,
},
- noneSelected: {
- type: String,
- required: true,
- },
- searchPlaceholder: {
- type: String,
+ selectedIdValid: {
+ type: Boolean,
required: true,
},
- selectedId: {
- type: [Number, String],
- required: false,
- default: null,
- },
},
data() {
return {
searchValue: '',
- selectedIdValid: true,
};
},
- computed: {
- toggleClassList() {
- return `gl-max-w-full gl-display-flex gl-align-items-center gl-text-trunate ${
- this.selectedIdValid ? '' : 'gl-inset-border-1-red-400!'
- }`;
- },
- },
- watch: {
- selectedId(val) {
- if (val) {
- this.$refs.dropdown.hide(true);
- this.selectedIdValid = true;
- }
- },
- },
methods: {
...mapActions(['setAddColumnFormVisibility']),
- setFocus() {
- this.$refs.searchBox.focusInput();
- },
- onHide() {
- this.searchValue = '';
- this.$emit('filter-items', '');
- this.$emit('hide');
- },
onSubmit() {
- if (!this.selectedId) {
- this.selectedIdValid = false;
- } else {
- this.$emit('add-list');
- }
+ this.$emit('add-list');
},
},
};
@@ -126,44 +72,7 @@ export default {
:state="selectedIdValid"
:invalid-feedback="$options.i18n.requiredFieldFeedback"
>
- <gl-dropdown
- ref="dropdown"
- class="gl-mb-3 gl-max-w-full"
- :toggle-class="toggleClassList"
- boundary="viewport"
- @shown="setFocus"
- @hide="onHide"
- >
- <template #button-content>
- <slot name="selected">
- <div>{{ noneSelected }}</div>
- </slot>
- <gl-icon class="dropdown-chevron gl-flex-shrink-0" name="chevron-down" />
- </template>
-
- <template #header>
- <gl-search-box-by-type
- ref="searchBox"
- v-model="searchValue"
- debounce="250"
- class="gl-mt-0!"
- :placeholder="searchPlaceholder"
- @input="$emit('filter-items', $event)"
- />
- </template>
-
- <div v-if="loading" class="gl-px-5">
- <gl-skeleton-loader :width="400" :height="172">
- <rect width="380" height="20" x="10" y="15" rx="4" />
- <rect width="280" height="20" x="10" y="50" rx="4" />
- <rect width="330" height="20" x="10" y="85" rx="4" />
- </gl-skeleton-loader>
- </div>
-
- <slot v-else name="items">
- <p class="gl-mx-5">{{ $options.i18n.noResults }}</p>
- </slot>
- </gl-dropdown>
+ <slot name="dropdown"></slot>
</gl-form-group>
</div>
<div class="gl-display-flex gl-mb-4">
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index d41fc1e9300..3a247819850 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -1,24 +1,106 @@
<script>
import { mapGetters } from 'vuex';
-import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import { refreshCurrentPage, queryToObject } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
import BoardContent from '~/boards/components/board_content.vue';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
+import { listsQuery } from 'ee_else_ce/boards/constants';
+import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
export default {
+ i18n: {
+ fetchError: s__(
+ 'Boards|An error occurred while fetching the board lists. Please reload the page.',
+ ),
+ },
components: {
BoardContent,
BoardSettingsSidebar,
BoardTopBar,
},
- inject: ['initialBoardId'],
+ inject: [
+ 'fullPath',
+ 'initialBoardId',
+ 'initialFilterParams',
+ 'isIssueBoard',
+ 'isGroupBoard',
+ 'issuableType',
+ 'boardType',
+ 'isApolloBoard',
+ ],
data() {
return {
+ activeListId: '',
boardId: this.initialBoardId,
+ filterParams: { ...this.initialFilterParams },
+ isShowingEpicsSwimlanes: Boolean(queryToObject(window.location.search).group_by),
+ apolloError: null,
};
},
+ apollo: {
+ activeBoardItem: {
+ query: activeBoardItemQuery,
+ variables() {
+ return {
+ isIssue: this.isIssueBoard,
+ };
+ },
+ result({ data: { activeBoardItem } }) {
+ if (activeBoardItem) {
+ this.setActiveId('');
+ }
+ },
+ skip() {
+ return !this.isApolloBoard;
+ },
+ },
+ boardListsApollo: {
+ query() {
+ return listsQuery[this.issuableType].query;
+ },
+ variables() {
+ return this.listQueryVariables;
+ },
+ skip() {
+ return !this.isApolloBoard;
+ },
+ update(data) {
+ const { lists } = data[this.boardType].board;
+ return formatBoardLists(lists);
+ },
+ error() {
+ this.apolloError = this.$options.i18n.fetchError;
+ },
+ },
+ },
+
computed: {
...mapGetters(['isSidebarOpen']),
+ listQueryVariables() {
+ return {
+ ...(this.isIssueBoard && {
+ isGroup: this.isGroupBoard,
+ isProject: !this.isGroupBoard,
+ }),
+ fullPath: this.fullPath,
+ boardId: this.boardId,
+ filters: this.filterParams,
+ };
+ },
+ isSwimlanesOn() {
+ return (gon?.licensed_features?.swimlanes && this.isShowingEpicsSwimlanes) ?? false;
+ },
+ isAnySidebarOpen() {
+ if (this.isApolloBoard) {
+ return this.activeBoardItem?.id || this.activeListId;
+ }
+ return this.isSidebarOpen;
+ },
+ activeList() {
+ return this.activeListId ? this.boardListsApollo[this.activeListId] : undefined;
+ },
},
created() {
window.addEventListener('popstate', refreshCurrentPage);
@@ -27,17 +109,47 @@ export default {
window.removeEventListener('popstate', refreshCurrentPage);
},
methods: {
+ setActiveId(id) {
+ this.activeListId = id;
+ },
switchBoard(id) {
this.boardId = id;
+ this.setActiveId('');
+ },
+ setFilters(filters) {
+ const filterParams = { ...filters };
+ if (filterParams.groupBy) delete filterParams.groupBy;
+ this.filterParams = filterParams;
},
},
};
</script>
<template>
- <div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }">
- <board-top-bar :board-id="boardId" @switchBoard="switchBoard" />
- <board-content :board-id="boardId" />
- <board-settings-sidebar />
+ <div class="boards-app gl-relative" :class="{ 'is-compact': isAnySidebarOpen }">
+ <board-top-bar
+ :board-id="boardId"
+ :is-swimlanes-on="isSwimlanesOn"
+ @switchBoard="switchBoard"
+ @setFilters="setFilters"
+ @toggleSwimlanes="isShowingEpicsSwimlanes = $event"
+ />
+ <board-content
+ v-if="!isApolloBoard || boardListsApollo"
+ :board-id="boardId"
+ :is-swimlanes-on="isSwimlanesOn"
+ :filter-params="filterParams"
+ :board-lists-apollo="boardListsApollo"
+ :apollo-error="apolloError"
+ @setActiveList="setActiveId"
+ />
+ <board-settings-sidebar
+ v-if="!isApolloBoard || activeList"
+ :list="activeList"
+ :list-id="activeListId"
+ :board-id="boardId"
+ :query-variables="listQueryVariables"
+ @unsetActiveId="setActiveId('')"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 3071c1f334e..18495f285da 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,6 +1,8 @@
<script>
import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
+import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import BoardCardInner from './board_card_inner.vue';
export default {
@@ -9,7 +11,7 @@ export default {
BoardCardInner,
},
mixins: [Tracking.mixin()],
- inject: ['disabled', 'isApolloBoard'],
+ inject: ['disabled', 'isIssueBoard', 'isApolloBoard'],
props: {
list: {
type: Object,
@@ -37,14 +39,30 @@ export default {
default: true,
},
},
+ apollo: {
+ activeBoardItem: {
+ query: activeBoardItemQuery,
+ variables() {
+ return {
+ isIssue: this.isIssueBoard,
+ };
+ },
+ skip() {
+ return !this.isApolloBoard;
+ },
+ },
+ },
computed: {
...mapState(['selectedBoardItems', 'activeId']),
+ activeItemId() {
+ return this.isApolloBoard ? this.activeBoardItem?.id : this.activeId;
+ },
isActive() {
- return this.item.id === this.activeId;
+ return this.item.id === this.activeItemId;
},
multiSelectVisible() {
return (
- !this.activeId &&
+ !this.activeItemId &&
this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1
);
},
@@ -83,10 +101,23 @@ export default {
if (isMultiSelect && gon?.features?.boardMultiSelect) {
this.toggleBoardItemMultiSelection(this.item);
} else {
- this.toggleBoardItem({ boardItem: this.item });
+ if (this.isApolloBoard) {
+ this.toggleItem();
+ } else {
+ this.toggleBoardItem({ boardItem: this.item });
+ }
this.track('click_card', { label: 'right_sidebar' });
}
},
+ toggleItem() {
+ this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: {
+ boardItem: this.item,
+ isIssue: this.isIssueBoard,
+ },
+ });
+ },
},
};
</script>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 88f51c71e06..befd04c29ae 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -275,7 +275,7 @@ export default {
<gl-loading-icon v-if="item.isLoading" size="lg" class="gl-mt-5" />
<span
v-if="item.referencePath"
- class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3 gl-text-secondary"
+ class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3 gl-font-sm gl-text-secondary"
:class="{ 'gl-font-base': isEpicBoard }"
>
<work-item-type-icon
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 708e1539c6e..b2054d76e95 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -20,6 +20,10 @@ export default {
type: String,
required: true,
},
+ filters: {
+ type: Object,
+ required: true,
+ },
},
computed: {
...mapState(['filterParams', 'highlightedLists']),
@@ -33,11 +37,14 @@ export default {
isListDraggable() {
return isListDraggable(this.list);
},
+ filtersToUse() {
+ return this.isApolloBoard ? this.filters : this.filterParams;
+ },
},
watch: {
filterParams: {
handler() {
- if (this.list.id && !this.list.collapsed) {
+ if (!this.isApolloBoard && this.list.id && !this.list.collapsed) {
this.fetchItemsForList({ listId: this.list.id });
}
},
@@ -46,7 +53,7 @@ export default {
},
'list.id': {
handler(id) {
- if (id) {
+ if (!this.isApolloBoard && id) {
this.fetchItemsForList({ listId: this.list.id });
}
},
@@ -83,13 +90,18 @@ export default {
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-gray-50"
:class="{ 'board-column-highlighted': highlighted }"
>
- <board-list-header :list="list" />
+ <board-list-header
+ :list="list"
+ :filter-params="filtersToUse"
+ :board-id="boardId"
+ @setActiveList="$emit('setActiveList', $event)"
+ />
<board-list
ref="board-list"
:board-id="boardId"
:board-items="listItems"
:list="list"
- :filter-params="filterParams"
+ :filter-params="filtersToUse"
/>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 8a37719eae8..8304dfef527 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,23 +1,15 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import { breakpoints } from '@gitlab/ui/dist/utils';
-import { sortBy, throttle } from 'lodash';
+import { sortBy } from 'lodash';
import Draggable from 'vuedraggable';
-import { mapState, mapGetters, mapActions } from 'vuex';
-import { contentTop } from '~/lib/utils/common_utils';
-import { s__ } from '~/locale';
-import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
+import { mapState, mapActions } from 'vuex';
+import eventHub from '~/boards/eventhub';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { defaultSortableOptions } from '~/sortable/constants';
-import { DraggableItemTypes, listsQuery } from 'ee_else_ce/boards/constants';
+import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
import BoardColumn from './board_column.vue';
export default {
- i18n: {
- fetchError: s__(
- 'Boards|An error occurred while fetching the board lists. Please reload the page.',
- ),
- },
draggableItemTypes: DraggableItemTypes,
components: {
BoardAddNewColumn,
@@ -28,73 +20,41 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
- inject: [
- 'canAdminList',
- 'boardType',
- 'fullPath',
- 'issuableType',
- 'isIssueBoard',
- 'isEpicBoard',
- 'isGroupBoard',
- 'disabled',
- 'isApolloBoard',
- ],
+ inject: ['canAdminList', 'isIssueBoard', 'isEpicBoard', 'disabled', 'isApolloBoard'],
props: {
boardId: {
type: String,
required: true,
},
+ filterParams: {
+ type: Object,
+ required: true,
+ },
+ isSwimlanesOn: {
+ type: Boolean,
+ required: true,
+ },
+ boardListsApollo: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ apolloError: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
boardHeight: null,
- boardListsApollo: {},
- apolloError: null,
- updatedBoardId: this.boardId,
};
},
- apollo: {
- boardListsApollo: {
- query() {
- return listsQuery[this.issuableType].query;
- },
- variables() {
- return this.queryVariables;
- },
- skip() {
- return !this.isApolloBoard;
- },
- update(data) {
- const { lists } = data[this.boardType].board;
- return formatBoardLists(lists);
- },
- result() {
- // this allows us to delay fetching lists when we switch a board to fetch the actual board lists
- // instead of fetching lists for the "previous" board
- this.updatedBoardId = this.boardId;
- },
- error() {
- this.apolloError = this.$options.i18n.fetchError;
- },
- },
- },
computed: {
...mapState(['boardLists', 'error', 'addColumnForm']),
- ...mapGetters(['isSwimlanesOn']),
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
- queryVariables() {
- return {
- ...(this.isIssueBoard && {
- isGroup: this.isGroupBoard,
- isProject: !this.isGroupBoard,
- }),
- fullPath: this.fullPath,
- boardId: this.boardId,
- filterParams: this.filterParams,
- };
- },
boardListsToUse() {
const lists = this.isApolloBoard ? this.boardListsApollo : this.boardLists;
return sortBy([...Object.values(lists)], 'position');
@@ -126,18 +86,11 @@ export default {
return this.isApolloBoard ? this.apolloError : this.error;
},
},
- mounted() {
- this.setBoardHeight();
-
- this.resizeObserver = new ResizeObserver(
- throttle(() => {
- this.setBoardHeight();
- }, 150),
- );
- this.resizeObserver.observe(document.body);
+ created() {
+ eventHub.$on('updateBoard', this.refetchLists);
},
- unmounted() {
- this.resizeObserver.disconnect();
+ beforeDestroy() {
+ eventHub.$off('updateBoard', this.refetchLists);
},
methods: {
...mapActions(['moveList', 'unsetError']),
@@ -145,19 +98,19 @@ export default {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
},
- setBoardHeight() {
- if (window.innerWidth < breakpoints.md) {
- this.boardHeight = `${window.innerHeight - contentTop()}px`;
- } else {
- this.boardHeight = `${window.innerHeight - this.$el.getBoundingClientRect().top}px`;
- }
+ refetchLists() {
+ this.$apollo.queries.boardListsApollo.refetch();
},
},
};
</script>
<template>
- <div v-cloak data-qa-selector="boards_list">
+ <div
+ v-cloak
+ data-qa-selector="boards_list"
+ class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-min-h-0"
+ >
<gl-alert v-if="errorToDisplay" variant="danger" :dismissible="true" @dismiss="unsetError">
{{ errorToDisplay }}
</gl-alert>
@@ -166,8 +119,7 @@ export default {
v-if="!isSwimlanesOn"
ref="list"
v-bind="draggableOptions"
- class="boards-list gl-w-full gl-py-5 gl-pr-3 gl-white-space-nowrap gl-overflow-x-scroll"
- :style="{ height: boardHeight }"
+ class="boards-list gl-w-full gl-py-5 gl-pr-3 gl-white-space-nowrap gl-overflow-x-auto"
@end="moveList"
>
<board-column
@@ -176,8 +128,10 @@ export default {
ref="board"
:board-id="boardId"
:list="list"
+ :filters="filterParams"
:data-draggable-item-type="$options.draggableItemTypes.list"
:class="{ 'gl-xs-display-none!': addColumnFormVisible }"
+ @setActiveList="$emit('setActiveList', $event)"
/>
<transition name="slide" @after-enter="afterFormEnters">
@@ -188,9 +142,11 @@ export default {
<epics-swimlanes
v-else-if="boardListsToUse.length"
ref="swimlanes"
+ :board-id="boardId"
:lists="boardListsToUse"
:can-admin-list="canAdminList"
- :style="{ height: boardHeight }"
+ :filters="filterParams"
+ @setActiveList="$emit('setActiveList', $event)"
/>
<board-content-sidebar v-if="isIssueBoard" data-testid="issue-boards-sidebar" />
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 6227f185eda..1b97214ff8b 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -3,12 +3,14 @@ import { GlDrawer } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
+import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import { __, sprintf } from '~/locale';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
-import { BoardType, ISSUABLE, INCIDENT } from '~/boards/constants';
+import { INCIDENT } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
@@ -16,8 +18,6 @@ import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severit
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
-import { LabelType } from '~/sidebar/components/labels/labels_select_widget/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -40,7 +40,6 @@ export default {
SidebarWeightWidget: () =>
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
},
- mixins: [glFeatureFlagMixin()],
inject: {
multipleAssigneesFeatureAvailable: {
default: false,
@@ -72,33 +71,48 @@ export default {
isGroupBoard: {
default: false,
},
+ isApolloBoard: {
+ default: false,
+ },
},
inheritAttrs: false,
+ apollo: {
+ activeBoardCard: {
+ query: activeBoardItemQuery,
+ variables: {
+ isIssue: true,
+ },
+ update(data) {
+ if (!data.activeBoardItem?.id) {
+ return { id: '', iid: '' };
+ }
+ return {
+ ...data.activeBoardItem,
+ assignees: data.activeBoardItem.assignees?.nodes || [],
+ };
+ },
+ skip() {
+ return !this.isApolloBoard;
+ },
+ },
+ },
computed: {
- ...mapGetters([
- 'isSidebarOpen',
- 'activeBoardItem',
- 'groupPathForActiveIssue',
- 'projectPathForActiveIssue',
- ]),
+ ...mapGetters(['activeBoardItem']),
...mapState(['sidebarType']),
- isIssuableSidebar() {
- return this.sidebarType === ISSUABLE;
+ activeBoardIssuable() {
+ return this.isApolloBoard ? this.activeBoardCard : this.activeBoardItem;
},
- isIncidentSidebar() {
- return this.activeBoardItem.type === INCIDENT;
+ isSidebarOpen() {
+ return Boolean(this.activeBoardIssuable?.id);
},
- showSidebar() {
- return this.isIssuableSidebar && this.isSidebarOpen;
+ isIncidentSidebar() {
+ return this.activeBoardIssuable?.type === INCIDENT;
},
sidebarTitle() {
return this.isIncidentSidebar ? __('Incident details') : __('Issue details');
},
- fullPath() {
- return this.activeBoardItem?.referencePath?.split('#')[0] || '';
- },
parentType() {
- return this.isGroupBoard ? BoardType.group : BoardType.project;
+ return this.isGroupBoard ? WORKSPACE_GROUP : WORKSPACE_PROJECT;
},
createLabelTitle() {
return sprintf(__('Create %{workspace} label'), {
@@ -114,13 +128,21 @@ export default {
return this.isGroupBoard ? this.groupPathForActiveIssue : this.projectPathForActiveIssue;
},
labelType() {
- return this.isGroupBoard ? LabelType.group : LabelType.project;
+ return this.isGroupBoard ? WORKSPACE_GROUP : WORKSPACE_PROJECT;
},
labelsFilterPath() {
return this.isGroupBoard
? this.labelsFilterBasePath.replace(':project_path', this.projectPathForActiveIssue)
: this.labelsFilterBasePath;
},
+ groupPathForActiveIssue() {
+ const { referencePath = '' } = this.activeBoardIssuable;
+ return referencePath.slice(0, referencePath.lastIndexOf('/'));
+ },
+ projectPathForActiveIssue() {
+ const { referencePath = '' } = this.activeBoardIssuable;
+ return referencePath.slice(0, referencePath.indexOf('#'));
+ },
},
methods: {
...mapActions([
@@ -132,7 +154,19 @@ export default {
'setActiveItemHealthStatus',
]),
handleClose() {
- this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
+ if (this.isApolloBoard) {
+ this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: {
+ boardItem: null,
+ },
+ });
+ } else {
+ this.toggleBoardItem({
+ boardItem: this.activeBoardIssuable,
+ sidebarType: this.sidebarType,
+ });
+ }
},
handleUpdateSelectedLabels({ labels, id }) {
this.setActiveBoardItemLabels({
@@ -144,7 +178,7 @@ export default {
},
handleLabelRemove(removeLabelId) {
this.setActiveBoardItemLabels({
- iid: this.activeBoardItem.iid,
+ iid: this.activeBoardIssuable.iid,
projectPath: this.projectPathForActiveIssue,
removeLabelIds: [removeLabelId],
});
@@ -157,7 +191,7 @@ export default {
<mounting-portal mount-to="#js-right-sidebar-portal" name="board-content-sidebar" append>
<gl-drawer
v-bind="$attrs"
- :open="showSidebar"
+ :open="isSidebarOpen"
class="boards-sidebar"
variant="sidebar"
@close="handleClose"
@@ -168,25 +202,27 @@ export default {
<template #header>
<sidebar-todo-widget
class="gl-mt-3"
- :issuable-id="activeBoardItem.id"
- :issuable-iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :issuable-id="activeBoardIssuable.id"
+ :issuable-iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
/>
</template>
<template #default>
- <board-sidebar-title data-testid="sidebar-title" />
+ <board-sidebar-title :active-item="activeBoardIssuable" data-testid="sidebar-title" />
<sidebar-assignees-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :initial-assignees="activeBoardItem.assignees"
+ v-if="activeBoardItem.assignees"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
+ :initial-assignees="activeBoardIssuable.assignees"
:allow-multiple-assignees="multipleAssigneesFeatureAvailable"
:editable="canUpdate"
- @assignees-updated="setAssignees"
+ @assignees-updated="!isApolloBoard && setAssignees($event)"
/>
<sidebar-dropdown-widget
v-if="epicFeatureAvailable && !isIncidentSidebar"
- :iid="activeBoardItem.iid"
+ :key="`epic-${activeBoardItem.iid}`"
+ :iid="activeBoardIssuable.iid"
issuable-attribute="epic"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
@@ -195,7 +231,8 @@ export default {
/>
<div>
<sidebar-dropdown-widget
- :iid="activeBoardItem.iid"
+ :key="`milestone-${activeBoardItem.iid}`"
+ :iid="activeBoardIssuable.iid"
issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="projectPathForActiveIssue"
@@ -204,7 +241,8 @@ export default {
/>
<sidebar-iteration-widget
v-if="iterationFeatureAvailable && !isIncidentSidebar"
- :iid="activeBoardItem.iid"
+ :key="`iteration-${activeBoardItem.iid}`"
+ :iid="activeBoardIssuable.iid"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
:issuable-type="issuableType"
@@ -214,14 +252,14 @@ export default {
</div>
<board-sidebar-time-tracker />
<sidebar-date-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
data-testid="sidebar-due-date"
/>
<sidebar-labels-widget
class="block labels"
- :iid="activeBoardItem.iid"
+ :iid="activeBoardIssuable.iid"
:full-path="projectPathForActiveIssue"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
@@ -233,40 +271,40 @@ export default {
workspace-type="project"
:issuable-type="issuableType"
:label-create-type="labelType"
- @onLabelRemove="handleLabelRemove"
- @updateSelectedLabels="handleUpdateSelectedLabels"
+ @onLabelRemove="!isApolloBoard && handleLabelRemove($event)"
+ @updateSelectedLabels="!isApolloBoard && handleUpdateSelectedLabels($event)"
>
{{ __('None') }}
</sidebar-labels-widget>
<sidebar-severity-widget
v-if="isIncidentSidebar"
- :iid="activeBoardItem.iid"
- :project-path="fullPath"
- :initial-severity="activeBoardItem.severity"
+ :iid="activeBoardIssuable.iid"
+ :project-path="projectPathForActiveIssue"
+ :initial-severity="activeBoardIssuable.severity"
/>
<sidebar-weight-widget
v-if="weightFeatureAvailable && !isIncidentSidebar"
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @weightUpdated="setActiveItemWeight($event)"
+ @weightUpdated="!isApolloBoard && setActiveItemWeight($event)"
/>
<sidebar-health-status-widget
v-if="healthStatusFeatureAvailable"
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @statusUpdated="setActiveItemHealthStatus($event)"
+ @statusUpdated="!isApolloBoard && setActiveItemHealthStatus($event)"
/>
<sidebar-confidentiality-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @confidentialityUpdated="setActiveItemConfidential($event)"
+ @confidentialityUpdated="!isApolloBoard && setActiveItemConfidential($event)"
/>
<sidebar-subscriptions-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
+ :iid="activeBoardIssuable.iid"
+ :full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
data-testid="sidebar-notifications"
/>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 1bc5d910561..b5d3613ca27 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -1,7 +1,7 @@
<script>
import { pickBy, isEmpty, mapValues } from 'lodash';
import { mapActions } from 'vuex';
-import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
+import { getIdFromGraphQLId, isGid, convertToGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -22,7 +22,8 @@ import {
TOKEN_TYPE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { AssigneeFilterType } from '~/boards/constants';
+import { AssigneeFilterType, GroupByParamType } from 'ee_else_ce/boards/constants';
+import { TYPENAME_ITERATION } from '~/graphql_shared/constants';
import eventHub from '../eventhub';
export default {
@@ -30,8 +31,13 @@ export default {
search: __('Search'),
},
components: { FilteredSearch },
- inject: ['initialFilterParams'],
+ inject: ['initialFilterParams', 'isApolloBoard'],
props: {
+ isSwimlanesOn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
tokens: {
type: Array,
required: true,
@@ -320,6 +326,7 @@ export default {
release_tag: releaseTag,
confidential,
health_status: healthStatus,
+ group_by: this.isSwimlanesOn ? GroupByParamType.epic : undefined,
},
(value) => {
if (value || value === false) {
@@ -334,11 +341,23 @@ export default {
},
);
},
+ formattedFilterParams() {
+ const filtersCopy = { ...this.filterParams };
+ if (this.filterParams?.iterationId) {
+ filtersCopy.iterationId = convertToGraphQLId(
+ TYPENAME_ITERATION,
+ this.filterParams.iterationId,
+ );
+ }
+
+ return filtersCopy;
+ },
},
created() {
eventHub.$on('updateTokens', this.updateTokens);
if (!isEmpty(this.eeFilters)) {
this.filterParams = this.eeFilters;
+ this.$emit('setFilters', this.formattedFilterParams);
}
},
beforeDestroy() {
@@ -349,6 +368,7 @@ export default {
updateTokens() {
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
this.filterParams = convertObjectPropsToCamelCase(rawFilterParams, {});
+ this.$emit('setFilters', this.formattedFilterParams);
this.filteredSearchKey += 1;
},
handleFilter(filters) {
@@ -360,7 +380,11 @@ export default {
replace: true,
});
- this.performSearch();
+ if (this.isApolloBoard) {
+ this.$emit('setFilters', this.formattedFilterParams);
+ } else {
+ this.performSearch();
+ }
},
getFilterParams(filters = []) {
const notFilters = filters.filter((item) => item.value.operator === '!=');
@@ -373,7 +397,6 @@ export default {
generateParams(filters = []) {
const filterParams = {};
const labels = [];
- const plainText = [];
filters.forEach((filter) => {
switch (filter.type) {
@@ -415,7 +438,9 @@ export default {
filterParams.confidential = filter.value.data;
break;
case FILTERED_SEARCH_TERM:
- if (filter.value.data) plainText.push(filter.value.data);
+ if (filter.value.data) {
+ filterParams.search = filter.value.data;
+ }
break;
case TOKEN_TYPE_HEALTH:
filterParams.healthStatus = filter.value.data;
@@ -429,10 +454,6 @@ export default {
filterParams.labelName = labels;
}
- if (plainText.length) {
- filterParams.search = plainText.join(' ');
- }
-
return filterParams;
},
},
@@ -444,6 +465,7 @@ export default {
:key="filteredSearchKey"
class="gl-w-full"
namespace=""
+ terms-as-tokens
:tokens="tokens"
:search-input-placeholder="$options.i18n.search"
:initial-filter-value="getFilteredSearchValue"
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index a71bde54a8f..604e71f5993 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
+import eventHub from '~/boards/eventhub';
import { formType } from '../constants';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
@@ -57,6 +58,9 @@ export default {
isProjectBoard: {
default: false,
},
+ isApolloBoard: {
+ default: false,
+ },
},
props: {
canAdminBoard: {
@@ -124,14 +128,12 @@ export default {
primaryProps() {
return {
text: this.buttonText,
- attributes: [
- {
- variant: this.buttonKind,
- disabled: this.submitDisabled,
- loading: this.isLoading,
- 'data-qa-selector': 'save_changes_button',
- },
- ],
+ attributes: {
+ variant: this.buttonKind,
+ disabled: this.submitDisabled,
+ loading: this.isLoading,
+ 'data-qa-selector': 'save_changes_button',
+ },
};
},
cancelProps() {
@@ -213,7 +215,15 @@ export default {
} else {
try {
const board = await this.createOrUpdateBoard();
- this.setBoard(board);
+ if (this.isApolloBoard) {
+ if (this.board.id) {
+ eventHub.$emit('updateBoard', board);
+ } else {
+ this.$emit('addBoard', board);
+ }
+ } else {
+ this.setBoard(board);
+ }
this.cancel();
const param = getParameterByName('group_by')
@@ -278,7 +288,7 @@ export default {
@hide.prevent
>
<gl-alert
- v-if="error"
+ v-if="!isApolloBoard && error"
class="gl-mb-3"
variant="danger"
:dismissible="true"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 6f2b35f5191..5f082066ad4 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -2,6 +2,7 @@
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { mapActions, mapState } from 'vuex';
+import { STATUS_CLOSED } from '~/issues/constants';
import { sprintf, __ } from '~/locale';
import { defaultSortableOptions } from '~/sortable/constants';
import { sortableStart, sortableEnd } from '~/sortable/utils';
@@ -59,6 +60,10 @@ export default {
type: Array,
required: true,
},
+ filterParams: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -108,7 +113,7 @@ export default {
},
},
computed: {
- ...mapState(['pageInfoByListId', 'listsFlags', 'filterParams', 'isUpdateIssueOrderInProgress']),
+ ...mapState(['pageInfoByListId', 'listsFlags', 'isUpdateIssueOrderInProgress']),
boardListItems() {
return this.isApolloBoard
? this.currentList?.[`${this.issuableType}s`].nodes || []
@@ -125,7 +130,7 @@ export default {
};
},
listItemsCount() {
- return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
+ return this.isEpicBoard ? this.list.metadata.epicsCount : this.boardList?.issuesCount;
},
paginatedIssueText() {
return sprintf(__('Showing %{pageSize} of %{total} %{issuableType}'), {
@@ -154,10 +159,10 @@ export default {
return this.isApolloBoard ? this.isLoadingMore : this.listsFlags[this.list.id]?.isLoadingMore;
},
epicCreateFormVisible() {
- return this.isEpicBoard && this.list.listType !== 'closed' && this.showEpicForm;
+ return this.isEpicBoard && this.list.listType !== STATUS_CLOSED && this.showEpicForm;
},
issueCreateFormVisible() {
- return !this.isEpicBoard && this.list.listType !== 'closed' && this.showIssueForm;
+ return !this.isEpicBoard && this.list.listType !== STATUS_CLOSED && this.showIssueForm;
},
listRef() {
// When list is draggable, the reference to the list needs to be accessed differently
@@ -260,6 +265,10 @@ export default {
this.showIssueForm = !this.showIssueForm;
}
},
+ isObservableItem(index) {
+ // observe every 6 item of 10 to achieve smooth loading state
+ return index !== 0 && index % 6 === 0;
+ },
onReachingListBottom() {
if (!this.loadingMore && this.hasNextPage) {
this.showCount = true;
@@ -393,8 +402,14 @@ export default {
:list="list"
:list-items-length="boardListItems.length"
/>
+ <gl-intersection-observer
+ v-if="isObservableItem(index)"
+ data-testid="board-card-gl-io"
+ @appear="onReachingListBottom"
+ />
</board-card>
- <gl-intersection-observer @appear="onReachingListBottom">
+ <div>
+ <!-- for supporting previous structure with intersection observer -->
<li
v-if="showCount"
class="board-list-count gl-text-center gl-text-secondary gl-py-4"
@@ -404,12 +419,11 @@ export default {
v-if="loadingMore"
size="sm"
:label="$options.i18n.loadingMoreboardItems"
- data-testid="count-loading-icon"
/>
<span v-if="showingAllItems">{{ showingAllItemsText }}</span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
- </gl-intersection-observer>
+ </div>
</component>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 749fae0c426..1b711feb686 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -1,38 +1,48 @@
<script>
import {
GlButton,
- GlButtonGroup,
GlLabel,
GlTooltip,
GlIcon,
GlSprintf,
GlTooltipDirective,
+ GlDisclosureDropdown,
} from '@gitlab/ui';
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import { isListDraggable } from '~/boards/boards_util';
import { isScopedLabel, parseBoolean } from '~/lib/utils/common_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { n__, s__, __ } from '~/locale';
+import { n__, s__ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
+import { TYPE_ISSUE } from '~/issues/constants';
import { formatDate } from '~/lib/utils/datetime_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
+import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import AccessorUtilities from '~/lib/utils/accessor';
-import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
+import {
+ inactiveId,
+ LIST,
+ ListType,
+ toggleFormEventPrefix,
+ updateListQueries,
+ toggleCollapsedMutations,
+} from 'ee_else_ce/boards/constants';
import eventHub from '../eventhub';
import ItemCount from './item_count.vue';
export default {
i18n: {
- newIssue: __('New issue'),
- newEpic: s__('Boards|New epic'),
- listSettings: __('List settings'),
+ newIssue: s__('Boards|Create new issue'),
+ listActions: s__('Boards|List actions'),
+ newEpic: s__('Boards|Create new epic'),
+ listSettings: s__('Boards|Edit list settings'),
expand: s__('Boards|Expand'),
collapse: s__('Boards|Collapse'),
},
components: {
- GlButtonGroup,
+ GlDisclosureDropdown,
GlButton,
GlLabel,
GlTooltip,
@@ -63,6 +73,12 @@ export default {
disabled: {
default: true,
},
+ issuableType: {
+ default: TYPE_ISSUE,
+ },
+ isApolloBoard: {
+ default: false,
+ },
},
props: {
list: {
@@ -75,16 +91,26 @@ export default {
required: false,
default: false,
},
+ filterParams: {
+ type: Object,
+ required: true,
+ },
+ boardId: {
+ type: String,
+ required: true,
+ },
},
computed: {
- ...mapState(['activeId', 'filterParams', 'boardId']),
- ...mapGetters(['isSwimlanesOn']),
+ ...mapState(['activeId']),
isLoggedIn() {
return Boolean(this.currentUserId);
},
listType() {
return this.list.listType;
},
+ itemsCount() {
+ return this.isEpicBoard ? this.list.metadata.epicsCount : this.boardList?.issuesCount;
+ },
listAssignee() {
return this.list?.assignee?.username || '';
},
@@ -111,7 +137,10 @@ export default {
},
showListHeaderActions() {
if (this.isLoggedIn) {
- return this.isNewIssueShown || this.isNewEpicShown || this.isSettingsShown;
+ return (
+ (this.isNewIssueShown || this.isNewEpicShown || this.isSettingsShown) &&
+ !this.list.collapsed
+ );
}
return false;
},
@@ -162,6 +191,50 @@ export default {
canShowTotalWeight() {
return this.weightFeatureAvailable && !this.isLoading;
},
+ actionListItems() {
+ const items = [];
+
+ if (this.isNewIssueShown) {
+ const newIssueText = this.$options.i18n.newIssue;
+ items.push({
+ text: newIssueText,
+ action: this.showNewIssueForm,
+ extraAttrs: {
+ 'data-testid': 'newIssueBtn',
+ title: newIssueText,
+ 'aria-label': newIssueText,
+ },
+ });
+ }
+
+ if (this.isNewEpicShown) {
+ const newEpicText = this.$options.i18n.newEpic;
+ items.push({
+ text: newEpicText,
+ action: this.showNewEpicForm,
+ extraAttrs: {
+ 'data-testid': 'newEpicBtn',
+ title: newEpicText,
+ 'aria-label': newEpicText,
+ },
+ });
+ }
+
+ if (this.isSettingsShown) {
+ const listSettingsText = this.$options.i18n.listSettings;
+ items.push({
+ text: listSettingsText,
+ action: this.openSidebarSettings,
+ extraAttrs: {
+ 'data-testid': 'settingsBtn',
+ title: listSettingsText,
+ 'aria-label': listSettingsText,
+ },
+ });
+ }
+
+ return items;
+ },
},
apollo: {
boardList: {
@@ -175,34 +248,43 @@ export default {
context: {
isSingleRequest: true,
},
- skip() {
- return this.isEpicBoard;
- },
},
},
created() {
const localCollapsed = parseBoolean(localStorage.getItem(`${this.uniqueKey}.collapsed`));
if ((!this.isLoggedIn || this.isEpicBoard) && localCollapsed) {
- this.toggleListCollapsed({ listId: this.list.id, collapsed: true });
+ this.updateLocalCollapsedStatus(true);
}
},
methods: {
...mapActions(['updateList', 'setActiveId', 'toggleListCollapsed']),
+ closeListActions() {
+ this.$refs.headerListActions?.close();
+ },
openSidebarSettings() {
if (this.activeId === inactiveId) {
sidebarEventHub.$emit('sidebar.closeAll');
}
- this.setActiveId({ id: this.list.id, sidebarType: LIST });
+ if (this.isApolloBoard) {
+ this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: { boardItem: null },
+ });
+ this.$emit('setActiveList', this.list.id);
+ } else {
+ this.setActiveId({ id: this.list.id, sidebarType: LIST });
+ }
this.track('click_button', { label: 'list_settings' });
+
+ this.closeListActions();
},
showScopedLabels(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
},
-
showNewIssueForm() {
- if (this.isSwimlanesOn) {
+ if (this.isSwimlanesHeader) {
eventHub.$emit('open-unassigned-lane');
this.$nextTick(() => {
eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
@@ -210,18 +292,22 @@ export default {
} else {
eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
}
+
+ this.closeListActions();
},
showNewEpicForm() {
eventHub.$emit(`${toggleFormEventPrefix.epic}${this.list.id}`);
+
+ this.closeListActions();
},
toggleExpanded() {
const collapsed = !this.list.collapsed;
- this.toggleListCollapsed({ listId: this.list.id, collapsed });
+ this.updateLocalCollapsedStatus(collapsed);
if (!this.isLoggedIn) {
- this.addToLocalStorage();
+ this.addToLocalStorage(collapsed);
} else {
- this.updateListFunction();
+ this.updateListFunction(collapsed);
}
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
@@ -233,13 +319,37 @@ export default {
property: collapsed ? 'closed' : 'open',
});
},
- addToLocalStorage() {
+ addToLocalStorage(collapsed) {
if (AccessorUtilities.canUseLocalStorage()) {
- localStorage.setItem(`${this.uniqueKey}.collapsed`, this.list.collapsed);
+ localStorage.setItem(`${this.uniqueKey}.collapsed`, collapsed);
}
},
- updateListFunction() {
- this.updateList({ listId: this.list.id, collapsed: this.list.collapsed });
+ async updateListFunction(collapsed) {
+ if (this.isApolloBoard) {
+ try {
+ await this.$apollo.mutate({
+ mutation: updateListQueries[this.issuableType].mutation,
+ variables: {
+ listId: this.list.id,
+ collapsed,
+ },
+ optimisticResponse: {
+ updateBoardList: {
+ __typename: 'UpdateBoardListPayload',
+ errors: [],
+ list: {
+ ...this.list,
+ collapsed,
+ },
+ },
+ },
+ });
+ } catch {
+ this.$emit('error');
+ }
+ } else {
+ this.updateList({ listId: this.list.id, collapsed });
+ }
},
/**
* TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
@@ -251,6 +361,19 @@ export default {
const due = formatDate(dueDate, 'mmm d, yyyy', true);
return `${start} - ${due}`;
},
+ updateLocalCollapsedStatus(collapsed) {
+ if (this.isApolloBoard) {
+ this.$apollo.mutate({
+ mutation: toggleCollapsedMutations[this.issuableType].mutation,
+ variables: {
+ list: this.list,
+ collapsed,
+ },
+ });
+ } else {
+ this.toggleListCollapsed({ listId: this.list.id, collapsed });
+ }
+ },
},
};
</script>
@@ -364,7 +487,7 @@ export default {
<div v-if="list.maxIssueCount !== 0">
•
<gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')">
- <template #issuesSize>{{ itemsTooltipLabel }}</template>
+ <template #issuesSize>{{ itemsCount }}</template>
<template #maxIssueCount>{{ list.maxIssueCount }}</template>
</gl-sprintf>
</div>
@@ -392,7 +515,7 @@ export default {
<gl-icon class="gl-mr-2" :name="countIcon" :size="14" />
<item-count
v-if="!isLoading"
- :items-size="isEpicBoard ? list.epicsCount : boardList.issuesCount"
+ :items-size="itemsCount"
:max-issue-count="list.maxIssueCount"
/>
</span>
@@ -407,44 +530,24 @@ export default {
<!-- EE end -->
</span>
</div>
- <gl-button-group v-if="showListHeaderActions" class="board-list-button-group gl-pl-2">
- <gl-button
- v-if="isNewIssueShown"
- v-show="!list.collapsed"
- ref="newIssueBtn"
- v-gl-tooltip.hover
- :aria-label="$options.i18n.newIssue"
- :title="$options.i18n.newIssue"
- class="no-drag"
- size="small"
- icon="plus"
- @click="showNewIssueForm"
- />
-
- <gl-button
- v-if="isNewEpicShown"
- v-show="!list.collapsed"
- v-gl-tooltip.hover
- :aria-label="$options.i18n.newEpic"
- :title="$options.i18n.newEpic"
- class="no-drag"
- size="small"
- icon="plus"
- @click="showNewEpicForm"
- />
-
- <gl-button
- v-if="isSettingsShown"
- ref="settingsBtn"
- v-gl-tooltip.hover
- :aria-label="$options.i18n.listSettings"
- class="no-drag"
- size="small"
- :title="$options.i18n.listSettings"
- icon="settings"
- @click="openSidebarSettings"
- />
- </gl-button-group>
+ <gl-disclosure-dropdown
+ v-if="showListHeaderActions"
+ ref="headerListActions"
+ v-gl-tooltip.hover.top="{
+ title: $options.i18n.listActions,
+ boundary: 'viewport',
+ }"
+ data-testid="header-list-actions"
+ class="gl-py-2 gl-ml-3"
+ :aria-label="$options.i18n.listActions"
+ :title="$options.i18n.listActions"
+ category="tertiary"
+ icon="ellipsis_v"
+ :text-sr-only="true"
+ :items="actionListItems"
+ no-caret
+ placement="right"
+ />
</h3>
</header>
</template>
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index c0c2699b63d..23e0f2510a7 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -1,8 +1,15 @@
<script>
+import produce from 'immer';
import { GlButton, GlDrawer, GlLabel, GlModal, GlModalDirective } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
+import {
+ LIST,
+ ListType,
+ ListTypeTitles,
+ listsQuery,
+ deleteListQueries,
+} from 'ee_else_ce/boards/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
@@ -31,8 +38,34 @@ export default {
GlModal: GlModalDirective,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['canAdminList', 'scopedLabelsAvailable', 'isIssueBoard'],
+ inject: [
+ 'boardType',
+ 'canAdminList',
+ 'issuableType',
+ 'scopedLabelsAvailable',
+ 'isIssueBoard',
+ 'isApolloBoard',
+ ],
inheritAttrs: false,
+ props: {
+ listId: {
+ type: String,
+ required: true,
+ },
+ boardId: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ },
data() {
return {
ListType,
@@ -45,8 +78,11 @@ export default {
isWipLimitsOn() {
return this.glFeatures.wipLimits && this.isIssueBoard;
},
+ activeListId() {
+ return this.isApolloBoard ? this.listId : this.activeId;
+ },
activeList() {
- return this.boardLists[this.activeId] || {};
+ return (this.isApolloBoard ? this.list : this.boardLists[this.activeId]) || {};
},
activeListLabel() {
return this.activeList.label;
@@ -58,27 +94,60 @@ export default {
return ListTypeTitles[ListType.label];
},
showSidebar() {
+ if (this.isApolloBoard) {
+ return Boolean(this.listId);
+ }
return this.sidebarType === LIST && this.isSidebarOpen;
},
},
created() {
- eventHub.$on('sidebar.closeAll', this.unsetActiveId);
+ eventHub.$on('sidebar.closeAll', this.unsetActiveListId);
},
beforeDestroy() {
- eventHub.$off('sidebar.closeAll', this.unsetActiveId);
+ eventHub.$off('sidebar.closeAll', this.unsetActiveListId);
},
methods: {
...mapActions(['unsetActiveId', 'removeList']),
handleModalPrimary() {
- this.deleteBoard();
+ this.deleteBoardList();
},
showScopedLabels(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
},
- deleteBoard() {
+ async deleteBoardList() {
this.track('click_button', { label: 'remove_list' });
- this.removeList(this.activeId);
- this.unsetActiveId();
+ if (this.isApolloBoard) {
+ await this.deleteList(this.activeListId);
+ } else {
+ this.removeList(this.activeId);
+ }
+ this.unsetActiveListId();
+ },
+ unsetActiveListId() {
+ if (this.isApolloBoard) {
+ this.$emit('unsetActiveId');
+ } else {
+ this.unsetActiveId();
+ }
+ },
+ async deleteList(listId) {
+ await this.$apollo.mutate({
+ mutation: deleteListQueries[this.issuableType].mutation,
+ variables: {
+ listId,
+ },
+ update: (store) => {
+ store.updateQuery(
+ { query: listsQuery[this.issuableType].query, variables: this.queryVariables },
+ (sourceData) =>
+ produce(sourceData, (draftData) => {
+ draftData[this.boardType].board.lists.nodes = draftData[
+ this.boardType
+ ].board.lists.nodes.filter((list) => list.id !== listId);
+ }),
+ );
+ },
+ });
},
},
};
@@ -91,7 +160,7 @@ export default {
class="js-board-settings-sidebar gl-absolute"
:open="showSidebar"
variant="sidebar"
- @close="unsetActiveId"
+ @close="unsetActiveListId"
>
<template #title>
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">
@@ -109,7 +178,7 @@ export default {
</gl-button>
</div>
</template>
- <template v-if="isSidebarOpen">
+ <template v-if="showSidebar">
<div v-if="boardListType === ListType.label">
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
<gl-label
@@ -127,6 +196,7 @@ export default {
<board-settings-sidebar-wip-limit
v-if="isWipLimitsOn"
:max-issue-count="activeList.maxIssueCount"
+ :active-list-id="activeListId"
/>
</template>
</gl-drawer>
@@ -136,11 +206,11 @@ export default {
size="sm"
:action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalAction,
- attributes: [{ variant: 'danger' }],
+ attributes: { variant: 'danger' },
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:action-secondary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
- attributes: [{ variant: 'default' }],
+ attributes: { variant: 'default' },
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary="handleModalPrimary"
>
diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue
index 2e20ed70bb0..c186346b2ac 100644
--- a/app/assets/javascripts/boards/components/board_top_bar.vue
+++ b/app/assets/javascripts/boards/components/board_top_bar.vue
@@ -35,6 +35,10 @@ export default {
type: String,
required: true,
},
+ isSwimlanesOn: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -56,10 +60,28 @@ export default {
return !this.isApolloBoard;
},
update(data) {
- return data.workspace.board;
+ const { board } = data.workspace;
+ return {
+ ...board,
+ labels: board.labels?.nodes,
+ };
},
},
},
+ computed: {
+ hasScope() {
+ if (this.board.labels?.length > 0) {
+ return true;
+ }
+ let hasScope = false;
+ ['assignee', 'iterationCadence', 'iteration', 'milestone', 'weight'].forEach((attr) => {
+ if (this.board[attr] !== null && this.board[attr] !== undefined) {
+ hasScope = true;
+ }
+ });
+ return hasScope;
+ },
+ },
};
</script>
@@ -73,15 +95,28 @@ export default {
>
<boards-selector :board-apollo="board" @switchBoard="$emit('switchBoard', $event)" />
<new-board-button />
- <issue-board-filtered-search v-if="isIssueBoard" />
- <epic-board-filtered-search v-else />
+ <issue-board-filtered-search
+ v-if="isIssueBoard"
+ :board="board"
+ :is-swimlanes-on="isSwimlanesOn"
+ @setFilters="$emit('setFilters', $event)"
+ />
+ <epic-board-filtered-search
+ v-else
+ :board="board"
+ @setFilters="$emit('setFilters', $event)"
+ />
</div>
<div
class="filter-dropdown-container gl-md-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
>
<toggle-labels />
- <toggle-epics-swimlanes v-if="swimlanesFeatureAvailable && isSignedIn" />
- <config-toggle />
+ <toggle-epics-swimlanes
+ v-if="swimlanesFeatureAvailable && isSignedIn"
+ :is-swimlanes-on="isSwimlanesOn"
+ @toggleSwimlanes="$emit('toggleSwimlanes', $event)"
+ />
+ <config-toggle :board-has-scope="hasScope" />
<board-add-new-column-trigger v-if="canAdminList" />
<toggle-focus />
</div>
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index a1a49386b37..fddb58c45fe 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -8,6 +8,7 @@ import {
GlDropdownItem,
GlModalDirective,
} from '@gitlab/ui';
+import { produce } from 'immer';
import { throttle } from 'lodash';
import { mapActions, mapState } from 'vuex';
@@ -89,6 +90,9 @@ export default {
parentType() {
return this.boardType;
},
+ boardQuery() {
+ return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
+ },
loading() {
return this.loadingRecentBoards || this.loadingBoards;
},
@@ -140,6 +144,9 @@ export default {
},
methods: {
...mapActions(['setError', 'fetchBoard', 'unsetActiveId']),
+ fullBoardId(boardId) {
+ return fullBoardId(boardId);
+ },
showPage(page) {
this.currentPage = page;
},
@@ -155,9 +162,6 @@ export default {
name: node.name,
}));
},
- boardQuery() {
- return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
- },
recentBoardsQuery() {
return this.isGroupBoard ? groupRecentBoardsQuery : projectRecentBoardsQuery;
},
@@ -191,6 +195,29 @@ export default {
},
});
},
+ addBoard(board) {
+ const { defaultClient: store } = this.$apollo.provider.clients;
+
+ const sourceData = store.readQuery({
+ query: this.boardQuery,
+ variables: { fullPath: this.fullPath },
+ });
+
+ const newData = produce(sourceData, (draftState) => {
+ draftState[this.parentType].boards.edges = [
+ ...draftState[this.parentType].boards.edges,
+ { node: board },
+ ];
+ });
+
+ store.writeQuery({
+ query: this.boardQuery,
+ variables: { fullPath: this.fullPath },
+ data: newData,
+ });
+
+ this.$emit('switchBoard', board.id);
+ },
isScrolledUp() {
const { content } = this.$refs;
@@ -226,14 +253,13 @@ export default {
boardType: this.boardType,
});
},
- fullBoardId(boardId) {
- return fullBoardId(boardId);
- },
async switchBoard(boardId, e) {
if (isMetaKey(e)) {
window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
} else if (this.isApolloBoard) {
+ // Epic board ID is supported in EE version of this file
this.$emit('switchBoard', this.fullBoardId(boardId));
+ updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
} else {
this.unsetActiveId();
this.fetchCurrentBoard(boardId);
@@ -357,6 +383,7 @@ export default {
:weights="weights"
:current-board="boardToUse"
:current-page="currentPage"
+ @addBoard="addBoard"
@cancel="cancel"
/>
</span>
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index 7002fd44294..dd3b9472879 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -16,6 +16,13 @@ export default {
},
mixins: [Tracking.mixin()],
inject: ['canAdminList'],
+ props: {
+ boardHasScope: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
computed: {
...mapGetters(['hasScope']),
buttonText() {
@@ -40,7 +47,7 @@ export default {
v-gl-modal-directive="'board-config-modal'"
v-gl-tooltip
:title="tooltipTitle"
- :class="{ 'dot-highlight': hasScope }"
+ :class="{ 'dot-highlight': hasScope || boardHasScope }"
data-qa-selector="boards_config_button"
@click.prevent="showPage"
>
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index 7749391ec6f..3c056f296e1 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -1,12 +1,11 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { mapActions } from 'vuex';
import { orderBy } from 'lodash';
import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
-import issueBoardFilters from '~/boards/issue_board_filters';
+import issueBoardFilters from 'ee_else_ce/boards/issue_board_filters';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
@@ -47,11 +46,23 @@ export default {
},
components: { BoardFilteredSearch },
inject: ['isSignedIn', 'releasesFetchPath', 'fullPath', 'isGroupBoard'],
+ props: {
+ board: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ isSwimlanesOn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
computed: {
tokensCE() {
const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
- const { fetchUsers, fetchLabels } = issueBoardFilters(
+ const { fetchUsers, fetchLabels, fetchMilestones } = issueBoardFilters(
this.$apollo,
this.fullPath,
this.isGroupBoard,
@@ -135,7 +146,7 @@ export default {
token: MilestoneToken,
unique: true,
shouldSkipSort: true,
- fetchMilestones: this.fetchMilestones,
+ fetchMilestones,
},
{
icon: 'issues',
@@ -176,7 +187,6 @@ export default {
},
},
methods: {
- ...mapActions(['fetchMilestones']),
preloadedUsers() {
return gon?.current_user_id
? [
@@ -194,5 +204,11 @@ export default {
</script>
<template>
- <board-filtered-search data-testid="issue-board-filtered-search" :tokens="tokens" />
+ <board-filtered-search
+ data-testid="issue-board-filtered-search"
+ :tokens="tokens"
+ :board="board"
+ :is-swimlanes-on="isSwimlanesOn"
+ @setFilters="$emit('setFilters', $event)"
+ />
</template>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index c3f7c7d3ca2..1f28974afd1 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -95,9 +95,12 @@ export default {
class="board-card-info-icon gl-mr-2"
name="calendar"
/>
- <time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{
- body
- }}</time>
+ <time
+ :class="{ 'text-danger': isPastDue }"
+ datetime="date"
+ class="gl-font-sm board-card-info-text"
+ >{{ body }}</time
+ >
</span>
<gl-tooltip :target="() => $refs.issueDueDate" :placement="tooltipPlacement">
<span class="bold">{{ __('Due date') }}</span>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index bc12717a92d..611e875fa40 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -38,7 +38,7 @@ export default {
<span>
<span ref="issueTimeEstimate" class="board-card-info gl-mr-3 gl-text-secondary gl-cursor-help">
<gl-icon name="hourglass" class="board-card-info-icon gl-mr-2" />
- <time class="board-card-info-text">{{ timeEstimate }}</time>
+ <time class="gl-font-sm board-card-info-text">{{ timeEstimate }}</time>
</span>
<gl-tooltip
:target="() => $refs.issueTimeEstimate"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index 43a2b13b81c..020edcb01b8 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -5,6 +5,7 @@ import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.v
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+import { titleQueries } from 'ee_else_ce/boards/constants';
export default {
components: {
@@ -19,6 +20,13 @@ export default {
directives: {
autofocusonshow,
},
+ inject: ['fullPath', 'issuableType', 'isEpicBoard', 'isApolloBoard'],
+ props: {
+ activeItem: {
+ type: Object,
+ required: true,
+ },
+ },
data() {
return {
title: '',
@@ -27,7 +35,10 @@ export default {
};
},
computed: {
- ...mapGetters({ item: 'activeBoardItem' }),
+ ...mapGetters(['activeBoardItem']),
+ item() {
+ return this.isApolloBoard ? this.activeItem : this.activeBoardItem;
+ },
pendingChangesStorageKey() {
return this.getPendingChangesKey(this.item);
},
@@ -67,8 +78,9 @@ export default {
},
async setPendingState() {
const pendingChanges = localStorage.getItem(this.pendingChangesStorageKey);
+ const shouldOpen = pendingChanges !== this.title;
- if (pendingChanges) {
+ if (pendingChanges && shouldOpen) {
this.title = pendingChanges;
this.showChangesAlert = true;
await this.$nextTick();
@@ -83,6 +95,26 @@ export default {
this.showChangesAlert = false;
localStorage.removeItem(this.pendingChangesStorageKey);
},
+ async setActiveBoardItemTitle() {
+ if (!this.isApolloBoard) {
+ await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
+ return;
+ }
+ const { fullPath, issuableType, isEpicBoard, title } = this;
+ const workspacePath = isEpicBoard
+ ? { groupPath: fullPath }
+ : { projectPath: this.projectPath };
+ await this.$apollo.mutate({
+ mutation: titleQueries[issuableType].mutation,
+ variables: {
+ input: {
+ ...workspacePath,
+ iid: String(this.item.iid),
+ title,
+ },
+ },
+ });
+ },
async setTitle() {
this.$refs.sidebarItem.collapse();
@@ -92,7 +124,7 @@ export default {
try {
this.loading = true;
- await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
+ await this.setActiveBoardItemTitle();
localStorage.removeItem(this.pendingChangesStorageKey);
this.showChangesAlert = false;
} catch (e) {
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 712e3e1ac4a..7fe89ffbb52 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,25 +1,18 @@
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_EPIC, TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { s__, __ } from '~/locale';
import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
import destroyBoardListMutation from './graphql/board_list_destroy.mutation.graphql';
import updateBoardListMutation from './graphql/board_list_update.mutation.graphql';
+import toggleListCollapsedMutation from './graphql/client/board_toggle_collapsed.mutation.graphql';
import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
import groupBoardQuery from './graphql/group_board.query.graphql';
import projectBoardQuery from './graphql/project_board.query.graphql';
import listIssuesQuery from './graphql/lists_issues.query.graphql';
-/* eslint-disable-next-line @gitlab/require-i18n-strings */
-export const AssigneeIdParamValues = ['Any', 'None'];
-
-export const issuableTypes = {
- issue: 'issue',
- epic: 'epic',
-};
-
export const BoardType = {
project: 'project',
group: 'group',
@@ -64,10 +57,10 @@ export const INCIDENT = 'INCIDENT';
export const flashAnimationDuration = 2000;
export const boardQuery = {
- [BoardType.group]: {
+ [WORKSPACE_GROUP]: {
query: groupBoardQuery,
},
- [BoardType.project]: {
+ [WORKSPACE_PROJECT]: {
query: projectBoardQuery,
},
};
@@ -84,6 +77,12 @@ export const updateListQueries = {
},
};
+export const toggleCollapsedMutations = {
+ [TYPE_ISSUE]: {
+ mutation: toggleListCollapsedMutation,
+ },
+};
+
export const deleteListQueries = {
[TYPE_ISSUE]: {
mutation: destroyBoardListMutation,
@@ -94,7 +93,7 @@ export const titleQueries = {
[TYPE_ISSUE]: {
mutation: issueSetTitleMutation,
},
- [issuableTypes.epic]: {
+ [TYPE_EPIC]: {
mutation: updateEpicTitleMutation,
},
};
@@ -103,7 +102,7 @@ export const subscriptionQueries = {
[TYPE_ISSUE]: {
mutation: issueSetSubscriptionMutation,
},
- [issuableTypes.epic]: {
+ [TYPE_EPIC]: {
mutation: updateEpicSubscriptionMutation,
},
};
@@ -143,6 +142,7 @@ export const MilestoneFilterType = {
started: 'Started',
upcoming: 'Upcoming',
};
+/* eslint-enable @gitlab/require-i18n-strings */
export const DraggableItemTypes = {
card: 'card',
@@ -155,7 +155,6 @@ export const MilestoneIDs = {
};
export default {
- BoardType,
ListType,
};
@@ -178,3 +177,5 @@ export const BOARD_CARD_MOVE_TO_POSITIONS_OPTIONS = [
action: () => {},
},
];
+
+export const GroupByParamType = {};
diff --git a/app/assets/javascripts/boards/graphql/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
index 06e8c8783de..e987ee08df2 100644
--- a/app/assets/javascripts/boards/graphql/board_lists.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
@@ -3,6 +3,7 @@
query BoardLists(
$fullPath: ID!
$boardId: BoardID!
+ $listId: ListID
$filters: BoardIssueInput
$isGroup: Boolean = false
$isProject: Boolean = false
@@ -12,7 +13,7 @@ query BoardLists(
board(id: $boardId) {
id
hideBacklogList
- lists(issueFilters: $filters) {
+ lists(issueFilters: $filters, id: $listId) {
nodes {
...BoardListFragment
}
@@ -24,7 +25,7 @@ query BoardLists(
board(id: $boardId) {
id
hideBacklogList
- lists(issueFilters: $filters) {
+ lists(issueFilters: $filters, id: $listId) {
nodes {
...BoardListFragment
}
diff --git a/app/assets/javascripts/boards/graphql/client/active_board_item.query.graphql b/app/assets/javascripts/boards/graphql/client/active_board_item.query.graphql
new file mode 100644
index 00000000000..81b1b68a038
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/active_board_item.query.graphql
@@ -0,0 +1,7 @@
+#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
+
+query activeBoardItem {
+ activeBoardItem @client {
+ ...Issue
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/client/board_toggle_collapsed.mutation.graphql b/app/assets/javascripts/boards/graphql/client/board_toggle_collapsed.mutation.graphql
new file mode 100644
index 00000000000..890152989eb
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/board_toggle_collapsed.mutation.graphql
@@ -0,0 +1,9 @@
+#import "ee_else_ce/boards/graphql/board_list.fragment.graphql"
+
+mutation toggleListCollapsed($list: BoardList!, $collapsed: Boolean!) {
+ clientToggleListCollapsed(list: $list, collapsed: $collapsed) @client {
+ list {
+ ...BoardListFragment
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/client/set_active_board_item.mutation.graphql b/app/assets/javascripts/boards/graphql/client/set_active_board_item.mutation.graphql
new file mode 100644
index 00000000000..cce558c649e
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/set_active_board_item.mutation.graphql
@@ -0,0 +1,7 @@
+#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
+
+mutation setActiveBoardItem($boardItem: Issue) {
+ setActiveBoardItem(boardItem: $boardItem) @client {
+ ...Issue
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
index 9e6c26063e9..14811b435e1 100644
--- a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
@@ -1,5 +1,5 @@
query GroupBoardMilestones($fullPath: ID!, $searchTerm: String, $state: MilestoneStateEnum) {
- group(fullPath: $fullPath) {
+ workspace: group(fullPath: $fullPath) {
id
milestones(
includeAncestors: true
diff --git a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
index 02aa08f90ef..9af92a6ff2d 100644
--- a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
@@ -1,5 +1,5 @@
query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String, $state: MilestoneStateEnum) {
- project(fullPath: $fullPath) {
+ workspace: project(fullPath: $fullPath) {
id
milestones(
searchTitle: $searchTerm
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 4c6f341828c..67388284d31 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -3,9 +3,8 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import BoardApp from '~/boards/components/board_app.vue';
import '~/boards/filters/due_date_filters';
-import { BoardType } from '~/boards/constants';
import store from '~/boards/stores';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import {
NavigationType,
isLoggedIn,
@@ -68,8 +67,8 @@ function mountBoardApp(el) {
initialFilterParams,
boardBaseUrl: el.dataset.boardBaseUrl,
boardType,
- isGroupBoard: boardType === BoardType.group,
- isProjectBoard: boardType === BoardType.project,
+ isGroupBoard: boardType === WORKSPACE_GROUP,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
currentUserId: gon.current_user_id || null,
boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
labelsManagePath: el.dataset.labelsManagePath,
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
index 7e9b68778d5..27efb3f775c 100644
--- a/app/assets/javascripts/boards/issue_board_filters.js
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -1,5 +1,7 @@
import groupBoardMembers from '~/boards/graphql/group_board_members.query.graphql';
import projectBoardMembers from '~/boards/graphql/project_board_members.query.graphql';
+import groupBoardMilestonesQuery from './graphql/group_board_milestones.query.graphql';
+import projectBoardMilestonesQuery from './graphql/project_board_milestones.query.graphql';
import boardLabels from './graphql/board_labels.query.graphql';
export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
@@ -37,8 +39,27 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
.then(transformLabels);
};
+ const fetchMilestones = (searchTerm) => {
+ const variables = {
+ fullPath,
+ searchTerm,
+ };
+
+ const query = isGroupBoard ? groupBoardMilestonesQuery : projectBoardMilestonesQuery;
+
+ return apollo
+ .query({
+ query,
+ variables,
+ })
+ .then(({ data }) => {
+ return data.workspace?.milestones.nodes;
+ });
+ };
+
return {
fetchLabels,
fetchUsers,
+ fetchMilestones,
};
}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 1b4e6334723..a144054d680 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/browser';
import { sortBy } from 'lodash';
import {
- BoardType,
ListType,
inactiveId,
flashAnimationDuration,
@@ -34,7 +33,7 @@ import totalCountAndWeightQuery from 'ee_else_ce/boards/graphql/board_lists_defe
import { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
@@ -61,7 +60,7 @@ export default {
return gqlClient
.query({
- query: boardType === BoardType.group ? groupBoardQuery : projectBoardQuery,
+ query: boardType === WORKSPACE_GROUP ? groupBoardQuery : projectBoardQuery,
variables,
})
.then(({ data }) => {
@@ -139,8 +138,8 @@ export default {
boardId: fullBoardId,
filters: filterParams,
...(issuableType === TYPE_ISSUE && {
- isGroup: boardType === BoardType.group,
- isProject: boardType === BoardType.project,
+ isGroup: boardType === WORKSPACE_GROUP,
+ isProject: boardType === WORKSPACE_PROJECT,
}),
};
@@ -234,8 +233,8 @@ export default {
const variables = {
fullPath,
searchTerm,
- isGroup: boardType === BoardType.group,
- isProject: boardType === BoardType.project,
+ isGroup: boardType === WORKSPACE_GROUP,
+ isProject: boardType === WORKSPACE_PROJECT,
};
commit(types.RECEIVE_LABELS_REQUEST);
@@ -268,10 +267,10 @@ export default {
};
let query;
- if (boardType === BoardType.project) {
+ if (boardType === WORKSPACE_PROJECT) {
query = projectBoardMilestonesQuery;
}
- if (boardType === BoardType.group) {
+ if (boardType === WORKSPACE_GROUP) {
query = groupBoardMilestonesQuery;
}
@@ -286,8 +285,8 @@ export default {
variables,
})
.then(({ data }) => {
- const errors = data[boardType]?.errors;
- const milestones = data[boardType]?.milestones.nodes;
+ const errors = data.workspace?.errors;
+ const milestones = data.workspace?.milestones.nodes;
if (errors?.[0]) {
throw new Error(errors[0]);
@@ -431,8 +430,8 @@ export default {
boardId: fullBoardId,
id: listId,
filters: filterParams,
- isGroup: boardType === BoardType.group,
- isProject: boardType === BoardType.project,
+ isGroup: boardType === WORKSPACE_GROUP,
+ isProject: boardType === WORKSPACE_PROJECT,
first: DEFAULT_BOARD_LIST_ITEMS_SIZE,
after: fetchNext ? state.pageInfoByListId[listId].endCursor : undefined,
};
@@ -710,7 +709,7 @@ export default {
) => {
const input = formatIssueInput(issueInput, boardConfig);
- if (boardType === BoardType.project) {
+ if (boardType === WORKSPACE_PROJECT) {
input.projectPath = fullPath;
}
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index fef5862f319..505c011b034 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,18 +1,19 @@
import { cloneDeep, pull, union } from 'lodash';
import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_EPIC } from '~/issues/constants';
import { s__, __ } from '~/locale';
import { formatIssue } from '../boards_util';
-import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types';
const updateListItemsCount = ({ state, listId, value }) => {
const list = state.boardLists[listId];
- if (state.issuableType === issuableTypes.epic) {
- Vue.set(state.boardLists, listId, { ...list, epicsCount: list.epicsCount + value });
- } else {
- Vue.set(state.boardLists, listId, { ...list });
+ if (state.issuableType === TYPE_EPIC) {
+ const listItem = cloneDeep(state.boardLists[listId]);
+ listItem.metadataepicsCount += value;
+ Vue.set(state.boardLists[listId], listId, listItem);
}
+ Vue.set(state.boardLists, listId, { ...list });
};
export const removeItemFromList = ({ state, listId, itemId, reordering = false }) => {