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