diff options
Diffstat (limited to 'app/assets/javascripts/boards/components')
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> |