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:
Diffstat (limited to 'app/assets/javascripts/boards/components')
-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
41 files changed, 866 insertions, 494 deletions
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>