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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 13:34:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 13:34:06 +0300
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /app/assets/javascripts/boards
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/boards_util.js14
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_trigger.vue21
-rw-r--r--app/assets/javascripts/boards/components/board_assignee_dropdown.vue196
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout_deprecated.vue102
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_column_deprecated.vue16
-rw-r--r--app/assets/javascripts/boards/components/board_configuration_options.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue12
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue20
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue43
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_deprecated.vue41
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue19
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js67
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue38
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue357
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue10
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue4
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue8
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue2
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js41
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue2
-rw-r--r--app/assets/javascripts/boards/components/project_select_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue20
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue62
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue8
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue86
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue2
-rw-r--r--app/assets/javascripts/boards/components/toggle_focus.vue52
-rw-r--r--app/assets/javascripts/boards/constants.js20
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js8
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js2
-rw-r--r--app/assets/javascripts/boards/graphql/project_milestones.query.graphql (renamed from app/assets/javascripts/boards/graphql/group_milestones.query.graphql)6
-rw-r--r--app/assets/javascripts/boards/index.js50
-rw-r--r--app/assets/javascripts/boards/models/issue.js6
-rw-r--r--app/assets/javascripts/boards/models/iteration.js9
-rw-r--r--app/assets/javascripts/boards/models/list.js11
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js20
-rw-r--r--app/assets/javascripts/boards/stores/actions.js105
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js22
-rw-r--r--app/assets/javascripts/boards/stores/getters.js10
-rw-r--r--app/assets/javascripts/boards/stores/index.js4
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js7
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js34
-rw-r--r--app/assets/javascripts/boards/stores/state.js4
-rw-r--r--app/assets/javascripts/boards/toggle_focus.js48
59 files changed, 1114 insertions, 626 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 965d3571f42..13ad820477f 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,6 +1,6 @@
import { sortBy } from 'lodash';
-import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ListType, NOT_FILTER } from './constants';
export function getMilestone() {
return null;
@@ -144,6 +144,17 @@ export function isListDraggable(list) {
return list.listType !== ListType.backlog && list.listType !== ListType.closed;
}
+export function transformNotFilters(filters) {
+ return Object.keys(filters)
+ .filter((key) => key.startsWith(NOT_FILTER))
+ .reduce((obj, key) => {
+ return {
+ ...obj,
+ [key.substring(4, key.length - 1)]: filters[key],
+ };
+ }, {});
+}
+
// EE-specific feature. Find the implementation in the `ee/`-folder
export function transformBoardConfig() {
return '';
@@ -157,4 +168,5 @@ export default {
fullLabelId,
fullIterationId,
isListDraggable,
+ transformNotFilters,
};
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
new file mode 100644
index 00000000000..85fca589279
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { mapActions } from 'vuex';
+
+export default {
+ components: {
+ GlButton,
+ },
+ methods: {
+ ...mapActions(['setAddColumnFormVisibility']),
+ },
+};
+</script>
+
+<template>
+ <span class="gl-ml-3 gl-display-flex gl-align-items-center">
+ <gl-button variant="confirm" @click="setAddColumnFormVisibility(true)"
+ >{{ __('Create list') }}
+ </gl-button>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
deleted file mode 100644
index 5d381f9a570..00000000000
--- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
+++ /dev/null
@@ -1,196 +0,0 @@
-<script>
-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/graphql/users_search.query.graphql';
-
-export default {
- noSearchDelay: 0,
- searchDelay: 250,
- i18n: {
- unassigned: __('Unassigned'),
- assignee: __('Assignee'),
- assignees: __('Assignees'),
- assignTo: __('Assign to'),
- },
- components: {
- BoardEditableItem,
- IssuableAssignees,
- MultiSelectDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlAvatarLabeled,
- GlAvatarLink,
- GlSearchBoxByType,
- GlLoadingIcon,
- },
- data() {
- return {
- search: '',
- participants: [],
- selected: [],
- };
- },
- apollo: {
- participants: {
- query() {
- return this.isSearchEmpty ? getIssueParticipants : searchUsers;
- },
- variables() {
- if (this.isSearchEmpty) {
- return {
- id: `gid://gitlab/Issue/${this.activeIssue.iid}`,
- };
- }
-
- return {
- search: this.search,
- };
- },
- update(data) {
- if (this.isSearchEmpty) {
- return data.issue?.participants?.nodes || [];
- }
-
- return data.users?.nodes || [];
- },
- debounce() {
- const { noSearchDelay, searchDelay } = this.$options;
-
- return this.isSearchEmpty ? noSearchDelay : searchDelay;
- },
- },
- },
- computed: {
- ...mapGetters(['activeIssue']),
- ...mapState(['isSettingAssignees']),
- assigneeText() {
- return n__('Assignee', '%d Assignees', this.selected.length);
- },
- unSelectedFiltered() {
- return this.participants.filter(({ username }) => {
- return !this.selectedUserNames.includes(username);
- });
- },
- selectedIsEmpty() {
- return this.selected.length === 0;
- },
- selectedUserNames() {
- return this.selected.map(({ username }) => username);
- },
- 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 = [];
- },
- selectAssignee(name) {
- if (name === undefined) {
- this.clearSelected();
- return;
- }
-
- this.selected = this.selected.concat(name);
- },
- unselect(name) {
- this.selected = this.selected.filter((user) => user.username !== name);
- },
- saveAssignees() {
- this.setAssignees(this.selectedUserNames);
- },
- isChecked(id) {
- return this.selectedUserNames.includes(id);
- },
- },
-};
-</script>
-
-<template>
- <board-editable-item :loading="isSettingAssignees" :title="assigneeText" @close="saveAssignees">
- <template #collapsed>
- <issuable-assignees :users="activeIssue.assignees" @assign-self="assignSelf" />
- </template>
-
- <template #default>
- <multi-select-dropdown
- class="w-100"
- :text="$options.i18n.assignees"
- :header-text="$options.i18n.assignTo"
- >
- <template #search>
- <gl-search-box-by-type v-model.trim="search" />
- </template>
- <template #items>
- <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>
- </board-editable-item>
-</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 31050eef83d..e6009343626 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,13 +1,14 @@
<script>
-import BoardCardLayout from './board_card_layout.vue';
-import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
+import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
+import BoardCardLayout from './board_card_layout.vue';
+import BoardCardLayoutDeprecated from './board_card_layout_deprecated.vue';
export default {
name: 'BoardsIssueCard',
components: {
- BoardCardLayout,
+ BoardCardLayout: gon.features?.graphqlBoardLists ? BoardCardLayout : BoardCardLayoutDeprecated,
},
props: {
list: {
diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue
index 0a2301394c1..5e3c3702519 100644
--- a/app/assets/javascripts/boards/components/board_card_layout.vue
+++ b/app/assets/javascripts/boards/components/board_card_layout.vue
@@ -1,17 +1,13 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
-import IssueCardInner from './issue_card_inner.vue';
-import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
-import boardsStore from '../stores/boards_store';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { ISSUABLE } from '~/boards/constants';
+import IssueCardInner from './issue_card_inner.vue';
export default {
name: 'BoardCardLayout',
components: {
- IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
+ IssueCardInner,
},
- mixins: [glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -42,17 +38,17 @@ export default {
data() {
return {
showDetail: false,
- multiSelect: boardsStore.multiSelect,
};
},
computed: {
+ ...mapState(['selectedBoardItems']),
...mapGetters(['isSwimlanesOn']),
multiSelectVisible() {
- return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1;
+ return this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.issue.id) > -1;
},
},
methods: {
- ...mapActions(['setActiveId']),
+ ...mapActions(['setActiveId', 'toggleBoardItemMultiSelection']),
mouseDown() {
this.showDetail = true;
},
@@ -63,16 +59,16 @@ export default {
// Don't do anything if this happened on a no trigger element
if (e.target.classList.contains('js-no-trigger')) return;
- if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) {
+ const isMultiSelect = e.ctrlKey || e.metaKey;
+
+ if (!isMultiSelect) {
this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
- return;
+ } else {
+ this.toggleBoardItemMultiSelection(this.issue);
}
- const isMultiSelect = e.ctrlKey || e.metaKey;
-
if (this.showDetail || isMultiSelect) {
this.showDetail = false;
- this.$emit('show', { event: e, isMultiSelect });
}
},
},
diff --git a/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
new file mode 100644
index 00000000000..f9a726134a3
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
@@ -0,0 +1,102 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import { ISSUABLE } from '~/boards/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import boardsStore from '../stores/boards_store';
+import IssueCardInner from './issue_card_inner.vue';
+import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
+
+export default {
+ name: 'BoardCardLayout',
+ components: {
+ IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ issue: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ required: false,
+ },
+ isActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showDetail: false,
+ multiSelect: boardsStore.multiSelect,
+ };
+ },
+ computed: {
+ ...mapGetters(['isSwimlanesOn']),
+ multiSelectVisible() {
+ return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1;
+ },
+ },
+ methods: {
+ ...mapActions(['setActiveId']),
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ // Don't do anything if this happened on a no trigger element
+ if (e.target.classList.contains('js-no-trigger')) return;
+
+ if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) {
+ this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
+ return;
+ }
+
+ const isMultiSelect = e.ctrlKey || e.metaKey;
+
+ if (this.showDetail || isMultiSelect) {
+ this.showDetail = false;
+ this.$emit('show', { event: e, isMultiSelect });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="{
+ 'multi-select': multiSelectVisible,
+ 'user-can-drag': !disabled && issue.id,
+ 'is-disabled': disabled || !issue.id,
+ 'is-active': isActive,
+ }"
+ :index="index"
+ :data-issue-id="issue.id"
+ :data-issue-iid="issue.iid"
+ :data-issue-path="issue.referencePath"
+ data-testid="board_card"
+ class="board-card gl-p-5 gl-rounded-base"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)"
+ >
+ <issue-card-inner :list="list" :issue="issue" :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 9f0eef844f6..41b9ee795eb 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
-import BoardList from './board_list.vue';
import { isListDraggable } from '../boards_util';
+import BoardList from './board_list.vue';
export default {
components: {
@@ -31,8 +31,11 @@ export default {
},
},
computed: {
- ...mapState(['filterParams']),
+ ...mapState(['filterParams', 'highlightedLists']),
...mapGetters(['getIssuesByList']),
+ highlighted() {
+ return this.highlightedLists.includes(this.list.id);
+ },
listIssues() {
return this.getIssuesByList(this.list.id);
},
@@ -48,6 +51,16 @@ export default {
deep: true,
immediate: true,
},
+ highlighted: {
+ handler(highlighted) {
+ if (highlighted) {
+ this.$nextTick(() => {
+ this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ }
+ },
+ immediate: true,
+ },
},
methods: {
...mapActions(['fetchIssuesForList']),
@@ -68,6 +81,7 @@ export default {
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
+ :class="{ 'board-column-highlighted': highlighted }"
>
<board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list
diff --git a/app/assets/javascripts/boards/components/board_column_deprecated.vue b/app/assets/javascripts/boards/components/board_column_deprecated.vue
index 35688efceb4..3dc77654e28 100644
--- a/app/assets/javascripts/boards/components/board_column_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_column_deprecated.vue
@@ -2,9 +2,9 @@
// This component is being replaced in favor of './board_column.vue' for GraphQL boards
import Sortable from 'sortablejs';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_deprecated.vue';
-import BoardList from './board_list_deprecated.vue';
-import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
+import boardsStore from '../stores/boards_store';
+import BoardList from './board_list_deprecated.vue';
export default {
components: {
@@ -46,6 +46,7 @@ export default {
watch: {
filter: {
handler() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.page = 1;
this.list.getIssues(true).catch(() => {
// TODO: handle request error
@@ -53,6 +54,16 @@ export default {
},
deep: true,
},
+ 'list.highlighted': {
+ handler(highlighted) {
+ if (highlighted) {
+ this.$nextTick(() => {
+ this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ }
+ },
+ immediate: true,
+ },
},
mounted() {
const instance = this;
@@ -97,6 +108,7 @@ export default {
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
+ :class="{ 'board-column-highlighted': list.highlighted }"
>
<board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" />
diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue
index b8ee930a8c9..4d79f2a4bc6 100644
--- a/app/assets/javascripts/boards/components/board_configuration_options.vue
+++ b/app/assets/javascripts/boards/components/board_configuration_options.vue
@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: true,
},
+ readonly: {
+ type: Boolean,
+ required: true,
+ },
},
};
</script>
@@ -28,12 +32,14 @@ export default {
</p>
<gl-form-checkbox
:checked="!hideBacklogList"
+ :disabled="readonly"
data-testid="backlog-list-checkbox"
@change="$emit('update:hideBacklogList', !hideBacklogList)"
>{{ __('Show the Open list') }}
</gl-form-checkbox>
<gl-form-checkbox
:checked="!hideClosedList"
+ :disabled="readonly"
data-testid="closed-list-checkbox"
@change="$emit('update:hideClosedList', !hideClosedList)"
>{{ __('Show the Closed list') }}
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 19254343208..9b10e7d7db5 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,13 +1,13 @@
<script>
+import { GlAlert } from '@gitlab/ui';
+import { sortBy } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
-import { sortBy } from 'lodash';
-import { GlAlert } from '@gitlab/ui';
-import BoardColumnDeprecated from './board_column_deprecated.vue';
-import BoardColumn from './board_column.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';
+import defaultSortableConfig from '~/sortable/sortable_config';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import BoardColumn from './board_column.vue';
+import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index c701ecd3040..f65f00bcccc 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,17 +1,17 @@
<script>
import { GlModal } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import { visitUrl } from '~/lib/utils/url_utility';
-import { getParameterByName } from '~/lib/utils/common_utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import boardsStore from '~/boards/stores/boards_store';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { __, s__ } from '~/locale';
import { fullLabelId, fullBoardId } from '../boards_util';
+import { formType } from '../constants';
-import BoardConfigurationOptions from './board_configuration_options.vue';
-import updateBoardMutation from '../graphql/board_update.mutation.graphql';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
import destroyBoardMutation from '../graphql/board_destroy.mutation.graphql';
+import updateBoardMutation from '../graphql/board_update.mutation.graphql';
+import BoardConfigurationOptions from './board_configuration_options.vue';
const boardDefaults = {
id: false,
@@ -26,12 +26,6 @@ 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') },
@@ -100,11 +94,14 @@ export default {
type: Object,
required: true,
},
+ currentPage: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
- currentPage: boardsStore.state.currentPage,
isLoading: false,
};
},
@@ -256,7 +253,7 @@ export default {
}
},
cancel() {
- boardsStore.showPage('');
+ this.$emit('cancel');
},
resetFormState() {
if (this.isNewForm) {
@@ -308,6 +305,7 @@ export default {
<board-configuration-options
:hide-backlog-list.sync="board.hide_backlog_list"
:hide-closed-list.sync="board.hide_closed_list"
+ :readonly="readonly"
/>
<board-scope
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index b6e4d0980fa..7495b1163be 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,13 +1,13 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
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.vue';
-import BoardCard from './board_card.vue';
-import eventHub from '../eventhub';
import { sprintf, __ } from '~/locale';
+import defaultSortableConfig from '~/sortable/sortable_config';
+import eventHub from '../eventhub';
+import BoardCard from './board_card.vue';
+import BoardNewIssue from './board_new_issue.vue';
export default {
name: 'BoardList',
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index 24900346bda..9b4961d362d 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -1,17 +1,18 @@
<script>
-import { Sortable, MultiDrag } from 'sortablejs';
import { GlLoadingIcon } from '@gitlab/ui';
-import boardNewIssue from './board_new_issue_deprecated.vue';
-import boardCard from './board_card.vue';
-import eventHub from '../eventhub';
-import boardsStore from '../stores/boards_store';
-import { sprintf, __ } from '~/locale';
+import { Sortable, MultiDrag } from 'sortablejs';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import { sprintf, __ } from '~/locale';
+import eventHub from '../eventhub';
import {
getBoardSortableDefaultOptions,
sortableStart,
sortableEnd,
} from '../mixins/sortable_default_options';
+import boardsStore from '../stores/boards_store';
+import boardCard from './board_card.vue';
+import boardNewIssue from './board_new_issue_deprecated.vue';
// This component is being replaced in favor of './board_list.vue' for GraphQL boards
@@ -63,6 +64,7 @@ export default {
watch: {
filters: {
handler() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
@@ -75,6 +77,7 @@ export default {
this.list.issuesSize > this.list.issues.length &&
this.list.isExpanded
) {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.page += 1;
this.list.getIssues(false).catch(() => {
// TODO: handle request error
@@ -165,7 +168,7 @@ export default {
boardsStore.startMoving(list, issue);
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
sortableStart();
},
@@ -283,6 +286,7 @@ export default {
* issue indexes are far apart, this logic should ever kick in.
*/
setTimeout(() => {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.issues.splice(i, 1);
}, 0);
});
@@ -386,10 +390,12 @@ export default {
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = false;
};
if (getIssues) {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = true;
getIssues.then(loadingDone).catch(loadingDone);
}
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 06f39eceb08..a933370427c 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlButton,
GlButtonGroup,
@@ -9,14 +8,16 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { isListDraggable } from '~/boards/boards_util';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
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 AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType } from '../constants';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { isListDraggable } from '~/boards/boards_util';
+import eventHub from '../eventhub';
+import IssueCount from './issue_count.vue';
export default {
i18n: {
@@ -85,16 +86,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
- return (
- this.listType === ListType.milestone &&
- this.list.milestone &&
- (!this.list.collapsed || !this.isSwimlanesHeader)
- );
+ return this.listType === ListType.milestone && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
- return (
- this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader)
- );
+ return this.listType === ListType.assignee && this.showListDetails;
+ },
+ showIterationListDetails() {
+ return this.listType === ListType.iteration && this.showListDetails;
+ },
+ showListDetails() {
+ return !this.list.collapsed || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesCount;
@@ -147,6 +148,7 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.collapsed = !this.list.collapsed;
if (!this.isLoggedIn) {
@@ -157,7 +159,7 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
@@ -216,6 +218,17 @@ export default {
<gl-icon name="timer" />
</span>
+ <span
+ v-if="showIterationListDetails"
+ aria-hidden="true"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': list.collapsed,
+ 'gl-mr-2': !list.collapsed,
+ }"
+ >
+ <gl-icon name="iteration" />
+ </span>
+
<a
v-if="showAssigneeListDetails"
:href="list.assignee.webUrl"
diff --git a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
index 21147f1616c..ff043d3aa01 100644
--- a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlButton,
GlButtonGroup,
@@ -9,14 +8,16 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__ } from '~/locale';
-import AccessorUtilities from '../../lib/utils/accessor';
-import IssueCount from './issue_count.vue';
-import boardsStore from '../stores/boards_store';
-import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
+import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType } from '../constants';
-import { isScopedLabel } from '~/lib/utils/common_utils';
+import eventHub from '../eventhub';
+import boardsStore from '../stores/boards_store';
+import IssueCount from './issue_count.vue';
// This component is being replaced in favor of './board_list_header.vue' for GraphQL boards
@@ -77,14 +78,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
- return (
- this.list.type === 'milestone' &&
- this.list.milestone &&
- (this.list.isExpanded || !this.isSwimlanesHeader)
- );
+ return this.list.type === 'milestone' && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
- return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
+ return this.list.type === 'assignee' && this.showListDetails;
+ },
+ showIterationListDetails() {
+ return this.listType === ListType.iteration && this.showListDetails;
+ },
+ showListDetails() {
+ return this.list.isExpanded || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesSize;
@@ -131,6 +134,7 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.isExpanded = !this.list.isExpanded;
if (!this.isLoggedIn) {
@@ -141,7 +145,7 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
@@ -201,6 +205,17 @@ export default {
<gl-icon name="timer" />
</span>
+ <span
+ v-if="showIterationListDetails"
+ aria-hidden="true"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ 'gl-mr-2': list.isExpanded,
+ }"
+ >
+ <gl-icon name="iteration" />
+ </span>
+
<a
v-if="showAssigneeListDetails"
:href="list.assignee.path"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 14d28643046..1df154688c8 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,11 +1,11 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
+import { __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { __ } from '~/locale';
export default {
name: 'BoardNewIssue',
diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
index 4fc58742783..eff87ff110e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
@@ -2,10 +2,10 @@
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub';
-import ProjectSelect from './project_select_deprecated.vue';
import boardsStore from '../stores/boards_store';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ProjectSelect from './project_select_deprecated.vue';
// This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index f362fc60bd3..7cfedad0aed 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -1,21 +1,17 @@
<script>
import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { __ } from '~/locale';
+import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
-import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import { LIST } from '~/boards/constants';
+import { __ } from '~/locale';
+import eventHub from '~/sidebar/event_hub';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List settings'),
- assignee: 'assignee',
- milestone: 'milestone',
- label: 'label',
- labelListText: __('Label'),
components: {
GlButton,
GlDrawer,
@@ -33,6 +29,11 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ ListType,
+ };
+ },
computed: {
...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
@@ -56,7 +57,7 @@ export default {
return this.activeList.type || this.activeList.listType || null;
},
listTypeTitle() {
- return this.$options.labelListText;
+ return ListTypeTitles[ListType.label];
},
showSidebar() {
return this.sidebarType === LIST;
@@ -98,7 +99,7 @@ export default {
>
<template #header>{{ $options.listSettingsText }}</template>
<template v-if="isSidebarOpen">
- <div v-if="boardListType === $options.label">
+ <div v-if="boardListType === ListType.label">
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
<gl-label
:title="activeListLabel.title"
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index bf3dc5c608f..6d5a13be3ac 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,23 +1,26 @@
-/* eslint-disable no-new */
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/shared/boards/components/_sidebar.html.haml for its
+// template.
+/* eslint-disable no-new, @gitlab/no-runtime-template-compiler */
+import { GlLabel } from '@gitlab/ui';
import $ from 'jquery';
import Vue from 'vue';
-import { GlLabel } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '~/flash';
-import { sprintf, __ } from '~/locale';
-import Sidebar from '~/right_sidebar';
-import eventHub from '~/sidebar/event_hub';
import DueDateSelectors from '~/due_date_select';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { sprintf, __ } from '~/locale';
+import MilestoneSelect from '~/milestone_select';
+import Sidebar from '~/right_sidebar';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
import Assignees from '~/sidebar/components/assignees/assignees.vue';
+import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
-import MilestoneSelect from '~/milestone_select';
-import RemoveBtn from './sidebar/remove_issue.vue';
+import eventHub from '~/sidebar/event_hub';
import boardsStore from '../stores/boards_store';
-import { isScopedLabel } from '~/lib/utils/common_utils';
+import RemoveBtn from './sidebar/remove_issue.vue';
export default Vue.extend({
components: {
@@ -29,6 +32,7 @@ export default Vue.extend({
RemoveBtn,
Subscriptions,
TimeTracker,
+ SidebarAssigneesWidget,
},
props: {
currentUser: {
@@ -75,12 +79,6 @@ export default Vue.extend({
detail: {
handler() {
if (this.issue.id !== this.detail.issue.id) {
- $('.block.assignee')
- .find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
- .each((i, el) => {
- $(el).remove();
- });
-
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('deprecatedJQueryDropdown').clearMenu();
});
@@ -93,18 +91,9 @@ export default Vue.extend({
},
},
created() {
- // Get events from deprecatedJQueryDropdown
- eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$on('sidebar.addAssignee', this.addAssignee);
- eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
eventHub.$on('sidebar.closeAll', this.closeSidebar);
},
beforeDestroy() {
- eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$off('sidebar.addAssignee', this.addAssignee);
- eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
eventHub.$off('sidebar.closeAll', this.closeSidebar);
},
mounted() {
@@ -118,34 +107,8 @@ export default Vue.extend({
closeSidebar() {
this.detail.issue = {};
},
- assignSelf() {
- // Notify gl dropdown that we are now assigning to current user
- this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
-
- this.addAssignee(this.currentUser);
- this.saveAssignees();
- },
- removeAssignee(a) {
- boardsStore.detail.issue.removeAssignee(a);
- },
- addAssignee(a) {
- boardsStore.detail.issue.addAssignee(a);
- },
- removeAllAssignees() {
- boardsStore.detail.issue.removeAllAssignees();
- },
- saveAssignees() {
- this.loadingAssignees = true;
-
- boardsStore.detail.issue
- .update()
- .then(() => {
- this.loadingAssignees = false;
- })
- .catch(() => {
- this.loadingAssignees = false;
- Flash(__('An error occurred while saving assignees'));
- });
+ setAssignees(data) {
+ boardsStore.detail.issue.setAssignees(data.issueSetAssignees.issue.assignees.nodes);
},
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index fcd1c3fdceb..2a064aaa885 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -1,5 +1,4 @@
<script>
-import { throttle } from 'lodash';
import {
GlLoadingIcon,
GlSearchBoxByType,
@@ -9,14 +8,16 @@ import {
GlDropdownItem,
GlModalDirective,
} from '@gitlab/ui';
+import { throttle } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import projectQuery from '../graphql/project_boards.query.graphql';
+import eventHub from '../eventhub';
import groupQuery from '../graphql/group_boards.query.graphql';
+import projectQuery from '../graphql/project_boards.query.graphql';
-import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
@@ -35,6 +36,7 @@ export default {
directives: {
GlModalDirective,
},
+ inject: ['fullPath', 'recentBoardsEndpoint'],
props: {
currentBoard: {
type: Object,
@@ -99,12 +101,11 @@ export default {
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
- state: boardsStore.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
- store: boardsStore,
filterTerm: '',
+ currentPage: '',
};
},
computed: {
@@ -114,16 +115,13 @@ export default {
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
- currentPage() {
- return this.state.currentPage;
- },
filteredBoards() {
return this.boards.filter((board) =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
board() {
- return this.state.currentBoard;
+ return this.currentBoard;
},
showDelete() {
return this.boards.length > 1;
@@ -148,11 +146,17 @@ export default {
},
},
created() {
- boardsStore.setCurrentBoard(this.currentBoard);
+ eventHub.$on('showBoardModal', this.showPage);
+ },
+ beforeDestroy() {
+ eventHub.$off('showBoardModal', this.showPage);
},
methods: {
showPage(page) {
- boardsStore.showPage(page);
+ this.currentPage = page;
+ },
+ cancel() {
+ this.showPage('');
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
@@ -161,7 +165,7 @@ export default {
this.$apollo.addSmartQuery('boards', {
variables() {
- return { fullPath: this.state.endpoints.fullPath };
+ return { fullPath: this.fullPath };
},
query() {
return this.groupId ? groupQuery : projectQuery;
@@ -179,8 +183,10 @@ export default {
});
this.loadingRecentBoards = true;
- boardsStore
- .recentBoards()
+ // Follow up to fetch recent boards using GraphQL
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/300985
+ axios
+ .get(this.recentBoardsEndpoint)
.then((res) => {
this.recentBoards = res.data;
})
@@ -346,6 +352,8 @@ export default {
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard"
+ :current-page="currentPage"
+ @cancel="cancel"
/>
</span>
</div>
diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
new file mode 100644
index 00000000000..33ad46a0d29
--- /dev/null
+++ b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
@@ -0,0 +1,357 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlModalDirective,
+} from '@gitlab/ui';
+import { throttle } from 'lodash';
+
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import httpStatusCodes from '~/lib/utils/http_status';
+
+import groupQuery from '../graphql/group_boards.query.graphql';
+import projectQuery from '../graphql/project_boards.query.graphql';
+
+import boardsStore from '../stores/boards_store';
+import BoardForm from './board_form.vue';
+
+const MIN_BOARDS_TO_VIEW_RECENT = 10;
+
+export default {
+ name: 'BoardsSelector',
+ components: {
+ BoardForm,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ props: {
+ currentBoard: {
+ type: Object,
+ required: true,
+ },
+ throttleDuration: {
+ type: Number,
+ default: 200,
+ required: false,
+ },
+ boardBaseUrl: {
+ type: String,
+ required: true,
+ },
+ hasMissingBoards: {
+ type: Boolean,
+ required: true,
+ },
+ canAdminBoard: {
+ type: Boolean,
+ required: true,
+ },
+ multipleIssueBoardsAvailable: {
+ type: Boolean,
+ required: true,
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ labelsWebUrl: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ groupId: {
+ type: Number,
+ required: true,
+ },
+ scopedIssueBoardFeatureEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ weights: {
+ type: Array,
+ required: true,
+ },
+ enabledScopedLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ hasScrollFade: false,
+ loadingBoards: 0,
+ loadingRecentBoards: false,
+ scrollFadeInitialized: false,
+ boards: [],
+ recentBoards: [],
+ state: boardsStore.state,
+ throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
+ contentClientHeight: 0,
+ maxPosition: 0,
+ store: boardsStore,
+ filterTerm: '',
+ };
+ },
+ computed: {
+ parentType() {
+ return this.groupId ? 'group' : 'project';
+ },
+ loading() {
+ return this.loadingRecentBoards || Boolean(this.loadingBoards);
+ },
+ currentPage() {
+ return this.state.currentPage;
+ },
+ filteredBoards() {
+ return this.boards.filter((board) =>
+ board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
+ );
+ },
+ board() {
+ return this.state.currentBoard;
+ },
+ showDelete() {
+ return this.boards.length > 1;
+ },
+ scrollFadeClass() {
+ return {
+ 'fade-out': !this.hasScrollFade,
+ };
+ },
+ showRecentSection() {
+ return (
+ this.recentBoards.length &&
+ this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
+ !this.filterTerm.length
+ );
+ },
+ },
+ watch: {
+ filteredBoards() {
+ this.scrollFadeInitialized = false;
+ this.$nextTick(this.setScrollFade);
+ },
+ },
+ created() {
+ boardsStore.setCurrentBoard(this.currentBoard);
+ },
+ methods: {
+ showPage(page) {
+ boardsStore.showPage(page);
+ },
+ cancel() {
+ this.showPage('');
+ },
+ loadBoards(toggleDropdown = true) {
+ if (toggleDropdown && this.boards.length > 0) {
+ return;
+ }
+
+ this.$apollo.addSmartQuery('boards', {
+ variables() {
+ return { fullPath: this.state.endpoints.fullPath };
+ },
+ query() {
+ return this.groupId ? groupQuery : projectQuery;
+ },
+ loadingKey: 'loadingBoards',
+ update(data) {
+ if (!data?.[this.parentType]) {
+ return [];
+ }
+ return data[this.parentType].boards.edges.map(({ node }) => ({
+ id: getIdFromGraphQLId(node.id),
+ name: node.name,
+ }));
+ },
+ });
+
+ this.loadingRecentBoards = true;
+ boardsStore
+ .recentBoards()
+ .then((res) => {
+ this.recentBoards = res.data;
+ })
+ .catch((err) => {
+ /**
+ * If user is unauthorized we'd still want to resolve the
+ * request to display all boards.
+ */
+ if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
+ this.recentBoards = []; // recent boards are empty
+ return;
+ }
+ throw err;
+ })
+ .then(() => this.$nextTick()) // Wait for boards list in DOM
+ .then(() => {
+ this.setScrollFade();
+ })
+ .catch(() => {})
+ .finally(() => {
+ this.loadingRecentBoards = false;
+ });
+ },
+ isScrolledUp() {
+ const { content } = this.$refs;
+
+ if (!content) {
+ return false;
+ }
+
+ const currentPosition = this.contentClientHeight + content.scrollTop;
+
+ return currentPosition < this.maxPosition;
+ },
+ initScrollFade() {
+ const { content } = this.$refs;
+
+ if (!content) {
+ return;
+ }
+
+ this.scrollFadeInitialized = true;
+
+ this.contentClientHeight = content.clientHeight;
+ this.maxPosition = content.scrollHeight;
+ },
+ setScrollFade() {
+ if (!this.scrollFadeInitialized) this.initScrollFade();
+
+ this.hasScrollFade = this.isScrolledUp();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="boards-switcher js-boards-selector gl-mr-3">
+ <span class="boards-selector-wrapper js-boards-selector-wrapper">
+ <gl-dropdown
+ data-qa-selector="boards_dropdown"
+ toggle-class="dropdown-menu-toggle js-dropdown-toggle"
+ menu-class="flex-column dropdown-extended-height"
+ :text="board.name"
+ @show="loadBoards"
+ >
+ <p class="gl-new-dropdown-header-top" @mousedown.prevent>
+ {{ s__('IssueBoards|Switch board') }}
+ </p>
+ <gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
+
+ <div
+ v-if="!loading"
+ ref="content"
+ data-qa-selector="boards_dropdown_content"
+ class="dropdown-content flex-fill"
+ @scroll.passive="throttledSetScrollFade"
+ >
+ <gl-dropdown-item
+ v-show="filteredBoards.length === 0"
+ class="gl-pointer-events-none text-secondary"
+ >
+ {{ s__('IssueBoards|No matching boards found') }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-section-header v-if="showRecentSection">
+ {{ __('Recent') }}
+ </gl-dropdown-section-header>
+
+ <template v-if="showRecentSection">
+ <gl-dropdown-item
+ v-for="recentBoard in recentBoards"
+ :key="`recent-${recentBoard.id}`"
+ class="js-dropdown-item"
+ :href="`${boardBaseUrl}/${recentBoard.id}`"
+ >
+ {{ recentBoard.name }}
+ </gl-dropdown-item>
+ </template>
+
+ <gl-dropdown-divider v-if="showRecentSection" />
+
+ <gl-dropdown-section-header v-if="showRecentSection">
+ {{ __('All') }}
+ </gl-dropdown-section-header>
+
+ <gl-dropdown-item
+ v-for="otherBoard in filteredBoards"
+ :key="otherBoard.id"
+ class="js-dropdown-item"
+ :href="`${boardBaseUrl}/${otherBoard.id}`"
+ >
+ {{ otherBoard.name }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
+ {{
+ s__(
+ 'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
+ )
+ }}
+ </gl-dropdown-item>
+ </div>
+
+ <div
+ v-show="filteredBoards.length > 0"
+ class="dropdown-content-faded-mask"
+ :class="scrollFadeClass"
+ ></div>
+
+ <gl-loading-icon v-if="loading" />
+
+ <div v-if="canAdminBoard">
+ <gl-dropdown-divider />
+
+ <gl-dropdown-item
+ v-if="multipleIssueBoardsAvailable"
+ v-gl-modal-directive="'board-config-modal'"
+ data-qa-selector="create_new_board_button"
+ @click.prevent="showPage('new')"
+ >
+ {{ s__('IssueBoards|Create new board') }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item
+ v-if="showDelete"
+ v-gl-modal-directive="'board-config-modal'"
+ class="text-danger js-delete-board"
+ @click.prevent="showPage('delete')"
+ >
+ {{ s__('IssueBoards|Delete board') }}
+ </gl-dropdown-item>
+ </div>
+ </gl-dropdown>
+
+ <board-form
+ v-if="currentPage"
+ :labels-path="labelsPath"
+ :labels-web-url="labelsWebUrl"
+ :project-id="projectId"
+ :group-id="groupId"
+ :can-admin-board="canAdminBoard"
+ :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
+ :weights="weights"
+ :enable-scoped-labels="enabledScopedLabels"
+ :current-board="currentBoard"
+ :current-page="state.currentPage"
+ @cancel="cancel"
+ />
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 457d0d4dcd6..e5ea30df767 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,17 +1,17 @@
<script>
+import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import { ListType } from '../constants';
+import eventHub from '../eventhub';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
-import eventHub from '../eventhub';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { ListType } from '../constants';
-import { updateHistory } from '~/lib/utils/url_utility';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
index 75cf1f0b9e1..069cc2cda22 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
@@ -1,15 +1,15 @@
<script>
+import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapState } from 'vuex';
-import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
+import { isScopedLabel } from '~/lib/utils/common_utils';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import boardsStore from '../stores/boards_store';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate_deprecated.vue';
-import boardsStore from '../stores/boards_store';
-import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index fb45de6e14d..7e3f36c8a17 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -1,13 +1,13 @@
<script>
-import dateFormat from 'dateformat';
import { GlTooltip, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
+import dateFormat from 'dateformat';
import {
getDayDifference,
getTimeago,
dateInWords,
parsePikadayDate,
} from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index f6b00b695da..42d187b9b40 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltip, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
export default {
i18n: {
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index eb2db260717..486b012e3d2 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
index 56a0fde5a91..2fb38a549f3 100644
--- a/app/assets/javascripts/boards/components/modal/filters.js
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -1,5 +1,5 @@
-import FilteredSearchBoards from '../../filtered_search_boards';
import FilteredSearchContainer from '../../../filtered_search/container';
+import FilteredSearchBoards from '../../filtered_search_boards';
export default {
name: 'modal-filters',
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index 10c29977cae..05e1219bc70 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -3,10 +3,10 @@ import { GlButton } from '@gitlab/ui';
import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __, n__ } from '../../../locale';
-import ListsDropdown from './lists_dropdown.vue';
-import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store';
+import ModalStore from '../../stores/modal_store';
+import ListsDropdown from './lists_dropdown.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 3e96ecca24c..c3a71e7177a 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -2,10 +2,10 @@
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
+import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
import ModalFilters from './filters';
import ModalTabs from './tabs.vue';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 84d687a46b9..5af90c1ee66 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -1,13 +1,13 @@
<script>
/* global ListIssue */
import { GlLoadingIcon } from '@gitlab/ui';
-import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '~/boards/stores/boards_store';
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+import ModalStore from '../../stores/modal_store';
+import EmptyState from './empty_state.vue';
+import ModalFooter from './footer.vue';
import ModalHeader from './header.vue';
import ModalList from './list.vue';
-import ModalFooter from './footer.vue';
-import EmptyState from './empty_state.vue';
-import ModalStore from '../../stores/modal_store';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index 219263bd9b9..bf69f8140d5 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -1,6 +1,6 @@
<script>
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { GlIcon } from '@gitlab/ui';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue';
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
index fe10e7fb856..2065568d275 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
-import ModalStore from '../../stores/modal_store';
import boardsStore from '../../stores/boards_store';
+import ModalStore from '../../stores/modal_store';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
index b066fb25360..0b717f516db 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
-import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2bc54155163..2fd16f06455 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,15 +1,15 @@
/* eslint-disable func-names, no-new */
import $ from 'jquery';
-import { __ } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
+import store from '~/boards/stores';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
import CreateLabelDropdown from '../../create_label';
-import boardsStore from '../stores/boards_store';
import { fullLabelId } from '../boards_util';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import store from '~/boards/stores';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import boardsStore from '../stores/boards_store';
function shouldCreateListGraphQL(label) {
return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
@@ -51,16 +51,27 @@ export default function initNewListDropdown() {
initDeprecatedJQueryDropdown($dropdownToggle, {
data(term, callback) {
- axios
- .get($dropdownToggle.attr('data-list-labels-path'))
- .then(({ data }) => callback(data))
- .catch(() => {
- $dropdownToggle.data('bs.dropdown').hide();
- flash(__('Error fetching labels.'));
- });
+ const reqFailed = () => {
+ $dropdownToggle.data('bs.dropdown').hide();
+ flash(__('Error fetching labels.'));
+ };
+
+ if (store.getters.shouldUseGraphQL) {
+ store
+ .dispatch('fetchLabels')
+ .then((data) => callback(data))
+ .catch(reqFailed);
+ } else {
+ axios
+ .get($dropdownToggle.attr('data-list-labels-path'))
+ .then(({ data }) => callback(data))
+ .catch(reqFailed);
+ }
},
renderRow(label) {
- const active = boardsStore.findListByLabelId(label.id);
+ const active = store.getters.shouldUseGraphQL
+ ? store.getters.getListByLabelId(label.id)
+ : boardsStore.findListByLabelId(label.id);
const $li = $('<li />');
const $a = $('<a />', {
class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
@@ -87,7 +98,7 @@ export default function initNewListDropdown() {
e.preventDefault();
if (shouldCreateListGraphQL(label)) {
- store.dispatch('createList', { labelId: fullLabelId(label) });
+ store.dispatch('createList', { labelId: label.id });
} else if (!boardsStore.findListByLabelId(label.id)) {
boardsStore.new({
title: label.title,
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 04699d0d3a4..cfc1752a828 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlDropdown,
GlDropdownItem,
@@ -8,6 +7,7 @@ import {
GlIntersectionObserver,
GlLoadingIcon,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import { ListType } from '../constants';
diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue
index a043dc575ca..5605e9945ea 100644
--- a/app/assets/javascripts/boards/components/project_select_deprecated.vue
+++ b/app/assets/javascripts/boards/components/project_select_deprecated.vue
@@ -6,11 +6,11 @@ import {
GlSearchBoxByType,
GlLoadingIcon,
} from '@gitlab/ui';
-import eventHub from '../eventhub';
import { s__ } from '~/locale';
-import Api from '../../api';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
+import Api from '../../api';
import { ListType } from '../constants';
+import eventHub from '../eventhub';
export default {
name: 'ProjectSelect',
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 4a664d5beef..6d928337396 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
@@ -1,9 +1,9 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlButton, GlDatepicker } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import createFlash from '~/flash';
+import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
export default {
@@ -88,15 +88,13 @@ export default {
</gl-button>
</div>
</template>
- <template>
- <gl-datepicker
- ref="datePicker"
- :value="parsedDueDate"
- show-clear-button
- @input="setDueDate"
- @clear="setDueDate(null)"
- />
- </template>
+ <gl-datepicker
+ ref="datePicker"
+ :value="parsedDueDate"
+ show-clear-button
+ @input="setDueDate"
+ @clear="setDueDate(null)"
+ />
</board-editable-item>
</template>
<style>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
index d0e641daf5c..95864bd62a7 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
@@ -1,11 +1,11 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-import { joinPaths } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
+import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
export default {
components: {
@@ -136,36 +136,34 @@ export default {
<template #collapsed>
<span class="gl-text-gray-800">{{ issue.referencePath }}</span>
</template>
- <template>
- <gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
- {{ $options.i18n.reviewYourChanges }}
- </gl-alert>
- <gl-form @submit.prevent="setTitle">
- <gl-form-group :invalid-feedback="$options.i18n.invalidFeedback" :state="validationState">
- <gl-form-input
- v-model="title"
- v-autofocusonshow
- :placeholder="$options.i18n.issueTitlePlaceholder"
- :state="validationState"
- />
- </gl-form-group>
+ <gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
+ {{ $options.i18n.reviewYourChanges }}
+ </gl-alert>
+ <gl-form @submit.prevent="setTitle">
+ <gl-form-group :invalid-feedback="$options.i18n.invalidFeedback" :state="validationState">
+ <gl-form-input
+ v-model="title"
+ v-autofocusonshow
+ :placeholder="$options.i18n.issueTitlePlaceholder"
+ :state="validationState"
+ />
+ </gl-form-group>
- <div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5">
- <gl-button
- variant="success"
- size="small"
- data-testid="submit-button"
- :disabled="!title"
- @click="setTitle"
- >
- {{ $options.i18n.submitButton }}
- </gl-button>
+ <div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5">
+ <gl-button
+ variant="success"
+ size="small"
+ data-testid="submit-button"
+ :disabled="!title"
+ @click="setTitle"
+ >
+ {{ $options.i18n.submitButton }}
+ </gl-button>
- <gl-button size="small" data-testid="cancel-button" @click="cancel">
- {{ $options.i18n.cancelButton }}
- </gl-button>
- </div>
- </gl-form>
- </template>
+ <gl-button size="small" data-testid="cancel-button" @click="cancel">
+ {{ $options.i18n.cancelButton }}
+ </gl-button>
+ </div>
+ </gl-form>
</board-editable-item>
</template>
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 dcf769e6fe5..55b1596ee18 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
@@ -1,12 +1,12 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlLabel } from '@gitlab/ui';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import { isScopedLabel } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
export default {
components: {
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
index 144a81f009b..829f1c72806 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -1,5 +1,4 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import {
GlDropdown,
GlDropdownItem,
@@ -8,11 +7,11 @@ import {
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
-import { fetchPolicies } from '~/lib/graphql';
+import { mapGetters, mapActions } from 'vuex';
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';
+import projectMilestones from '../../graphql/project_milestones.query.graphql';
export default {
components: {
@@ -34,22 +33,21 @@ export default {
},
apollo: {
milestones: {
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
- query: groupMilestones,
+ query: projectMilestones,
debounce: 250,
skip() {
return !this.edit;
},
variables() {
return {
- fullPath: this.groupFullPath,
+ fullPath: this.projectPath,
searchTitle: this.searchTitle,
state: 'active',
- includeDescendants: true,
+ includeAncestors: true,
};
},
update(data) {
- const edges = data?.group?.milestones?.edges ?? [];
+ const edges = data?.project?.milestones?.edges ?? [];
return edges.map((item) => item.node);
},
error() {
@@ -74,21 +72,20 @@ export default {
return this.activeIssue.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();
},
+ handleClose() {
+ this.edit = false;
+ this.$refs.sidebarItem.collapse();
+ },
async setMilestone(milestoneId) {
this.loading = true;
this.searchTitle = '';
- this.$refs.sidebarItem.collapse();
+ this.handleClose();
try {
const input = { milestoneId, projectPath: this.projectPath };
@@ -117,45 +114,44 @@ export default {
:title="$options.i18n.milestone"
:loading="loading"
@open="handleOpen()"
- @close="edit = false"
+ @close="handleClose"
>
<template v-if="hasMilestone" #collapsed>
<strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong>
</template>
- <template>
- <gl-dropdown
- ref="dropdown"
- :text="dropdownText"
- :header-text="$options.i18n.assignMilestone"
- block
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropdownText"
+ :header-text="$options.i18n.assignMilestone"
+ block
+ @hide="handleClose"
+ >
+ <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="!activeIssue.milestone"
+ @click="setMilestone(null)"
>
- <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
+ {{ $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
- data-testid="no-milestone-item"
+ v-for="milestone in milestones"
+ :key="milestone.id"
:is-check-item="true"
- :is-checked="!activeIssue.milestone"
- @click="setMilestone(null)"
+ :is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id"
+ data-testid="milestone-item"
+ @click="setMilestone(milestone.id)"
>
- {{ $options.i18n.noMilestone }}
+ {{ milestone.title }}
</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="activeIssue.milestone && milestone.id === activeIssue.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>
+ </template>
+ <gl-dropdown-text v-else data-testid="no-milestones-found">
+ {{ $options.i18n.noMilestonesFound }}
+ </gl-dropdown-text>
+ </gl-dropdown>
</board-editable-item>
</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
index 4aa8d2f55e4..aa4fdcf9a94 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlToggle } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue
new file mode 100644
index 00000000000..74805f8a681
--- /dev/null
+++ b/app/assets/javascripts/boards/components/toggle_focus.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { hide } from '~/tooltips';
+
+export default {
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ issueBoardsContentSelector: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isFullscreen: false,
+ };
+ },
+ methods: {
+ toggleFocusMode() {
+ hide(this.$refs.toggleFocusModeButton);
+
+ const issueBoardsContent = document.querySelector(this.issueBoardsContentSelector);
+ issueBoardsContent.classList.toggle('is-focused');
+
+ this.isFullscreen = !this.isFullscreen;
+ },
+ },
+ i18n: {
+ toggleFocusMode: __('Toggle focus mode'),
+ },
+};
+</script>
+
+<template>
+ <div class="board-extra-actions gl-ml-3 gl-display-flex gl-align-items-center">
+ <gl-button
+ ref="toggleFocusModeButton"
+ v-gl-tooltip
+ :icon="isFullscreen ? 'minimize' : 'maximize'"
+ class="js-focus-mode-btn"
+ data-qa-selector="focus_mode_button"
+ :title="$options.i18n.toggleFocusMode"
+ @click="toggleFocusMode"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 9264fac5eda..3ab89b2c9da 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const BoardType = {
project: 'project',
group: 'group',
@@ -6,16 +8,34 @@ export const BoardType = {
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
+ iteration: 'iteration',
backlog: 'backlog',
closed: 'closed',
label: 'label',
};
+export const ListTypeTitles = {
+ assignee: __('Assignee'),
+ milestone: __('Milestone'),
+ iteration: __('Iteration'),
+ label: __('Label'),
+};
+
+export const formType = {
+ new: 'new',
+ delete: 'delete',
+ edit: 'edit',
+};
+
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
export const LIST = 'list';
+export const NOT_FILTER = 'not[';
+
+export const flashAnimationDuration = 2000;
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 94b35aadaf1..66580bdd30f 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,10 +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 FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import { updateHistory } from '~/lib/utils/url_utility';
import FilteredSearchContainer from '../filtered_search/container';
-import boardsStore from './stores/boards_store';
import vuexstore from './stores';
-import { updateHistory } from '~/lib/utils/url_utility';
+import boardsStore from './stores/boards_store';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js
index c35dedde71b..1745ab3bab4 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import dateFormat from 'dateformat';
+import Vue from 'vue';
Vue.filter('due-date', (value) => {
const date = new Date(value);
diff --git a/app/assets/javascripts/boards/graphql/group_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
index f2ab12ef4a7..776530ebb83 100644
--- a/app/assets/javascripts/boards/graphql/group_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
@@ -1,11 +1,11 @@
query groupMilestones(
$fullPath: ID!
$state: MilestoneStateEnum
- $includeDescendants: Boolean
+ $includeAncestors: Boolean
$searchTitle: String
) {
- group(fullPath: $fullPath) {
- milestones(state: $state, includeDescendants: $includeDescendants, searchTitle: $searchTitle) {
+ project(fullPath: $fullPath) {
+ milestones(state: $state, includeAncestors: $includeAncestors, searchTitle: $searchTitle) {
edges {
node {
id
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index ef70a094f7c..859295318ed 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { mapActions, mapGetters } from 'vuex';
import 'ee_else_ce/boards/models/issue';
@@ -6,41 +7,39 @@ import 'ee_else_ce/boards/models/list';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
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 {
setWeightFetchingState,
setEpicFetchingState,
getMilestoneTitle,
getBoardsModalData,
} from 'ee_else_ce/boards/ee_functions';
-
-import VueApollo from 'vue-apollo';
+import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
+import toggleLabels from 'ee_else_ce/boards/toggle_labels';
+import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardExtraActions from '~/boards/components/board_extra_actions.vue';
-import createDefaultClient from '~/lib/graphql';
-import { deprecatedCreateFlash as Flash } from '~/flash';
-import { __ } from '~/locale';
import './models/label';
import './models/assignee';
-
-import toggleFocusMode from '~/boards/toggle_focus';
-import FilteredSearchBoards from '~/boards/filtered_search_boards';
-import eventHub from '~/boards/eventhub';
-import sidebarEventHub from '~/sidebar/event_hub';
import '~/boards/models/milestone';
import '~/boards/models/project';
+import '~/boards/filters/due_date_filters';
+import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
+import eventHub from '~/boards/eventhub';
+import FilteredSearchBoards from '~/boards/filtered_search_boards';
+import modalMixin from '~/boards/mixins/modal_mixins';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
-import modalMixin from '~/boards/mixins/modal_mixins';
-import '~/boards/filters/due_date_filters';
-import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
+import toggleFocusMode from '~/boards/toggle_focus';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import createDefaultClient from '~/lib/graphql';
import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import sidebarEventHub from '~/sidebar/event_hub';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo);
@@ -73,6 +72,7 @@ export default () => {
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
}
+ // eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
el: $boardApp,
components: {
@@ -86,7 +86,7 @@ export default () => {
groupId: Number($boardApp.dataset.groupId),
rootPath: $boardApp.dataset.rootPath,
currentUserId: gon.current_user_id || null,
- canUpdate: $boardApp.dataset.canUpdate,
+ canUpdate: parseBoolean($boardApp.dataset.canUpdate),
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
@@ -275,7 +275,7 @@ export default () => {
},
});
- // eslint-disable-next-line no-new
+ // eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler
new Vue({
el: document.getElementById('js-add-list'),
data: {
@@ -287,6 +287,21 @@ export default () => {
},
});
+ const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
+ if (createColumnTriggerEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: createColumnTriggerEl,
+ components: {
+ BoardAddNewColumnTrigger,
+ },
+ store,
+ render(createElement) {
+ return createElement('board-add-new-column-trigger');
+ },
+ });
+ }
+
boardConfigToggle(boardsStore);
const issueBoardsModal = document.getElementById('js-add-issues-btn');
@@ -341,5 +356,6 @@ export default () => {
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
+ recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
});
};
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 1e77326ba9c..46d1239457d 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -6,8 +6,8 @@
import axios from '~/lib/utils/axios_utils';
import './label';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import IssueProject from './project';
import boardsStore from '../stores/boards_store';
+import IssueProject from './project';
class ListIssue {
constructor(obj) {
@@ -53,6 +53,10 @@ class ListIssue {
return boardsStore.findIssueAssignee(this, findAssignee);
}
+ setAssignees(assignees) {
+ boardsStore.setIssueAssignees(this, assignees);
+ }
+
removeAssignee(removeAssignee) {
boardsStore.removeIssueAssignee(this, removeAssignee);
}
diff --git a/app/assets/javascripts/boards/models/iteration.js b/app/assets/javascripts/boards/models/iteration.js
new file mode 100644
index 00000000000..b7bdc204f7c
--- /dev/null
+++ b/app/assets/javascripts/boards/models/iteration.js
@@ -0,0 +1,9 @@
+export default class ListIteration {
+ constructor(obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ this.state = obj.state;
+ this.webUrl = obj.web_url || obj.webUrl;
+ this.description = obj.description;
+ }
+}
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index be02ac7b889..6c6e2522d92 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,9 +1,10 @@
/* eslint-disable class-methods-use-this */
-import { __ } from '~/locale';
-import ListLabel from './label';
-import ListAssignee from './assignee';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { __ } from '~/locale';
import boardsStore from '../stores/boards_store';
+import ListAssignee from './assignee';
+import ListIteration from './iteration';
+import ListLabel from './label';
import ListMilestone from './milestone';
import 'ee_else_ce/boards/models/issue';
@@ -43,6 +44,7 @@ class List {
this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = !obj.collapsed;
this.page = 1;
+ this.highlighted = obj.highlighted;
this.loading = true;
this.loadingMore = false;
this.issues = obj.issues || [];
@@ -57,6 +59,9 @@ class List {
} else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
this.title = this.milestone.title;
+ } else if (IS_EE && obj.iteration) {
+ this.iteration = new ListIteration(obj.iteration);
+ this.title = this.iteration.title;
}
// doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 738c8fb927e..fa58af24ba2 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -1,8 +1,12 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { mapGetters } from 'vuex';
+import BoardsSelector from '~/boards/components/boards_selector.vue';
+import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
+import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
-import BoardsSelector from '~/boards/components/boards_selector.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
Vue.use(VueApollo);
@@ -16,11 +20,15 @@ export default (params = {}) => {
el: boardsSwitcherElement,
components: {
BoardsSelector,
+ BoardsSelectorDeprecated,
},
+ mixins: [glFeatureFlagMixin()],
apolloProvider,
+ store,
provide: {
fullPath: params.fullPath,
rootPath: params.rootPath,
+ recentBoardsEndpoint: params.recentBoardsEndpoint,
},
data() {
const { dataset } = boardsSwitcherElement;
@@ -39,8 +47,16 @@ export default (params = {}) => {
return { boardsSelectorProps };
},
+ computed: {
+ ...mapGetters(['shouldUseGraphQL']),
+ },
render(createElement) {
- return createElement(BoardsSelector, {
+ if (this.shouldUseGraphQL) {
+ return createElement(BoardsSelector, {
+ props: this.boardsSelectorProps,
+ });
+ }
+ return createElement(BoardsSelectorDeprecated, {
props: this.boardsSelectorProps,
});
},
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 1d34f21798a..a7cf1e9e647 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,11 +1,9 @@
import { pick } from 'lodash';
-
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
-import createGqClient, { fetchPolicies } from '~/lib/graphql';
+import { BoardType, ListType, inactiveId, flashAnimationDuration } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
-import { BoardType, ListType, inactiveId } from '~/boards/constants';
-import * as types from './mutation_types';
import {
formatBoardLists,
formatListIssues,
@@ -14,23 +12,22 @@ import {
formatIssue,
formatIssueInput,
updateListPosition,
+ transformNotFilters,
} from '../boards_util';
-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 updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
+import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
-import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
+import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
-import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
+import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
+import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from '../graphql/issue_set_title.mutation.graphql';
-import groupProjectsQuery from '../graphql/group_projects.query.graphql';
+import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
+import * as types from './mutation_types';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -66,6 +63,7 @@ export default {
'releaseTag',
'search',
]);
+ filterParams.not = transformNotFilters(filters);
commit(types.SET_FILTERS, filterParams);
},
@@ -108,9 +106,31 @@ export default {
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
},
- createList: ({ state, commit, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
+ highlightList: ({ commit, state }, listId) => {
+ if ([ListType.backlog, ListType.closed].includes(state.boardLists[listId].listType)) {
+ return;
+ }
+
+ commit(types.ADD_LIST_TO_HIGHLIGHTED_LISTS, listId);
+
+ setTimeout(() => {
+ commit(types.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS, listId);
+ }, flashAnimationDuration);
+ },
+
+ createList: (
+ { state, commit, dispatch, getters },
+ { backlog, labelId, milestoneId, assigneeId },
+ ) => {
const { boardId } = state;
+ const existingList = getters.getListByLabelId(labelId);
+
+ if (existingList) {
+ dispatch('highlightList', existingList.id);
+ return;
+ }
+
gqlClient
.mutate({
mutation: createBoardListMutation,
@@ -128,6 +148,7 @@ export default {
} else {
const list = data.boardListCreate?.list;
dispatch('addList', list);
+ dispatch('highlightList', list.id);
}
})
.catch(() => commit(types.CREATE_LIST_FAILURE));
@@ -153,10 +174,10 @@ export default {
variables,
})
.then(({ data }) => {
- const labels = data[boardType]?.labels;
- return labels.nodes;
- })
- .catch(() => commit(types.RECEIVE_LABELS_FAILURE));
+ const labels = data[boardType]?.labels.nodes;
+ commit(types.RECEIVE_LABELS_SUCCESS, labels);
+ return labels;
+ });
},
moveList: (
@@ -308,34 +329,11 @@ export default {
},
setAssignees: ({ commit, getters }, assigneeUsernames) => {
- commit(types.SET_ASSIGNEE_LOADING, true);
-
- return gqlClient
- .mutate({
- mutation: updateAssigneesMutation,
- variables: {
- iid: getters.activeIssue.iid,
- projectPath: getters.activeIssue.referencePath.split('#')[0],
- assigneeUsernames,
- },
- })
- .then(({ data }) => {
- const { nodes } = data.issueSetAssignees?.issue?.assignees || [];
-
- commit('UPDATE_ISSUE_BY_ID', {
- issueId: getters.activeIssue.id,
- prop: 'assignees',
- value: nodes,
- });
-
- return nodes;
- })
- .catch(() => {
- createFlash({ message: __('An error occurred while updating assignees.') });
- })
- .finally(() => {
- commit(types.SET_ASSIGNEE_LOADING, false);
- });
+ commit('UPDATE_ISSUE_BY_ID', {
+ issueId: getters.activeIssue.id,
+ prop: 'assignees',
+ value: assigneeUsernames,
+ });
},
setActiveIssueMilestone: async ({ commit, getters }, input) => {
@@ -534,6 +532,21 @@ export default {
commit(types.SET_SELECTED_PROJECT, project);
},
+ toggleBoardItemMultiSelection: ({ commit, state }, boardItem) => {
+ const { selectedBoardItems } = state;
+ const index = selectedBoardItems.indexOf(boardItem);
+
+ if (index === -1) {
+ commit(types.ADD_BOARD_ITEM_TO_SELECTION, boardItem);
+ } else {
+ commit(types.REMOVE_BOARD_ITEM_FROM_SELECTION, boardItem);
+ }
+ },
+
+ setAddColumnFormVisibility: ({ commit }, visible) => {
+ commit(types.SET_ADD_COLUMN_FORM_VISIBLE, visible);
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index f59530ddf8f..fbff736c7e1 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -4,22 +4,22 @@
import { sortBy } from 'lodash';
import Vue from 'vue';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import createDefaultClient from '~/lib/graphql';
+import axios from '~/lib/utils/axios_utils';
import {
urlParamsToObject,
getUrlParamsArray,
parseBoolean,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
-import createDefaultClient from '~/lib/graphql';
-import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub';
-import { ListType } from '../constants';
-import IssueProject from '../models/project';
-import ListLabel from '../models/label';
import ListAssignee from '../models/assignee';
+import ListLabel from '../models/label';
import ListMilestone from '../models/milestone';
+import IssueProject from '../models/project';
const PER_PAGE = 20;
export const gqlClient = createDefaultClient();
@@ -106,6 +106,11 @@ const boardsStore = {
list
.save()
.then(() => {
+ list.highlighted = true;
+ setTimeout(() => {
+ list.highlighted = false;
+ }, flashAnimationDuration);
+
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
@@ -117,7 +122,6 @@ const boardsStore = {
},
updateNewListDropdown(listId) {
- // eslint-disable-next-line no-unused-expressions
document
.querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
?.classList.remove('is-active');
@@ -720,6 +724,10 @@ const boardsStore = {
}
},
+ setIssueAssignees(issue, assignees) {
+ issue.assignees = [...assignees];
+ },
+
removeIssueLabels(issue, labels) {
labels.forEach(issue.removeLabel.bind(issue));
},
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index d72b5c6fb8e..cab97088bc6 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -17,12 +17,20 @@ export default {
return state.issues[state.activeId] || {};
},
+ groupPathForActiveIssue: (_, getters) => {
+ const { referencePath = '' } = getters.activeIssue;
+ return referencePath.slice(0, referencePath.indexOf('/'));
+ },
+
projectPathForActiveIssue: (_, getters) => {
- const referencePath = getters.activeIssue.referencePath || '';
+ const { referencePath = '' } = getters.activeIssue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
getListByLabelId: (state) => (labelId) => {
+ if (!labelId) {
+ return null;
+ }
return find(state.boardLists, (l) => l.label?.id === labelId);
},
diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js
index 471b952a212..0a87c6ab821 100644
--- a/app/assets/javascripts/boards/stores/index.js
+++ b/app/assets/javascripts/boards/stores/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import state from 'ee_else_ce/boards/stores/state';
-import getters from 'ee_else_ce/boards/stores/getters';
import actions from 'ee_else_ce/boards/stores/actions';
+import getters from 'ee_else_ce/boards/stores/getters';
import mutations from 'ee_else_ce/boards/stores/mutations';
+import state from 'ee_else_ce/boards/stores/state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 4697f39498a..a89e961ae2d 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -2,7 +2,7 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
-export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
+export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
@@ -40,3 +40,8 @@ export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
export const RECEIVE_GROUP_PROJECTS_SUCCESS = 'RECEIVE_GROUP_PROJECTS_SUCCESS';
export const RECEIVE_GROUP_PROJECTS_FAILURE = 'RECEIVE_GROUP_PROJECTS_FAILURE';
export const SET_SELECTED_PROJECT = 'SET_SELECTED_PROJECT';
+export const ADD_BOARD_ITEM_TO_SELECTION = 'ADD_BOARD_ITEM_TO_SELECTION';
+export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_SELECTION';
+export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
+export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
+export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 6c79b22d308..79c98c3d90c 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import { pull, union } from 'lodash';
+import Vue from 'vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { s__ } from '~/locale';
import { formatIssue, moveIssueListHelper } from '../boards_util';
import * as mutationTypes from './mutation_types';
-import { s__ } from '~/locale';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -63,8 +63,8 @@ export default {
state.error = s__('Boards|An error occurred while creating the list. Please try again.');
},
- [mutationTypes.RECEIVE_LABELS_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while fetching labels. Please reload the page.');
+ [mutationTypes.RECEIVE_LABELS_SUCCESS]: (state, labels) => {
+ state.labels = labels;
},
[mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: (state) => {
@@ -258,4 +258,28 @@ export default {
[mutationTypes.SET_SELECTED_PROJECT]: (state, project) => {
state.selectedProject = project;
},
+
+ [mutationTypes.ADD_BOARD_ITEM_TO_SELECTION]: (state, boardItem) => {
+ state.selectedBoardItems = [...state.selectedBoardItems, boardItem];
+ },
+
+ [mutationTypes.REMOVE_BOARD_ITEM_FROM_SELECTION]: (state, boardItem) => {
+ Vue.set(
+ state,
+ 'selectedBoardItems',
+ state.selectedBoardItems.filter((obj) => obj !== boardItem),
+ );
+ },
+
+ [mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => {
+ state.addColumnFormVisible = visible;
+ },
+
+ [mutationTypes.ADD_LIST_TO_HIGHLIGHTED_LISTS]: (state, listId) => {
+ state.highlightedLists.push(listId);
+ },
+
+ [mutationTypes.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS]: (state, listId) => {
+ state.highlightedLists = state.highlightedLists.filter((id) => id !== listId);
+ },
};
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index aba7da373cf..91544d6c9c5 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -14,6 +14,9 @@ export default () => ({
issues: {},
filterParams: {},
boardConfig: {},
+ labels: [],
+ highlightedLists: [],
+ selectedBoardItems: [],
groupProjects: [],
groupProjectsFlags: {
isLoading: false,
@@ -22,6 +25,7 @@ export default () => ({
},
selectedProject: {},
error: undefined,
+ addColumnFormVisible: false,
// TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes: false,
});
diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js
index 347deb81846..0a230f72dcc 100644
--- a/app/assets/javascripts/boards/toggle_focus.js
+++ b/app/assets/javascripts/boards/toggle_focus.js
@@ -1,45 +1,17 @@
-import $ from 'jquery';
import Vue from 'vue';
-import { GlIcon } from '@gitlab/ui';
-import { hide } from '~/tooltips';
+import ToggleFocus from './components/toggle_focus.vue';
-export default (ModalStore, boardsStore) => {
- const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
+export default () => {
+ const issueBoardsContentSelector = '.content-wrapper > .js-focus-mode-board';
return new Vue({
- el: document.getElementById('js-toggle-focus-btn'),
- components: {
- GlIcon,
+ el: '#js-toggle-focus-btn',
+ render(h) {
+ return h(ToggleFocus, {
+ props: {
+ issueBoardsContentSelector,
+ },
+ });
},
- data: {
- modal: ModalStore.store,
- store: boardsStore.state,
- isFullscreen: false,
- },
- methods: {
- toggleFocusMode() {
- const $el = $(this.$refs.toggleFocusModeButton);
- hide($el);
-
- issueBoardsContent.classList.toggle('is-focused');
-
- this.isFullscreen = !this.isFullscreen;
- },
- },
- template: `
- <div class="board-extra-actions">
- <a
- href="#"
- class="btn btn-default has-tooltip gl-ml-3 js-focus-mode-btn"
- data-qa-selector="focus_mode_button"
- role="button"
- aria-label="Toggle focus mode"
- title="Toggle focus mode"
- ref="toggleFocusModeButton"
- @click="toggleFocusMode">
- <gl-icon :name="isFullscreen ? 'minimize' : 'maximize'" />
- </a>
- </div>
- `,
});
};