diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 11:59:07 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 11:59:07 +0000 |
commit | 8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch) | |
tree | 544930fb309b30317ae9797a9683768705d664c4 /app/assets/javascripts/boards | |
parent | 4b1de649d0168371549608993deac953eb692019 (diff) | |
download | gitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz |
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards')
53 files changed, 906 insertions, 553 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index 6b7b0c2e28d..e5ff41dab74 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,31 +1,39 @@ import { sortBy } from 'lodash'; -import ListIssue from 'ee_else_ce/boards/models/issue'; +import axios from '~/lib/utils/axios_utils'; import { ListType } from './constants'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import boardsStore from '~/boards/stores/boards_store'; export function getMilestone() { return null; } +export function updateListPosition(listObj) { + const { listType } = listObj; + let { position } = listObj; + if (listType === ListType.closed) { + position = Infinity; + } else if (listType === ListType.backlog) { + position = -Infinity; + } + + return { ...listObj, position }; +} + export function formatBoardLists(lists) { - const formattedLists = lists.nodes.map(list => - boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }), - ); - return formattedLists.reduce((map, list) => { + return lists.nodes.reduce((map, list) => { return { ...map, - [list.id]: list, + [list.id]: updateListPosition(list), }; }, {}); } export function formatIssue(issue) { - return new ListIssue({ + return { ...issue, labels: issue.labels?.nodes || [], assignees: issue.assignees?.nodes || [], - }); + }; } export function formatListIssues(listIssues) { @@ -44,12 +52,12 @@ export function formatListIssues(listIssues) { [list.id]: sortedIssues.map(i => { const id = getIdFromGraphQLId(i.id); - const listIssue = new ListIssue({ + const listIssue = { ...i, id, labels: i.labels?.nodes || [], assignees: i.assignees?.nodes || [], - }); + }; issues[id] = listIssue; @@ -83,21 +91,48 @@ export function fullLabelId(label) { } export function moveIssueListHelper(issue, fromList, toList) { - if (toList.type === ListType.label) { - issue.addLabel(toList.label); + const updatedIssue = issue; + if ( + toList.listType === ListType.label && + !updatedIssue.labels.find(label => label.id === toList.label.id) + ) { + updatedIssue.labels.push(toList.label); } - if (fromList && fromList.type === ListType.label) { - issue.removeLabel(fromList.label); + if (fromList?.label && fromList.listType === ListType.label) { + updatedIssue.labels = updatedIssue.labels.filter(label => fromList.label.id !== label.id); } - if (toList.type === ListType.assignee) { - issue.addAssignee(toList.assignee); + if ( + toList.listType === ListType.assignee && + !updatedIssue.assignees.find(assignee => assignee.id === toList.assignee.id) + ) { + updatedIssue.assignees.push(toList.assignee); + } + if (fromList?.assignee && fromList.listType === ListType.assignee) { + updatedIssue.assignees = updatedIssue.assignees.filter( + assignee => assignee.id !== fromList.assignee.id, + ); } - if (fromList && fromList.type === ListType.assignee) { - issue.removeAssignee(fromList.assignee); + + return updatedIssue; +} + +export function getBoardsPath(endpoint, board) { + const path = `${endpoint}${board.id ? `/${board.id}` : ''}.json`; + + if (board.id) { + return axios.put(path, { board }); } + return axios.post(path, { board }); +} + +export function isListDraggable(list) { + return list.listType !== ListType.backlog && list.listType !== ListType.closed; +} - return issue; +// EE-specific feature. Find the implementation in the `ee/`-folder +export function transformBoardConfig() { + return ''; } export default { @@ -106,4 +141,6 @@ export default { formatListIssues, fullBoardId, fullLabelId, + getBoardsPath, + isListDraggable, }; diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue index c81f171af2b..1469efae5a6 100644 --- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue +++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue @@ -1,18 +1,20 @@ <script> -import { mapActions, mapGetters } from 'vuex'; +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/queries/users_search.query.graphql'; +import searchUsers from '~/boards/graphql/users_search.query.graphql'; export default { noSearchDelay: 0, @@ -32,12 +34,13 @@ export default { GlAvatarLabeled, GlAvatarLink, GlSearchBoxByType, + GlLoadingIcon, }, data() { return { search: '', participants: [], - selected: this.$store.getters.activeIssue.assignees, + selected: [], }; }, apollo: { @@ -72,6 +75,7 @@ export default { }, computed: { ...mapGetters(['activeIssue']), + ...mapState(['isSettingAssignees']), assigneeText() { return n__('Assignee', '%d Assignees', this.selected.length); }, @@ -89,9 +93,20 @@ export default { 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 = []; }, @@ -117,9 +132,9 @@ export default { </script> <template> - <board-editable-item :title="assigneeText" @close="saveAssignees"> + <board-editable-item :loading="isSettingAssignees" :title="assigneeText" @close="saveAssignees"> <template #collapsed> - <issuable-assignees :users="activeIssue.assignees" /> + <issuable-assignees :users="activeIssue.assignees" @assign-self="assignSelf" /> </template> <template #default> @@ -132,45 +147,48 @@ export default { <gl-search-box-by-type v-model.trim="search" /> </template> <template #items> - <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> + <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> diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index cb93340bcf8..753e6941c43 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -2,15 +2,12 @@ // This component is being replaced in favor of './board_column_new.vue' for GraphQL boards import Sortable from 'sortablejs'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue'; -import EmptyComponent from '~/vue_shared/components/empty_component'; import BoardList from './board_list.vue'; import boardsStore from '../stores/boards_store'; import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; -import { ListType } from '../constants'; export default { components: { - BoardPromotionState: EmptyComponent, BoardListHeader, BoardList, }, @@ -42,9 +39,6 @@ export default { }; }, computed: { - showBoardListAndBoardInfo() { - return this.list.type !== ListType.promotion; - }, listIssues() { return this.list.issues; }, @@ -105,16 +99,7 @@ export default { class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base" > <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" /> - <board-list - v-if="showBoardListAndBoardInfo" - ref="board-list" - :disabled="disabled" - :issues="listIssues" - :list="list" - /> - - <!-- Will be only available in EE --> - <board-promotion-state v-if="list.id === 'promotion'" /> + <board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" /> </div> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_column_new.vue b/app/assets/javascripts/boards/components/board_column_new.vue index 8a59355eb83..7839f45c48b 100644 --- a/app/assets/javascripts/boards/components/board_column_new.vue +++ b/app/assets/javascripts/boards/components/board_column_new.vue @@ -1,13 +1,11 @@ <script> import { mapGetters, mapActions, mapState } from 'vuex'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_new.vue'; -import BoardPromotionState from 'ee_else_ce/boards/components/board_promotion_state'; import BoardList from './board_list_new.vue'; -import { ListType } from '../constants'; +import { isListDraggable } from '../boards_util'; export default { components: { - BoardPromotionState, BoardListHeader, BoardList, }, @@ -35,22 +33,17 @@ export default { computed: { ...mapState(['filterParams']), ...mapGetters(['getIssuesByList']), - showBoardListAndBoardInfo() { - return this.list.type !== ListType.promotion; - }, listIssues() { return this.getIssuesByList(this.list.id); }, - shouldFetchIssues() { - return this.list.type !== ListType.blank; + isListDraggable() { + return isListDraggable(this.list); }, }, watch: { filterParams: { handler() { - if (this.shouldFetchIssues) { - this.fetchIssuesForList({ listId: this.list.id }); - } + this.fetchIssuesForList({ listId: this.list.id }); }, deep: true, immediate: true, @@ -58,7 +51,6 @@ export default { }, methods: { ...mapActions(['fetchIssuesForList']), - // TODO: Reordering of lists https://gitlab.com/gitlab-org/gitlab/-/issues/280515 }, }; </script> @@ -66,13 +58,12 @@ export default { <template> <div :class="{ - 'is-draggable': !list.preset, - 'is-expandable': list.isExpandable, - 'is-collapsed': !list.isExpanded, - 'board-type-assignee': list.type === 'assignee', + 'is-draggable': isListDraggable, + 'is-collapsed': list.collapsed, + 'board-type-assignee': list.listType === 'assignee', }" :data-id="list.id" - class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal" + class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable" data-qa-selector="board_list" > <div @@ -80,15 +71,12 @@ export default { > <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" /> <board-list - v-if="showBoardListAndBoardInfo" ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" + :can-admin-list="canAdminList" /> - - <!-- Will be only available in EE --> - <board-promotion-state v-if="list.id === 'promotion'" /> </div> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue index 754b00b54e0..99d1e4a2611 100644 --- a/app/assets/javascripts/boards/components/board_configuration_options.vue +++ b/app/assets/javascripts/boards/components/board_configuration_options.vue @@ -42,7 +42,7 @@ export default { </script> <template> - <div class="append-bottom-20"> + <div class="gl-mb-5"> <label class="label-bold gl-font-lg" for="board-new-name"> {{ __('List options') }} </label> diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index 92976574efb..b366aa6fdb3 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -1,10 +1,13 @@ <script> +import Draggable from 'vuedraggable'; import { mapState, mapGetters, mapActions } from 'vuex'; import { sortBy } from 'lodash'; import { GlAlert } from '@gitlab/ui'; -import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; +import BoardColumn from './board_column.vue'; import BoardColumnNew from './board_column_new.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'; export default { components: { @@ -32,18 +35,51 @@ export default { ...mapState(['boardLists', 'error']), ...mapGetters(['isSwimlanesOn']), boardListsToUse() { - const lists = - this.glFeatures.graphqlBoardLists || this.isSwimlanesOn ? this.boardLists : this.lists; - return sortBy([...Object.values(lists)], 'position'); + return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn + ? sortBy([...Object.values(this.boardLists)], 'position') + : this.lists; + }, + canDragColumns() { + return this.glFeatures.graphqlBoardLists && this.canAdminList; + }, + boardColumnWrapper() { + return this.canDragColumns ? Draggable : 'div'; + }, + draggableOptions() { + const options = { + ...defaultSortableConfig, + disabled: this.disabled, + draggable: '.is-draggable', + fallbackOnBody: false, + group: 'boards-list', + tag: 'div', + value: this.lists, + }; + + return this.canDragColumns ? options : {}; }, - }, - mounted() { - if (this.glFeatures.graphqlBoardLists) { - this.showPromotionList(); - } }, methods: { - ...mapActions(['showPromotionList']), + ...mapActions(['moveList']), + handleDragOnStart() { + sortableStart(); + }, + + handleDragOnEnd(params) { + sortableEnd(); + + const { item, newIndex, oldIndex, to } = params; + + const listId = item.dataset.id; + const replacedListId = to.children[newIndex].dataset.id; + + this.moveList({ + listId, + replacedListId, + newIndex, + adjustmentValue: newIndex < oldIndex ? 1 : -1, + }); + }, }, }; </script> @@ -53,10 +89,14 @@ export default { <gl-alert v-if="error" variant="danger" :dismissible="false"> {{ error }} </gl-alert> - <div + <component + :is="boardColumnWrapper" v-if="!isSwimlanesOn" + ref="list" + v-bind="draggableOptions" class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap" - data-qa-selector="boards_list" + @start="handleDragOnStart" + @end="handleDragOnEnd" > <board-column v-for="list in boardListsToUse" @@ -66,7 +106,7 @@ export default { :list="list" :disabled="disabled" /> - </div> + </component> <template v-else> <epics-swimlanes diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index e4ef3600ff9..dab934352ca 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -1,11 +1,14 @@ <script> -import { __ } from '~/locale'; +import { GlModal } from '@gitlab/ui'; +import { pick } from 'lodash'; +import { __, s__ } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; -import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import { visitUrl } from '~/lib/utils/url_utility'; import boardsStore from '~/boards/stores/boards_store'; +import { fullBoardId, getBoardsPath } from '../boards_util'; import BoardConfigurationOptions from './board_configuration_options.vue'; +import createBoardMutation from '../graphql/board.mutation.graphql'; const boardDefaults = { id: false, @@ -19,10 +22,28 @@ 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') }, + [formType.delete]: { title: s__('Board|Delete board'), btnText: __('Delete') }, + [formType.edit]: { title: s__('Board|Edit board'), btnText: __('Save changes') }, + scopeModalTitle: s__('Board|Board scope'), + cancelButtonText: __('Cancel'), + deleteErrorMessage: s__('Board|Failed to delete board. Please try again.'), + saveErrorMessage: __('Unable to save your changes. Please try again.'), + deleteConfirmationMessage: s__('Board|Are you sure you want to delete this board?'), + titleFieldLabel: __('Title'), + titleFieldPlaceholder: s__('Board|Enter board name'), + }, components: { BoardScope: () => import('ee_component/boards/components/board_scope.vue'), - DeprecatedModal, + GlModal, BoardConfigurationOptions, }, props: { @@ -63,36 +84,35 @@ export default { required: false, default: false, }, + currentBoard: { + type: Object, + required: true, + }, + }, + inject: { + endpoints: { + default: {}, + }, }, data() { return { board: { ...boardDefaults, ...this.currentBoard }, - currentBoard: boardsStore.state.currentBoard, currentPage: boardsStore.state.currentPage, isLoading: false, }; }, computed: { isNewForm() { - return this.currentPage === 'new'; + return this.currentPage === formType.new; }, isDeleteForm() { - return this.currentPage === 'delete'; + return this.currentPage === formType.delete; }, isEditForm() { - return this.currentPage === 'edit'; - }, - isVisible() { - return this.currentPage !== ''; + return this.currentPage === formType.edit; }, buttonText() { - if (this.isNewForm) { - return __('Create board'); - } - if (this.isDeleteForm) { - return __('Delete'); - } - return __('Save changes'); + return this.$options.i18n[this.currentPage].btnText; }, buttonKind() { if (this.isNewForm) { @@ -104,16 +124,11 @@ export default { return 'info'; }, title() { - if (this.isNewForm) { - return __('Create new board'); - } - if (this.isDeleteForm) { - return __('Delete board'); - } if (this.readonly) { - return __('Board scope'); + return this.$options.i18n.scopeModalTitle; } - return __('Edit board'); + + return this.$options.i18n[this.currentPage].title; }, readonly() { return !this.canAdminBoard; @@ -121,6 +136,33 @@ export default { submitDisabled() { return this.isLoading || this.board.name.length === 0; }, + primaryProps() { + return { + text: this.buttonText, + attributes: [ + { + variant: this.buttonKind, + disabled: this.submitDisabled, + loading: this.isLoading, + 'data-qa-selector': 'save_changes_button', + }, + ], + }; + }, + cancelProps() { + return { + text: this.$options.i18n.cancelButtonText, + }; + }, + boardPayload() { + const { assignee, milestone, labels } = this.board; + return { + ...this.board, + assignee_id: assignee?.id, + milestone_id: milestone?.id, + label_ids: labels.length ? labels.map(b => b.id) : [''], + }; + }, }, mounted() { this.resetFormState(); @@ -129,6 +171,31 @@ export default { } }, methods: { + callBoardMutation(id) { + return this.$apollo.mutate({ + mutation: createBoardMutation, + variables: { + ...pick(this.boardPayload, ['hideClosedList', 'hideBacklogList']), + id, + }, + }); + }, + async updateBoard() { + const responses = await Promise.all([ + // Remove unnecessary REST API call when https://gitlab.com/gitlab-org/gitlab/-/issues/282299#note_462996301 is resolved + getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload), + this.callBoardMutation(fullBoardId(this.boardPayload.id)), + ]); + + return responses[0].data; + }, + async createBoard() { + // TODO: change this to use `createBoard` mutation https://gitlab.com/gitlab-org/gitlab/-/issues/292466 is resolved + const boardData = await getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload); + this.callBoardMutation(fullBoardId(boardData.data.id)); + + return boardData.data || boardData; + }, submit() { if (this.board.name.length === 0) return; this.isLoading = true; @@ -136,31 +203,21 @@ export default { boardsStore .deleteBoard(this.currentBoard) .then(() => { + this.isLoading = false; visitUrl(boardsStore.rootPath); }) .catch(() => { - Flash(__('Failed to delete board. Please try again.')); + Flash(this.$options.i18n.deleteErrorMessage); this.isLoading = false; }); } else { - boardsStore - .createBoard(this.board) - .then(resp => { - // This handles 2 use cases - // - In create call we only get one parameter, the new board - // - In update call, due to Promise.all, we get REST response in - // array index 0 - - if (Array.isArray(resp)) { - return resp[0].data; - } - return resp.data ? resp.data : resp; - }) + const boardAction = this.boardPayload.id ? this.updateBoard : this.createBoard; + boardAction() .then(data => { visitUrl(data.board_path); }) .catch(() => { - Flash(__('Unable to save your changes. Please try again.')); + Flash(this.$options.i18n.saveErrorMessage); this.isLoading = false; }); } @@ -181,53 +238,58 @@ export default { </script> <template> - <deprecated-modal - v-show="isVisible" + <gl-modal + modal-id="board-config-modal" + modal-class="board-config-modal" + content-class="gl-absolute gl-top-7" + visible :hide-footer="readonly" :title="title" - :primary-button-label="buttonText" - :kind="buttonKind" - :submit-disabled="submitDisabled" - modal-dialog-class="board-config-modal" + :action-primary="primaryProps" + :action-cancel="cancelProps" + @primary="submit" @cancel="cancel" - @submit="submit" + @close="cancel" + @hide.prevent > - <template #body> - <p v-if="isDeleteForm">{{ __('Are you sure you want to delete this board?') }}</p> - <form v-else class="js-board-config-modal" @submit.prevent> - <div v-if="!readonly" class="append-bottom-20"> - <label class="label-bold gl-font-lg" for="board-new-name">{{ __('Title') }}</label> - <input - id="board-new-name" - ref="name" - v-model="board.name" - class="form-control" - data-qa-selector="board_name_field" - type="text" - :placeholder="__('Enter board name')" - @keyup.enter="submit" - /> - </div> - - <board-configuration-options - :is-new-form="isNewForm" - :board="board" - :current-board="currentBoard" + <p v-if="isDeleteForm" data-testid="delete-confirmation-message"> + {{ $options.i18n.deleteConfirmationMessage }} + </p> + <form v-else class="js-board-config-modal" data-testid="board-form-wrapper" @submit.prevent> + <div v-if="!readonly" class="gl-mb-5" data-testid="board-form"> + <label class="gl-font-weight-bold gl-font-lg" for="board-new-name"> + {{ $options.i18n.titleFieldLabel }} + </label> + <input + id="board-new-name" + ref="name" + v-model="board.name" + class="form-control" + data-qa-selector="board_name_field" + type="text" + :placeholder="$options.i18n.titleFieldPlaceholder" + @keyup.enter="submit" /> + </div> - <board-scope - v-if="scopedIssueBoardFeatureEnabled" - :collapse-scope="isNewForm" - :board="board" - :can-admin-board="canAdminBoard" - :labels-path="labelsPath" - :labels-web-url="labelsWebUrl" - :enable-scoped-labels="enableScopedLabels" - :project-id="projectId" - :group-id="groupId" - :weights="weights" - /> - </form> - </template> - </deprecated-modal> + <board-configuration-options + :is-new-form="isNewForm" + :board="board" + :current-board="currentBoard" + /> + + <board-scope + v-if="scopedIssueBoardFeatureEnabled" + :collapse-scope="isNewForm" + :board="board" + :can-admin-board="canAdminBoard" + :labels-path="labelsPath" + :labels-web-url="labelsWebUrl" + :enable-scoped-labels="enableScopedLabels" + :project-id="projectId" + :group-id="groupId" + :weights="weights" + /> + </form> + </gl-modal> </template> diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 53989e2d9de..1f87b563e73 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -6,7 +6,6 @@ import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import boardsStore from '../stores/boards_store'; import { sprintf, __ } from '~/locale'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { getBoardSortableDefaultOptions, @@ -25,7 +24,6 @@ export default { boardNewIssue, GlLoadingIcon, }, - mixins: [glFeatureFlagMixin()], props: { disabled: { type: Boolean, diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index d85ba2038a7..3db5c2e0830 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -72,12 +72,7 @@ export default { return this.list?.label?.description || this.list.title || ''; }, showListHeaderButton() { - return ( - !this.disabled && - this.listType !== ListType.closed && - this.listType !== ListType.blank && - this.listType !== ListType.promotion - ); + return !this.disabled && this.listType !== ListType.closed; }, showMilestoneListDetails() { return ( @@ -109,9 +104,6 @@ export default { this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded ); }, - showBoardListAndBoardInfo() { - return this.listType !== ListType.blank && this.listType !== ListType.promotion; - }, uniqueKey() { // eslint-disable-next-line @gitlab/require-i18n-strings return `boards.${this.boardId}.${this.listType}.${this.list.id}`; @@ -190,7 +182,8 @@ export default { :title="chevronTooltip" :icon="chevronIcon" class="board-title-caret no-drag gl-cursor-pointer" - variant="link" + category="tertiary" + size="small" @click="toggleExpanded" /> <!-- The following is only true in EE and if it is a milestone --> @@ -288,7 +281,6 @@ export default { </gl-tooltip> <div - v-if="showBoardListAndBoardInfo" class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary" :class="{ 'gl-display-none!': !list.isExpanded && isSwimlanesHeader, diff --git a/app/assets/javascripts/boards/components/board_list_header_new.vue b/app/assets/javascripts/boards/components/board_list_header_new.vue index 99347a4cd4d..44eb2aa34c2 100644 --- a/app/assets/javascripts/boards/components/board_list_header_new.vue +++ b/app/assets/javascripts/boards/components/board_list_header_new.vue @@ -9,15 +9,22 @@ import { GlSprintf, GlTooltipDirective, } from '@gitlab/ui'; -import { n__, s__ } from '~/locale'; +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 { inactiveId, LIST, ListType } from '../constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; +import { isListDraggable } from '~/boards/boards_util'; export default { + i18n: { + newIssue: __('New issue'), + listSettings: __('List settings'), + expand: s__('Boards|Expand'), + collapse: s__('Boards|Collapse'), + }, components: { GlButtonGroup, GlButton, @@ -66,57 +73,49 @@ export default { return Boolean(this.currentUserId); }, listType() { - return this.list.type; + return this.list.listType; }, listAssignee() { return this.list?.assignee?.username || ''; }, listTitle() { - return this.list?.label?.description || this.list.title || ''; + return this.list?.label?.description || this.list?.assignee?.name || this.list.title || ''; }, showListHeaderButton() { - return ( - !this.disabled && - this.listType !== ListType.closed && - this.listType !== ListType.blank && - this.listType !== ListType.promotion - ); + return !this.disabled && this.listType !== ListType.closed; }, showMilestoneListDetails() { return ( - this.list.type === ListType.milestone && + this.listType === ListType.milestone && this.list.milestone && - (this.list.isExpanded || !this.isSwimlanesHeader) + (!this.list.collapsed || !this.isSwimlanesHeader) ); }, showAssigneeListDetails() { return ( - this.list.type === ListType.assignee && (this.list.isExpanded || !this.isSwimlanesHeader) + this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader) ); }, issuesCount() { - return this.list.issuesSize; + return this.list.issuesCount; }, issuesTooltipLabel() { return n__(`%d issue`, `%d issues`, this.issuesCount); }, chevronTooltip() { - return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand'); + return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse; }, chevronIcon() { - return this.list.isExpanded ? 'chevron-right' : 'chevron-down'; + return this.list.collapsed ? 'chevron-down' : 'chevron-right'; }, isNewIssueShown() { return this.listType === ListType.backlog || this.showListHeaderButton; }, isSettingsShown() { return ( - this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded + this.listType !== ListType.backlog && this.showListHeaderButton && !this.list.collapsed ); }, - showBoardListAndBoardInfo() { - return this.listType !== ListType.blank && this.listType !== ListType.promotion; - }, uniqueKey() { // eslint-disable-next-line @gitlab/require-i18n-strings return `boards.${this.boardId}.${this.listType}.${this.list.id}`; @@ -127,6 +126,9 @@ export default { headerStyle() { return { borderTopColor: this.list?.label?.color }; }, + userCanDrag() { + return !this.disabled && isListDraggable(this.list); + }, }, methods: { ...mapActions(['updateList', 'setActiveId']), @@ -145,7 +147,7 @@ export default { eventHub.$emit(`toggle-issue-form-${this.list.id}`); }, toggleExpanded() { - this.list.isExpanded = !this.list.isExpanded; + this.list.collapsed = !this.list.collapsed; if (!this.isLoggedIn) { this.addToLocalStorage(); @@ -159,11 +161,11 @@ export default { }, addToLocalStorage() { if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); + localStorage.setItem(`${this.uniqueKey}.expanded`, !this.list.collapsed); } }, updateListFunction() { - this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded }); + this.updateList({ listId: this.list.id, collapsed: this.list.collapsed }); }, }, }; @@ -173,7 +175,7 @@ export default { <header :class="{ 'has-border': list.label && list.label.color, - 'gl-h-full': !list.isExpanded, + 'gl-h-full': list.collapsed, 'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader, }" :style="headerStyle" @@ -183,22 +185,22 @@ export default { > <h3 :class="{ - 'user-can-drag': !disabled && !list.preset, - 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader, - 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader, - 'gl-py-2': !list.isExpanded && isSwimlanesHeader, - 'gl-flex-direction-column': !list.isExpanded, + 'user-can-drag': userCanDrag, + 'gl-py-3 gl-h-full': list.collapsed && !isSwimlanesHeader, + 'gl-border-b-0': list.collapsed || isSwimlanesHeader, + 'gl-py-2': list.collapsed && isSwimlanesHeader, + 'gl-flex-direction-column': list.collapsed, }" class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle" > <gl-button - v-if="list.isExpandable" v-gl-tooltip.hover :aria-label="chevronTooltip" :title="chevronTooltip" :icon="chevronIcon" class="board-title-caret no-drag gl-cursor-pointer" - variant="link" + category="tertiary" + size="small" @click="toggleExpanded" /> <!-- EE start --> @@ -207,8 +209,8 @@ export default { aria-hidden="true" class="milestone-icon" :class="{ - 'gl-mt-3 gl-rotate-90': !list.isExpanded, - 'gl-mr-2': list.isExpanded, + 'gl-mt-3 gl-rotate-90': list.collapsed, + 'gl-mr-2': !list.collapsed, }" > <gl-icon name="timer" /> @@ -216,17 +218,17 @@ export default { <a v-if="showAssigneeListDetails" - :href="list.assignee.path" + :href="list.assignee.webUrl" class="user-avatar-link js-no-trigger" :class="{ - 'gl-mt-3 gl-rotate-90': !list.isExpanded, + 'gl-mt-3 gl-rotate-90': list.collapsed, }" > <img v-gl-tooltip.hover.bottom :title="listAssignee" :alt="list.assignee.name" - :src="list.assignee.avatar" + :src="list.assignee.avatarUrl" class="avatar s20" height="20" width="20" @@ -236,9 +238,9 @@ export default { <div class="board-title-text" :class="{ - 'gl-display-none': !list.isExpanded && isSwimlanesHeader, - 'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded, - 'gl-flex-grow-1': list.isExpanded, + 'gl-display-none': list.collapsed && isSwimlanesHeader, + 'gl-flex-grow-0 gl-my-3 gl-mx-0': list.collapsed, + 'gl-flex-grow-1': !list.collapsed, }" > <!-- EE start --> @@ -246,16 +248,16 @@ export default { v-if="listType !== 'label'" v-gl-tooltip.hover :class="{ - 'gl-display-block': !list.isExpanded || listType === 'milestone', + 'gl-display-block': list.collapsed || listType === 'milestone', }" :title="listTitle" class="board-title-main-text gl-text-truncate" > - {{ list.title }} + {{ listTitle }} </span> <span v-if="listType === 'assignee'" - v-show="list.isExpanded" + v-show="!list.collapsed" class="gl-ml-2 gl-font-weight-normal gl-text-gray-500" > @{{ listAssignee }} @@ -267,21 +269,21 @@ export default { :background-color="list.label.color" :description="list.label.description" :scoped="showScopedLabels(list.label)" - :size="!list.isExpanded ? 'sm' : ''" + :size="list.collapsed ? 'sm' : ''" :title="list.label.title" /> </div> <!-- EE start --> <span - v-if="isSwimlanesHeader && !list.isExpanded" + v-if="isSwimlanesHeader && list.collapsed" ref="collapsedInfo" aria-hidden="true" - class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-500" + class="board-header-collapsed-info-icon gl-cursor-pointer gl-text-gray-500" > <gl-icon name="information" /> </span> - <gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo"> + <gl-tooltip v-if="isSwimlanesHeader && list.collapsed" :target="() => $refs.collapsedInfo"> <div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div> <div v-if="list.maxIssueCount !== 0"> • @@ -301,11 +303,10 @@ export default { <!-- EE end --> <div - v-if="showBoardListAndBoardInfo" class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500" :class="{ - 'gl-display-none!': !list.isExpanded && isSwimlanesHeader, - 'gl-p-0': !list.isExpanded, + 'gl-display-none!': list.collapsed && isSwimlanesHeader, + 'gl-p-0': list.collapsed, }" > <span class="gl-display-inline-flex"> @@ -331,11 +332,11 @@ export default { > <gl-button v-if="isNewIssueShown" - v-show="list.isExpanded" + v-show="!list.collapsed" ref="newIssueBtn" v-gl-tooltip.hover - :aria-label="__('New issue')" - :title="__('New issue')" + :aria-label="$options.i18n.newIssue" + :title="$options.i18n.newIssue" class="issue-count-badge-add-button no-drag" icon="plus" @click="showNewIssueForm" @@ -345,13 +346,13 @@ export default { v-if="isSettingsShown" ref="settingsBtn" v-gl-tooltip.hover - :aria-label="__('List settings')" + :aria-label="$options.i18n.listSettings" class="no-drag js-board-settings-button" - :title="__('List settings')" + :title="$options.i18n.listSettings" icon="settings" @click="openSidebarSettings" /> - <gl-tooltip :target="() => $refs.settingsBtn">{{ __('List settings') }}</gl-tooltip> + <gl-tooltip :target="() => $refs.settingsBtn">{{ $options.i18n.listSettings }}</gl-tooltip> </gl-button-group> </h3> </header> diff --git a/app/assets/javascripts/boards/components/board_list_new.vue b/app/assets/javascripts/boards/components/board_list_new.vue index 396aedcc557..92a381a8f57 100644 --- a/app/assets/javascripts/boards/components/board_list_new.vue +++ b/app/assets/javascripts/boards/components/board_list_new.vue @@ -1,21 +1,26 @@ <script> +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_new.vue'; import BoardCard from './board_card.vue'; import eventHub from '../eventhub'; -import boardsStore from '../stores/boards_store'; import { sprintf, __ } from '~/locale'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'BoardList', + i18n: { + loadingIssues: __('Loading issues'), + loadingMoreissues: __('Loading more issues'), + showingAllIssues: __('Showing all issues'), + }, components: { BoardCard, BoardNewIssue, GlLoadingIcon, }, - mixins: [glFeatureFlagMixin()], props: { disabled: { type: Boolean, @@ -29,11 +34,15 @@ export default { type: Array, required: true, }, + canAdminList: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { scrollOffset: 250, - filters: boardsStore.state.filters, showCount: false, showIssueForm: false, }; @@ -43,11 +52,11 @@ export default { paginatedIssueText() { return sprintf(__('Showing %{pageSize} of %{total} issues'), { pageSize: this.issues.length, - total: this.list.issuesSize, + total: this.list.issuesCount, }); }, issuesSizeExceedsMax() { - return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount; + return this.list.maxIssueCount > 0 && this.list.issuesCount > this.list.maxIssueCount; }, hasNextPage() { return this.pageInfoByListId[this.list.id].hasNextPage; @@ -55,15 +64,34 @@ export default { loading() { return this.listsFlags[this.list.id]?.isLoading; }, + loadingMore() { + return this.listsFlags[this.list.id]?.isLoadingMore; + }, + listRef() { + // When list is draggable, the reference to the list needs to be accessed differently + return this.canAdminList ? this.$refs.list.$el : this.$refs.list; + }, + showingAllIssues() { + return this.issues.length === this.list.issuesCount; + }, + treeRootWrapper() { + return this.canAdminList ? Draggable : 'ul'; + }, + treeRootOptions() { + const options = { + ...defaultSortableConfig, + fallbackOnBody: false, + group: 'board-list', + tag: 'ul', + 'ghost-class': 'board-card-drag-active', + 'data-list-id': this.list.id, + value: this.issues, + }; + + return this.canAdminList ? options : {}; + }, }, watch: { - filters: { - handler() { - this.list.loadingMore = false; - this.$refs.list.scrollTop = 0; - }, - deep: true, - }, issues() { this.$nextTick(() => { this.showCount = this.scrollHeight() > Math.ceil(this.listHeight()); @@ -76,35 +104,29 @@ export default { }, mounted() { // Scroll event on list to load more - this.$refs.list.addEventListener('scroll', this.onScroll); + this.listRef.addEventListener('scroll', this.onScroll); }, beforeDestroy() { eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm); eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); - this.$refs.list.removeEventListener('scroll', this.onScroll); + this.listRef.removeEventListener('scroll', this.onScroll); }, methods: { - ...mapActions(['fetchIssuesForList']), + ...mapActions(['fetchIssuesForList', 'moveIssue']), listHeight() { - return this.$refs.list.getBoundingClientRect().height; + return this.listRef.getBoundingClientRect().height; }, scrollHeight() { - return this.$refs.list.scrollHeight; + return this.listRef.scrollHeight; }, scrollTop() { - return this.$refs.list.scrollTop + this.listHeight(); + return this.listRef.scrollTop + this.listHeight(); }, scrollToTop() { - this.$refs.list.scrollTop = 0; + this.listRef.scrollTop = 0; }, loadNextPage() { - const loadingDone = () => { - this.list.loadingMore = false; - }; - this.list.loadingMore = true; - this.fetchIssuesForList({ listId: this.list.id, fetchNext: true }) - .then(loadingDone) - .catch(loadingDone); + this.fetchIssuesForList({ listId: this.list.id, fetchNext: true }); }, toggleForm() { this.showIssueForm = !this.showIssueForm; @@ -112,7 +134,7 @@ export default { onScroll() { window.requestAnimationFrame(() => { if ( - !this.list.loadingMore && + !this.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset && this.hasNextPage ) { @@ -120,32 +142,83 @@ export default { } }); }, + handleDragOnStart() { + sortableStart(); + }, + handleDragOnEnd(params) { + sortableEnd(); + const { newIndex, oldIndex, from, to, item } = params; + const { issueId, issueIid, issuePath } = item.dataset; + const { children } = to; + let moveBeforeId; + let moveAfterId; + + const getIssueId = el => Number(el.dataset.issueId); + + // If issue is being moved within the same list + if (from === to) { + if (newIndex > oldIndex && children.length > 1) { + // If issue is being moved down we look for the issue that ends up before + moveBeforeId = getIssueId(children[newIndex]); + } else if (newIndex < oldIndex && children.length > 1) { + // If issue is being moved up we look for the issue that ends up after + moveAfterId = getIssueId(children[newIndex]); + } else { + // If issue remains in the same list at the same position we do nothing + return; + } + } else { + // We look for the issue that ends up before the moved issue if it exists + if (children[newIndex - 1]) { + moveBeforeId = getIssueId(children[newIndex - 1]); + } + // We look for the issue that ends up after the moved issue if it exists + if (children[newIndex]) { + moveAfterId = getIssueId(children[newIndex]); + } + } + + this.moveIssue({ + issueId, + issueIid, + issuePath, + fromListId: from.dataset.listId, + toListId: to.dataset.listId, + moveBeforeId, + moveAfterId, + }); + }, }, }; </script> <template> <div - v-show="list.isExpanded" + v-show="!list.collapsed" class="board-list-component gl-relative gl-h-full gl-display-flex gl-flex-direction-column" data-qa-selector="board_list_cards_area" > <div v-if="loading" class="gl-mt-4 gl-text-center" - :aria-label="__('Loading issues')" + :aria-label="$options.i18n.loadingIssues" data-testid="board_list_loading" > <gl-loading-icon /> </div> - <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" /> - <ul + <board-new-issue v-if="list.listType !== 'closed' && showIssueForm" :list="list" /> + <component + :is="treeRootWrapper" v-show="!loading" ref="list" + v-bind="treeRootOptions" :data-board="list.id" - :data-board-type="list.type" + :data-board-type="list.listType" :class="{ 'bg-danger-100': issuesSizeExceedsMax }" class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list" + data-testid="tree-root-wrapper" + @start="handleDragOnStart" + @end="handleDragOnEnd" > <board-card v-for="(issue, index) in issues" @@ -157,10 +230,10 @@ export default { :disabled="disabled" /> <li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1"> - <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" /> - <span v-if="issues.length === list.issuesSize">{{ __('Showing all issues') }}</span> + <gl-loading-icon v-if="loadingMore" :label="$options.i18n.loadingMoreissues" /> + <span v-if="showingAllIssues">{{ $options.i18n.showingAllIssues }}</span> <span v-else>{{ paginatedIssueText }}</span> </li> - </ul> + </component> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_promotion_state.js b/app/assets/javascripts/boards/components/board_promotion_state.js deleted file mode 100644 index ff8b4c56321..00000000000 --- a/app/assets/javascripts/boards/components/board_promotion_state.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 80070b25bd0..60db8fefe82 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -53,7 +53,7 @@ export default { return this.activeList.label; }, boardListType() { - return this.activeList.type || null; + return this.activeList.type || this.activeList.listType || null; }, listTypeTitle() { return this.$options.labelListText; diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 0b079c78209..4f23c38d0f7 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -3,17 +3,18 @@ import { throttle } from 'lodash'; import { GlLoadingIcon, GlSearchBoxByType, - GlDeprecatedDropdown, - GlDeprecatedDropdownDivider, - GlDeprecatedDropdownHeader, - GlDeprecatedDropdownItem, + GlDropdown, + GlDropdownDivider, + GlDropdownSectionHeader, + GlDropdownItem, + GlModalDirective, } from '@gitlab/ui'; import httpStatusCodes from '~/lib/utils/http_status'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import projectQuery from '../queries/project_boards.query.graphql'; -import groupQuery from '../queries/group_boards.query.graphql'; +import projectQuery from '../graphql/project_boards.query.graphql'; +import groupQuery from '../graphql/group_boards.query.graphql'; import boardsStore from '../stores/boards_store'; import BoardForm from './board_form.vue'; @@ -26,10 +27,13 @@ export default { BoardForm, GlLoadingIcon, GlSearchBoxByType, - GlDeprecatedDropdown, - GlDeprecatedDropdownDivider, - GlDeprecatedDropdownHeader, - GlDeprecatedDropdownItem, + GlDropdown, + GlDropdownDivider, + GlDropdownSectionHeader, + GlDropdownItem, + }, + directives: { + GlModalDirective, }, props: { currentBoard: { @@ -108,7 +112,7 @@ export default { return this.groupId ? 'group' : 'project'; }, loading() { - return this.loadingRecentBoards && this.loadingBoards; + return this.loadingRecentBoards || Boolean(this.loadingBoards); }, currentPage() { return this.state.currentPage; @@ -235,22 +239,17 @@ export default { <template> <div class="boards-switcher js-boards-selector gl-mr-3"> <span class="boards-selector-wrapper js-boards-selector-wrapper"> - <gl-deprecated-dropdown + <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" > - <div> - <div class="dropdown-title mb-0" @mousedown.prevent> - {{ s__('IssueBoards|Switch board') }} - </div> - </div> - - <gl-deprecated-dropdown-header class="mt-0"> - <gl-search-box-by-type ref="searchBox" v-model="filterTerm" /> - </gl-deprecated-dropdown-header> + <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" @@ -259,49 +258,50 @@ export default { class="dropdown-content flex-fill" @scroll.passive="throttledSetScrollFade" > - <gl-deprecated-dropdown-item + <gl-dropdown-item v-show="filteredBoards.length === 0" class="gl-pointer-events-none text-secondary" > {{ s__('IssueBoards|No matching boards found') }} - </gl-deprecated-dropdown-item> + </gl-dropdown-item> - <h6 v-if="showRecentSection" class="dropdown-bold-header my-0"> + <gl-dropdown-section-header v-if="showRecentSection"> {{ __('Recent') }} - </h6> + </gl-dropdown-section-header> <template v-if="showRecentSection"> - <gl-deprecated-dropdown-item + <gl-dropdown-item v-for="recentBoard in recentBoards" :key="`recent-${recentBoard.id}`" class="js-dropdown-item" :href="`${boardBaseUrl}/${recentBoard.id}`" > {{ recentBoard.name }} - </gl-deprecated-dropdown-item> + </gl-dropdown-item> </template> - <hr v-if="showRecentSection" class="my-1" /> + <gl-dropdown-divider v-if="showRecentSection" /> - <h6 v-if="showRecentSection" class="dropdown-bold-header my-0"> + <gl-dropdown-section-header v-if="showRecentSection"> {{ __('All') }} - </h6> + </gl-dropdown-section-header> - <gl-deprecated-dropdown-item + <gl-dropdown-item v-for="otherBoard in filteredBoards" :key="otherBoard.id" class="js-dropdown-item" :href="`${boardBaseUrl}/${otherBoard.id}`" > {{ otherBoard.name }} - </gl-deprecated-dropdown-item> - <gl-deprecated-dropdown-item v-if="hasMissingBoards" class="small unclickable"> + </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-deprecated-dropdown-item> + </gl-dropdown-item> </div> <div @@ -313,25 +313,27 @@ export default { <gl-loading-icon v-if="loading" /> <div v-if="canAdminBoard"> - <gl-deprecated-dropdown-divider /> + <gl-dropdown-divider /> - <gl-deprecated-dropdown-item + <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-deprecated-dropdown-item> + </gl-dropdown-item> - <gl-deprecated-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-deprecated-dropdown-item> + </gl-dropdown-item> </div> - </gl-deprecated-dropdown> + </gl-dropdown> <board-form v-if="currentPage" @@ -343,6 +345,7 @@ export default { :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" :weights="weights" :enable-scoped-labels="enabledScopedLabels" + :current-board="currentBoard" /> </span> </div> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 45ce1e51489..ddd20ff281c 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -10,6 +10,7 @@ import IssueDueDate from './issue_due_date.vue'; import IssueTimeEstimate from './issue_time_estimate.vue'; import boardsStore from '../stores/boards_store'; import { isScopedLabel } from '~/lib/utils/common_utils'; +import { ListType } from '../constants'; export default { components: { @@ -122,7 +123,13 @@ export default { return true; }, isNonListLabel(label) { - return label.id && !(this.list.type === 'label' && this.list.title === label.title); + return ( + label.id && + !( + (this.list.type || this.list.listType) === ListType.label && + this.list.title === label.title + ) + ); }, filterByLabel(label) { if (!this.updateFilters) return; @@ -158,9 +165,13 @@ export default { class="confidential-icon gl-mr-2" :aria-label="__('Confidential')" /> - <a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{ - issue.title - }}</a> + <a + :href="issue.path || issue.webUrl || ''" + :title="issue.title" + class="js-no-trigger" + @mousemove.stop + >{{ issue.title }}</a + > </h4> </div> <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap"> @@ -196,7 +207,11 @@ export default { #{{ issue.iid }} </span> <span class="board-info-items gl-mt-3 gl-display-inline-block"> - <issue-due-date v-if="issue.dueDate" :date="issue.dueDate" :closed="issue.closed" /> + <issue-due-date + v-if="issue.dueDate" + :date="issue.dueDate" + :closed="issue.closed || Boolean(issue.closedAt)" + /> <issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" /> <issue-card-weight v-if="validIssueWeight" diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 47eee5306da..d1011c24977 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -15,6 +15,7 @@ function shouldCreateListGraphQL(label) { return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label)); } +// eslint-disable-next-line @gitlab/no-global-event-off $(document) .off('created.label') .on('created.label', (e, label, addNewList) => { diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index f90fe582566..9c90938fc52 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -7,6 +7,7 @@ import eventHub from '../eventhub'; import Api from '../../api'; import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; +import { ListType } from '../constants'; export default { name: 'BoardProjectSelect', @@ -53,7 +54,7 @@ export default { this.loading = true; const additionalAttrs = {}; - if (this.list.type && this.list.type !== 'backlog') { + if ((this.list.type || this.list.listType) !== ListType.backlog) { additionalAttrs.min_access_level = featureAccessLevel.EVERYONE; } diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue index 5fb7a9b210c..ce267be6d45 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue @@ -50,6 +50,13 @@ export default { } window.removeEventListener('click', this.collapseWhenOffClick); }, + toggle({ emitEvent = true } = {}) { + if (this.edit) { + this.collapse({ emitEvent }); + } else { + this.expand(); + } + }, }, }; </script> @@ -64,18 +71,18 @@ export default { <gl-button v-if="canUpdate" variant="link" - class="gl-text-gray-900!" + class="gl-text-gray-900! js-sidebar-dropdown-toggle" data-testid="edit-button" - @click="expand()" + @click="toggle" > {{ __('Edit') }} </gl-button> </div> - <div v-show="!edit" class="gl-text-gray-400" data-testid="collapsed-content"> + <div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content"> <slot name="collapsed">{{ __('None') }}</slot> </div> <div v-show="edit" data-testid="expanded-content"> - <slot></slot> + <slot :edit="edit"></slot> </div> </div> </template> 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 6935ead2706..904ceaed1b3 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 @@ -79,7 +79,7 @@ export default { <span class="gl-mx-2">-</span> <gl-button variant="link" - class="gl-text-gray-400!" + class="gl-text-gray-500!" data-testid="reset-button" :disabled="loading" @click="setDueDate(null)" 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 9d537a4ef2c..6a407bd6ba6 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 @@ -92,7 +92,7 @@ export default { @close="removeLabel(label.id)" /> </template> - <template> + <template #default="{ edit }"> <labels-select ref="labelsSelect" :allow-label-edit="false" @@ -105,6 +105,7 @@ export default { :labels-filter-base-path="labelsFilterBasePath" :labels-list-title="__('Select label')" :dropdown-button-text="__('Choose labels')" + :is-editing="edit" variant="embedded" class="gl-display-block labels gl-w-full" @updateSelectedLabels="setLabels" 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 new file mode 100644 index 00000000000..78c3f8acc62 --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue @@ -0,0 +1,161 @@ +<script> +import { mapGetters, mapActions } from 'vuex'; +import { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + GlDropdownDivider, + GlLoadingIcon, +} from '@gitlab/ui'; +import { fetchPolicies } from '~/lib/graphql'; +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'; + +export default { + components: { + BoardEditableItem, + GlDropdown, + GlLoadingIcon, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + GlDropdownDivider, + }, + data() { + return { + milestones: [], + searchTitle: '', + loading: false, + edit: false, + }; + }, + apollo: { + milestones: { + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, + query: groupMilestones, + debounce: 250, + skip() { + return !this.edit; + }, + variables() { + return { + fullPath: this.groupFullPath, + searchTitle: this.searchTitle, + state: 'active', + includeDescendants: true, + }; + }, + update(data) { + const edges = data?.group?.milestones?.edges ?? []; + return edges.map(item => item.node); + }, + error() { + createFlash({ message: this.$options.i18n.fetchMilestonesError }); + }, + }, + }, + computed: { + ...mapGetters({ issue: 'activeIssue' }), + hasMilestone() { + return this.issue.milestone !== null; + }, + groupFullPath() { + const { referencePath = '' } = this.issue; + return referencePath.slice(0, referencePath.indexOf('/')); + }, + projectPath() { + const { referencePath = '' } = this.issue; + return referencePath.slice(0, referencePath.indexOf('#')); + }, + dropdownText() { + return this.issue.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(); + }, + async setMilestone(milestoneId) { + this.loading = true; + this.searchTitle = ''; + this.$refs.sidebarItem.collapse(); + + try { + const input = { milestoneId, projectPath: this.projectPath }; + await this.setActiveIssueMilestone(input); + } catch (e) { + createFlash({ message: this.$options.i18n.updateMilestoneError }); + } finally { + this.loading = false; + } + }, + }, + i18n: { + milestone: __('Milestone'), + noMilestone: __('No milestone'), + assignMilestone: __('Assign milestone'), + noMilestonesFound: s__('Milestones|No milestones found'), + fetchMilestonesError: __('There was a problem fetching milestones.'), + updateMilestoneError: __('An error occurred while updating the milestone.'), + }, +}; +</script> + +<template> + <board-editable-item + ref="sidebarItem" + :title="$options.i18n.milestone" + :loading="loading" + @open="handleOpen()" + @close="edit = false" + > + <template v-if="hasMilestone" #collapsed> + <strong class="gl-text-gray-900">{{ issue.milestone.title }}</strong> + </template> + <template> + <gl-dropdown + ref="dropdown" + :text="dropdownText" + :header-text="$options.i18n.assignMilestone" + block + > + <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="!issue.milestone" + @click="setMilestone(null)" + > + {{ $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 + v-for="milestone in milestones" + :key="milestone.id" + :is-check-item="true" + :is-checked="issue.milestone && milestone.id === issue.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> + </board-editable-item> +</template> diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 49cb560594c..9264fac5eda 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -9,8 +9,6 @@ export const ListType = { backlog: 'backlog', closed: 'closed', label: 'label', - promotion: 'promotion', - blank: 'blank', }; export const inactiveId = 0; @@ -18,11 +16,7 @@ export const inactiveId = 0; export const ISSUABLE = 'issuable'; export const LIST = 'list'; -/* eslint-disable-next-line @gitlab/require-i18n-strings */ -export const DEFAULT_LABELS = ['to do', 'doing']; - export default { BoardType, ListType, - DEFAULT_LABELS, }; diff --git a/app/assets/javascripts/boards/ee_functions.js b/app/assets/javascripts/boards/ee_functions.js index 419a640d5c5..b6b34556663 100644 --- a/app/assets/javascripts/boards/ee_functions.js +++ b/app/assets/javascripts/boards/ee_functions.js @@ -1,5 +1,3 @@ -export const setPromotionState = () => {}; - export const setWeightFetchingState = () => {}; export const setEpicFetchingState = () => {}; diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 4fa78ecd5a4..1667dcc9f2e 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,7 +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 FilteredSearchContainer from '../filtered_search/container'; import boardsStore from './stores/boards_store'; +import vuexstore from './stores'; +import { updateHistory } from '~/lib/utils/url_utility'; export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { @@ -22,18 +25,28 @@ export default class FilteredSearchBoards extends FilteredSearchManager { this.isHandledAsync = true; this.cantEdit = cantEdit.filter(i => typeof i === 'string'); this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object'); + + if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) { + const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig); + if (boardConfigPath !== '') { + const filterPath = window.location.search ? `${window.location.search}&` : '?'; + updateHistory({ + url: `${filterPath}${transformBoardConfig(vuexstore.state.boardConfig)}`, + }); + } + } } updateObject(path) { const groupByParam = new URLSearchParams(window.location.search).get('group_by'); this.store.path = `${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`; - if (gon.features.boardsWithSwimlanes || gon.features.graphqlBoardLists) { - boardsStore.updateFiltersUrl(); - boardsStore.performSearch(); - } - - if (this.updateUrl) { + if (vuexstore.getters.shouldUseGraphQL) { + updateHistory({ + url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`, + }); + vuexstore.dispatch('performSearch'); + } else if (this.updateUrl) { boardsStore.updateFiltersUrl(); } } diff --git a/app/assets/javascripts/boards/queries/board.fragment.graphql b/app/assets/javascripts/boards/graphql/board.fragment.graphql index 872a4c4afbc..872a4c4afbc 100644 --- a/app/assets/javascripts/boards/queries/board.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/board.fragment.graphql diff --git a/app/assets/javascripts/boards/queries/board.mutation.graphql b/app/assets/javascripts/boards/graphql/board.mutation.graphql index ef2b81a7939..ef2b81a7939 100644 --- a/app/assets/javascripts/boards/queries/board.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/board.mutation.graphql diff --git a/app/assets/javascripts/boards/queries/board_labels.query.graphql b/app/assets/javascripts/boards/graphql/board_labels.query.graphql index 42a94419a97..42a94419a97 100644 --- a/app/assets/javascripts/boards/queries/board_labels.query.graphql +++ b/app/assets/javascripts/boards/graphql/board_labels.query.graphql diff --git a/app/assets/javascripts/boards/queries/board_list.fragment.graphql b/app/assets/javascripts/boards/graphql/board_list.fragment.graphql index bbf3314377e..bbf3314377e 100644 --- a/app/assets/javascripts/boards/queries/board_list.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/board_list.fragment.graphql diff --git a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql index 48420b349ae..f78a21baa7f 100644 --- a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql @@ -1,4 +1,4 @@ -#import "ee_else_ce/boards/queries/board_list.fragment.graphql" +#import "ee_else_ce/boards/graphql/board_list.fragment.graphql" mutation CreateBoardList( $boardId: BoardID! diff --git a/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql index ef3fd36e980..ef3fd36e980 100644 --- a/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql index d85b736720b..d85b736720b 100644 --- a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql diff --git a/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql index b474c9acb93..b474c9acb93 100644 --- a/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql diff --git a/app/assets/javascripts/boards/queries/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql index 88425e9a9c1..eb922f162f8 100644 --- a/app/assets/javascripts/boards/queries/board_lists.query.graphql +++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql @@ -1,4 +1,4 @@ -#import "ee_else_ce/boards/queries/board_list.fragment.graphql" +#import "ee_else_ce/boards/graphql/board_list.fragment.graphql" query ListIssues( $fullPath: ID! diff --git a/app/assets/javascripts/boards/queries/group_boards.query.graphql b/app/assets/javascripts/boards/graphql/group_boards.query.graphql index 74c224add7d..feafd6ae10d 100644 --- a/app/assets/javascripts/boards/queries/group_boards.query.graphql +++ b/app/assets/javascripts/boards/graphql/group_boards.query.graphql @@ -1,4 +1,4 @@ -#import "ee_else_ce/boards/queries/board.fragment.graphql" +#import "ee_else_ce/boards/graphql/board.fragment.graphql" query group_boards($fullPath: ID!) { group(fullPath: $fullPath) { diff --git a/app/assets/javascripts/boards/graphql/group_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_milestones.query.graphql new file mode 100644 index 00000000000..f2ab12ef4a7 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/group_milestones.query.graphql @@ -0,0 +1,17 @@ +query groupMilestones( + $fullPath: ID! + $state: MilestoneStateEnum + $includeDescendants: Boolean + $searchTitle: String +) { + group(fullPath: $fullPath) { + milestones(state: $state, includeDescendants: $includeDescendants, searchTitle: $searchTitle) { + edges { + node { + id + title + } + } + } + } +} diff --git a/app/assets/javascripts/boards/queries/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql index 4b429f875a6..1395bef39ed 100644 --- a/app/assets/javascripts/boards/queries/issue.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql @@ -11,6 +11,10 @@ fragment IssueNode on Issue { webUrl subscribed relativePosition + milestone { + id + title + } assignees { nodes { ...User diff --git a/app/assets/javascripts/boards/queries/issue_create.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_create.mutation.graphql index 65be147be07..c1a2361a4e8 100644 --- a/app/assets/javascripts/boards/queries/issue_create.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/issue_create.mutation.graphql @@ -1,4 +1,4 @@ -#import "ee_else_ce/boards/queries/issue.fragment.graphql" +#import "ee_else_ce/boards/graphql/issue.fragment.graphql" mutation CreateIssue($input: CreateIssueInput!) { createIssue(input: $input) { diff --git a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql index ff6aa597f48..3c574fd8c87 100644 --- a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql @@ -1,4 +1,4 @@ -#import "ee_else_ce/boards/queries/issue.fragment.graphql" +#import "ee_else_ce/boards/graphql/issue.fragment.graphql" mutation IssueMoveList( $projectPath: ID! diff --git a/app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql index bbea248cf85..bbea248cf85 100644 --- a/app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql diff --git a/app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql index 3c5f4b3e3bd..3c5f4b3e3bd 100644 --- a/app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql diff --git a/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql new file mode 100644 index 00000000000..5dc78a03a06 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql @@ -0,0 +1,12 @@ +mutation issueSetMilestone($input: UpdateIssueInput!) { + updateIssue(input: $input) { + issue { + milestone { + id + title + description + } + } + errors + } +} diff --git a/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql index 1f383245ac2..1f383245ac2 100644 --- a/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql +++ b/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql diff --git a/app/assets/javascripts/boards/queries/lists_issues.query.graphql b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql index 5dbfe4675c6..43af7d2b2f1 100644 --- a/app/assets/javascripts/boards/queries/lists_issues.query.graphql +++ b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql @@ -1,4 +1,4 @@ -#import "ee_else_ce/boards/queries/issue.fragment.graphql" +#import "ee_else_ce/boards/graphql/issue.fragment.graphql" query ListIssues( $fullPath: ID! diff --git a/app/assets/javascripts/boards/queries/project_boards.query.graphql b/app/assets/javascripts/boards/graphql/project_boards.query.graphql index a1326bd5eff..f98d25ba671 100644 --- a/app/assets/javascripts/boards/queries/project_boards.query.graphql +++ b/app/assets/javascripts/boards/graphql/project_boards.query.graphql @@ -1,4 +1,4 @@ -#import "ee_else_ce/boards/queries/board.fragment.graphql" +#import "ee_else_ce/boards/graphql/board.fragment.graphql" query project_boards($fullPath: ID!) { project(fullPath: $fullPath) { diff --git a/app/assets/javascripts/boards/queries/users_search.query.graphql b/app/assets/javascripts/boards/graphql/users_search.query.graphql index ca016495d79..ca016495d79 100644 --- a/app/assets/javascripts/boards/queries/users_search.query.graphql +++ b/app/assets/javascripts/boards/graphql/users_search.query.graphql diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index d3e40299d8d..64a4f246735 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { mapActions, mapGetters, mapState } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import 'ee_else_ce/boards/models/issue'; import 'ee_else_ce/boards/models/list'; @@ -9,7 +9,6 @@ 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 { - setPromotionState, setWeightFetchingState, setEpicFetchingState, getMilestoneTitle, @@ -41,7 +40,6 @@ import { NavigationType, convertObjectPropsToCamelCase, parseBoolean, - urlParamsToObject, } from '~/lib/utils/common_utils'; import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher'; @@ -77,7 +75,6 @@ export default () => { el: $boardApp, components: { BoardContent, - Board: () => import('ee_else_ce/boards/components/board_column.vue'), BoardSidebar, BoardAddIssuesModal, BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'), @@ -114,7 +111,6 @@ export default () => { }; }, computed: { - ...mapState(['isShowingEpicsSwimlanes']), ...mapGetters(['shouldUseGraphQL']), detailIssueVisible() { return Object.keys(this.detailIssue.issue).length; @@ -133,7 +129,17 @@ export default () => { ...endpoints, boardType: this.parent, disabled: this.disabled, - showPromotion: parseBoolean($boardApp.getAttribute('data-show-promotion')), + boardConfig: { + milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10), + milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '', + iterationId: parseInt($boardApp.dataset.boardIterationId, 10), + iterationTitle: $boardApp.dataset.boardIterationTitle || '', + assigneeUsername: $boardApp.dataset.boardAssigneeUsername, + labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels || []) : [], + weight: $boardApp.dataset.boardWeight + ? parseInt($boardApp.dataset.boardWeight, 10) + : null, + }, }); boardsStore.setEndpoints(endpoints); boardsStore.rootPath = this.boardsEndpoint; @@ -142,7 +148,6 @@ export default () => { eventHub.$on('newDetailIssue', this.updateDetailIssue); eventHub.$on('clearDetailIssue', this.clearDetailIssue); sidebarEventHub.$on('toggleSubscription', this.toggleSubscription); - eventHub.$on('performSearch', this.performSearch); eventHub.$on('initialBoardLoad', this.initialBoardLoad); }, beforeDestroy() { @@ -150,7 +155,6 @@ export default () => { eventHub.$off('newDetailIssue', this.updateDetailIssue); eventHub.$off('clearDetailIssue', this.clearDetailIssue); sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); - eventHub.$off('performSearch', this.performSearch); eventHub.$off('initialBoardLoad', this.initialBoardLoad); }, mounted() { @@ -166,22 +170,13 @@ export default () => { } }, methods: { - ...mapActions([ - 'setInitialBoardData', - 'setFilters', - 'fetchEpicsSwimlanes', - 'resetIssues', - 'resetEpics', - 'fetchLists', - ]), + ...mapActions(['setInitialBoardData', 'performSearch']), initialBoardLoad() { boardsStore .all() .then(res => res.data) .then(lists => { lists.forEach(list => boardsStore.addList(list)); - boardsStore.addBlankState(); - setPromotionState(boardsStore); this.loading = false; }) .catch(() => { @@ -191,17 +186,6 @@ export default () => { updateTokens() { this.filterManager.updateTokens(); }, - performSearch() { - this.setFilters(convertObjectPropsToCamelCase(urlParamsToObject(window.location.search))); - if (gon.features.boardsWithSwimlanes && this.isShowingEpicsSwimlanes) { - this.resetEpics(); - this.resetIssues(); - this.fetchEpicsSwimlanes({}); - } else if (gon.features.graphqlBoardLists && !this.isShowingEpicsSwimlanes) { - this.fetchLists(); - this.resetIssues(); - } - }, updateDetailIssue(newIssue, multiSelect = false) { const { sidebarInfoEndpoint } = newIssue; if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { @@ -303,7 +287,7 @@ export default () => { const issueBoardsModal = document.getElementById('js-add-issues-btn'); - if (issueBoardsModal) { + if (issueBoardsModal && gon.features.addIssuesButton) { // eslint-disable-next-line no-new new Vue({ el: issueBoardsModal, @@ -350,5 +334,8 @@ export default () => { toggleEpicsSwimlanes(); } - mountMultipleBoardsSwitcher(); + mountMultipleBoardsSwitcher({ + boardsEndpoint: $boardApp.dataset.boardsEndpoint, + recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, + }); }; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index 51bb72b7657..df65ebb7526 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -10,7 +10,7 @@ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); -export default () => { +export default (endpoints = {}) => { const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher'); return new Vue({ el: boardsSwitcherElement, @@ -35,6 +35,9 @@ export default () => { return { boardsSelectorProps }; }, + provide: { + endpoints, + }, render(createElement) { return createElement(BoardsSelector, { props: this.boardsSelectorProps, diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index dd950a45076..59b97eba9fe 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -1,9 +1,10 @@ import { pick } from 'lodash'; -import boardListsQuery from 'ee_else_ce/boards/queries/board_lists.query.graphql'; +import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { BoardType, ListType, inactiveId, DEFAULT_LABELS } from '~/boards/constants'; +import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils'; +import { BoardType, ListType, inactiveId } from '~/boards/constants'; import * as types from './mutation_types'; import { formatBoardLists, @@ -12,19 +13,20 @@ import { formatListsPageInfo, formatIssue, } from '../boards_util'; -import boardStore from '~/boards/stores/boards_store'; - -import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql'; -import listsIssuesQuery from '../queries/lists_issues.query.graphql'; -import boardLabelsQuery from '../queries/board_labels.query.graphql'; -import createBoardListMutation from '../queries/board_list_create.mutation.graphql'; -import updateBoardListMutation from '../queries/board_list_update.mutation.graphql'; -import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql'; -import destroyBoardListMutation from '../queries/board_list_destroy.mutation.graphql'; -import issueCreateMutation from '../queries/issue_create.mutation.graphql'; -import issueSetLabels from '../queries/issue_set_labels.mutation.graphql'; -import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql'; -import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql'; +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 issueCreateMutation from '../graphql/issue_create.mutation.graphql'; +import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql'; +import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql'; +import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql'; +import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql'; const notImplemented = () => { /* eslint-disable-next-line @gitlab/require-i18n-strings */ @@ -63,6 +65,18 @@ export default { commit(types.SET_FILTERS, filterParams); }, + performSearch({ dispatch }) { + dispatch( + 'setFilters', + convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)), + ); + + if (gon.features.graphqlBoardLists) { + dispatch('fetchLists'); + dispatch('resetIssues'); + } + }, + fetchLists: ({ commit, state, dispatch }) => { const { endpoints, boardType, filterParams } = state; const { fullPath, boardId } = endpoints; @@ -87,7 +101,6 @@ export default { if (!lists.nodes.find(l => l.listType === ListType.backlog) && !hideBacklogList) { dispatch('createList', { backlog: true }); } - dispatch('generateDefaultLists'); }) .catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE)); }, @@ -118,15 +131,9 @@ export default { }, addList: ({ commit }, list) => { - // Temporarily using positioning logic from boardStore - commit( - types.RECEIVE_ADD_LIST_SUCCESS, - boardStore.updateListPosition({ ...list, doNotFetchIssues: true }), - ); + commit(types.RECEIVE_ADD_LIST_SUCCESS, list); }, - showPromotionList: () => {}, - fetchLabels: ({ state, commit }, searchTerm) => { const { endpoints, boardType } = state; const { fullPath } = endpoints; @@ -150,35 +157,14 @@ export default { .catch(() => commit(types.RECEIVE_LABELS_FAILURE)); }, - generateDefaultLists: async ({ state, commit, dispatch }) => { - if (state.disabled) { - return; - } - if ( - Object.entries(state.boardLists).find( - ([, list]) => list.type !== ListType.backlog && list.type !== ListType.closed, - ) - ) { - return; - } - - const fetchLabelsAndCreateList = label => { - return dispatch('fetchLabels', label) - .then(res => { - if (res.length > 0) { - dispatch('createList', { labelId: res[0].id }); - } - }) - .catch(() => commit(types.GENERATE_DEFAULT_LISTS_FAILURE)); - }; - - await Promise.all(DEFAULT_LABELS.map(fetchLabelsAndCreateList)); - }, - moveList: ( { state, commit, dispatch }, { listId, replacedListId, newIndex, adjustmentValue }, ) => { + if (listId === replacedListId) { + return; + } + const { boardLists } = state; const backupList = { ...boardLists }; const movedList = boardLists[listId]; @@ -315,9 +301,11 @@ export default { }, setAssignees: ({ commit, getters }, assigneeUsernames) => { + commit(types.SET_ASSIGNEE_LOADING, true); + return gqlClient .mutate({ - mutation: updateAssignees, + mutation: updateAssigneesMutation, variables: { iid: getters.activeIssue.iid, projectPath: getters.activeIssue.referencePath.split('#')[0], @@ -325,14 +313,48 @@ export default { }, }) .then(({ data }) => { + const { nodes } = data.issueSetAssignees?.issue?.assignees || []; + commit('UPDATE_ISSUE_BY_ID', { issueId: getters.activeIssue.id, prop: 'assignees', - value: data.issueSetAssignees.issue.assignees.nodes, + value: nodes, }); + + return nodes; + }) + .catch(() => { + createFlash({ message: __('An error occurred while updating assignees.') }); + }) + .finally(() => { + commit(types.SET_ASSIGNEE_LOADING, false); }); }, + setActiveIssueMilestone: async ({ commit, getters }, input) => { + const { activeIssue } = getters; + const { data } = await gqlClient.mutate({ + mutation: issueSetMilestoneMutation, + variables: { + input: { + iid: String(activeIssue.iid), + milestoneId: getIdFromGraphQLId(input.milestoneId), + projectPath: input.projectPath, + }, + }, + }); + + if (data.updateIssue.errors?.length > 0) { + throw new Error(data.updateIssue.errors); + } + + commit(types.UPDATE_ISSUE_BY_ID, { + issueId: activeIssue.id, + prop: 'milestone', + value: data.updateIssue.issue.milestone, + }); + }, + createNewIssue: ({ commit, state }, issueInput) => { const input = issueInput; const { boardType, endpoints } = state; @@ -378,7 +400,7 @@ export default { setActiveIssueLabels: async ({ commit, getters }, input) => { const { activeIssue } = getters; const { data } = await gqlClient.mutate({ - mutation: issueSetLabels, + mutation: issueSetLabelsMutation, variables: { input: { iid: String(activeIssue.iid), @@ -403,7 +425,7 @@ export default { setActiveIssueDueDate: async ({ commit, getters }, input) => { const { activeIssue } = getters; const { data } = await gqlClient.mutate({ - mutation: issueSetDueDate, + mutation: issueSetDueDateMutation, variables: { input: { iid: String(activeIssue.iid), diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 337b2897fe9..36702b6ca5f 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -1,9 +1,8 @@ /* eslint-disable no-shadow, no-param-reassign,consistent-return */ /* global List */ /* global ListIssue */ -import { sortBy, pick } from 'lodash'; +import { sortBy } from 'lodash'; import Vue from 'vue'; -import Cookies from 'js-cookie'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import { urlParamsToObject, @@ -22,8 +21,6 @@ import ListLabel from '../models/label'; import ListAssignee from '../models/assignee'; import ListMilestone from '../models/milestone'; -import createBoardMutation from '../queries/board.mutation.graphql'; - const PER_PAGE = 20; export const gqlClient = createDefaultClient(); @@ -125,20 +122,6 @@ const boardsStore = { .querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`) ?.classList.remove('is-active'); }, - shouldAddBlankState() { - // Decide whether to add the blank state - return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]; - }, - addBlankState() { - if (!this.shouldAddBlankState() || this.welcomeIsHidden()) return; - - this.generateDefaultLists() - .then(res => res.data) - .then(data => Promise.all(data.map(list => this.addList(list)))) - .catch(() => { - this.removeList(undefined, 'label'); - }); - }, findIssueLabel(issue, findLabel) { return issue.labels.find(label => label.id === findLabel.id); @@ -202,9 +185,6 @@ const boardsStore = { return list.issues.find(issue => issue.id === id); }, - welcomeIsHidden() { - return parseBoolean(Cookies.get('issue_board_welcome_hidden')); - }, removeList(id, type = 'blank') { const list = this.findList('id', id, type); @@ -302,11 +282,7 @@ const boardsStore = { onNewListIssueResponse(list, issue, data) { issue.refreshData(data); - if ( - !gon.features.boardsWithSwimlanes && - !gon.features.graphqlBoardLists && - list.issues.length > 1 - ) { + if (list.issues.length > 1) { const moveBeforeId = list.issues[1].id; this.moveIssue(issue.id, null, null, null, moveBeforeId); } @@ -516,10 +492,6 @@ const boardsStore = { eventHub.$emit('updateTokens'); }, - performSearch() { - eventHub.$emit('performSearch'); - }, - setListDetail(newList) { this.detail.list = newList; }, @@ -566,10 +538,6 @@ const boardsStore = { return axios.get(this.state.endpoints.listsEndpoint); }, - generateDefaultLists() { - return axios.post(this.state.endpoints.listsEndpointGenerate, {}); - }, - createList(entityId, entityType) { const list = { [entityType]: entityId, @@ -785,52 +753,6 @@ const boardsStore = { return axios.get(this.state.endpoints.recentBoardsEndpoint); }, - createBoard(board) { - const boardPayload = { ...board }; - boardPayload.label_ids = (board.labels || []).map(b => b.id); - - if (boardPayload.label_ids.length === 0) { - boardPayload.label_ids = ['']; - } - - if (boardPayload.assignee) { - boardPayload.assignee_id = boardPayload.assignee.id; - } - - if (boardPayload.milestone) { - boardPayload.milestone_id = boardPayload.milestone.id; - } - - if (boardPayload.id) { - const input = { - ...pick(boardPayload, ['hideClosedList', 'hideBacklogList']), - id: this.generateBoardGid(boardPayload.id), - }; - - return Promise.all([ - axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload }), - gqlClient.mutate({ - mutation: createBoardMutation, - variables: input, - }), - ]); - } - - return axios - .post(this.generateBoardsPath(), { board: boardPayload }) - .then(resp => resp.data) - .then(data => { - gqlClient.mutate({ - mutation: createBoardMutation, - variables: { - ...pick(boardPayload, ['hideClosedList', 'hideBacklogList']), - id: this.generateBoardGid(data.id), - }, - }); - return data; - }); - }, - deleteBoard({ id }) { return axios.delete(this.generateBoardsPath(id)); }, diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index cd28b4a0ff7..ca6887b6f45 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -2,15 +2,8 @@ import { find } from 'lodash'; import { inactiveId } from '../constants'; export default { - labelToggleState: state => (state.isShowingLabels ? 'on' : 'off'), isSidebarOpen: state => state.activeId !== inactiveId, - isSwimlanesOn: state => { - if (!gon?.features?.boardsWithSwimlanes && !gon?.features?.swimlanes) { - return false; - } - - return state.isShowingEpicsSwimlanes; - }, + isSwimlanesOn: () => false, getIssueById: state => id => { return state.issues[id] || {}; }, diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 3a57cb9b5e1..2b2c2bee51c 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -34,4 +34,5 @@ export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID'; +export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING'; export const RESET_ISSUES = 'RESET_ISSUES'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index bb083158c8f..8c4e514710f 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -13,7 +13,7 @@ const notImplemented = () => { export const removeIssueFromList = ({ state, listId, issueId }) => { Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId)); const list = state.boardLists[listId]; - Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize - 1 }); + Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount - 1 }); }; export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => { @@ -27,16 +27,16 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter listIssues.splice(newIndex, 0, issueId); Vue.set(state.issuesByListId, listId, listIssues); const list = state.boardLists[listId]; - Vue.set(state.boardLists, listId, { ...list, issuesSize: list.issuesSize + 1 }); + Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount + 1 }); }; export default { [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) { - const { boardType, disabled, showPromotion, ...endpoints } = data; + const { boardType, disabled, boardConfig, ...endpoints } = data; state.endpoints = endpoints; state.boardType = boardType; state.disabled = disabled; - state.showPromotion = showPromotion; + state.boardConfig = boardConfig; }, [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => { @@ -143,6 +143,10 @@ export default { Vue.set(state.issues[issueId], prop, value); }, + [mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) { + state.isSettingAssignees = isLoading; + }, + [mutationTypes.REQUEST_ADD_ISSUE]: () => { notImplemented(); }, diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index b91c09f8051..573e98e56e0 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -4,16 +4,17 @@ export default () => ({ endpoints: {}, boardType: null, disabled: false, - showPromotion: false, isShowingLabels: true, activeId: inactiveId, sidebarType: '', boardLists: {}, listsFlags: {}, issuesByListId: {}, + isSettingAssignees: false, pageInfoByListId: {}, issues: {}, filterParams: {}, + boardConfig: {}, error: undefined, // TODO: remove after ce/ee split of board_content.vue isShowingEpicsSwimlanes: false, |