diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 16:18:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 16:18:24 +0300 |
commit | 0653e08efd039a5905f3fa4f6e9cef9f5d2f799c (patch) | |
tree | 4dcc884cf6d81db44adae4aa99f8ec1233a41f55 /app/assets/javascripts/boards | |
parent | 744144d28e3e7fddc117924fef88de5d9674fe4c (diff) |
Add latest changes from gitlab-org/gitlab@14-3-stable-eev14.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards')
48 files changed, 295 insertions, 3957 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index 3219d74f85f..d113a1d39d8 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,6 +1,5 @@ import { sortBy, cloneDeep } from 'lodash'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { ListType } from './constants'; +import { ListType, MilestoneIDs } from './constants'; export function getMilestone() { return null; @@ -49,12 +48,10 @@ export function formatListIssues(listIssues) { return { ...map, [list.id]: sortedIssues.map((i) => { - const id = getIdFromGraphQLId(i.id); + const { id } = i; const listIssue = { ...i, - id, - fullId: i.id, labels: i.labels?.nodes || [], assignees: i.assignees?.nodes || [], }; @@ -108,7 +105,10 @@ export function formatIssueInput(issueInput, boardConfig) { return { ...issueInput, - milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null, + milestoneId: + milestoneId && milestoneId !== MilestoneIDs.ANY + ? fullMilestoneId(milestoneId) + : issueInput?.milestoneId, labelIds: [...labelIds, ...(labels?.map((l) => fullLabelId(l)) || [])], assigneeIds: [...assigneeIds, ...(assigneeId ? [fullUserId(assigneeId)] : [])], }; diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue index d4b559add6e..22ad619e76b 100644 --- a/app/assets/javascripts/boards/components/board_add_new_column.vue +++ b/app/assets/javascripts/boards/components/board_add_new_column.vue @@ -2,9 +2,6 @@ import { GlFormRadio, GlFormRadioGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; -import { ListType } from '~/boards/constants'; -import boardsStore from '~/boards/stores/boards_store'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; export default { components: { @@ -24,7 +21,7 @@ export default { }, computed: { ...mapState(['labels', 'labelsLoading']), - ...mapGetters(['getListByLabelId', 'shouldUseGraphQL']), + ...mapGetters(['getListByLabelId']), columnForSelected() { return this.getListByLabelId(this.selectedId); }, @@ -34,17 +31,6 @@ export default { }, methods: { ...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']), - highlight(listId) { - if (this.shouldUseGraphQL) { - this.highlightList(listId); - } else { - const list = boardsStore.state.lists.find(({ id }) => id === listId); - list.highlighted = true; - setTimeout(() => { - list.highlighted = false; - }, 2000); - } - }, addList() { if (!this.selectedLabel) { return; @@ -54,23 +40,11 @@ export default { if (this.columnForSelected) { const listId = this.columnForSelected.id; - this.highlight(listId); + this.highlightList(listId); return; } - if (this.shouldUseGraphQL) { - this.createList({ labelId: this.selectedId }); - } else { - const listObj = { - labelId: getIdFromGraphQLId(this.selectedId), - title: this.selectedLabel.title, - position: boardsStore.state.lists.length - 2, - list_type: ListType.label, - label: this.selectedLabel, - }; - - boardsStore.new(listObj); - } + this.createList({ labelId: this.selectedId }); }, filterItems(searchTerm) { diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue new file mode 100644 index 00000000000..28f4a267077 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_app.vue @@ -0,0 +1,29 @@ +<script> +import { mapActions, mapGetters } from 'vuex'; +import BoardContent from '~/boards/components/board_content.vue'; +import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue'; + +export default { + components: { + BoardContent, + BoardSettingsSidebar, + }, + inject: ['disabled'], + computed: { + ...mapGetters(['isSidebarOpen']), + }, + mounted() { + this.performSearch(); + }, + methods: { + ...mapActions(['performSearch']), + }, +}; +</script> + +<template> + <div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }"> + <board-content :disabled="disabled" /> + <board-settings-sidebar /> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/board_card_deprecated.vue b/app/assets/javascripts/boards/components/board_card_deprecated.vue deleted file mode 100644 index e12a2836f67..00000000000 --- a/app/assets/javascripts/boards/components/board_card_deprecated.vue +++ /dev/null @@ -1,61 +0,0 @@ -<script> -// This component is being replaced in favor of './board_card.vue' for GraphQL boards -import sidebarEventHub from '~/sidebar/event_hub'; -import eventHub from '../eventhub'; -import boardsStore from '../stores/boards_store'; -import BoardCardLayoutDeprecated from './board_card_layout_deprecated.vue'; - -export default { - components: { - BoardCardLayout: BoardCardLayoutDeprecated, - }, - props: { - list: { - type: Object, - default: () => ({}), - required: false, - }, - issue: { - type: Object, - default: () => ({}), - required: false, - }, - }, - methods: { - // These are methods instead of computed's, because boardsStore is not reactive. - isActive() { - return this.getActiveId() === this.issue.id; - }, - getActiveId() { - return boardsStore.detail?.issue?.id; - }, - showIssue({ isMultiSelect }) { - // If no issues are opened, close all sidebars first - if (!this.getActiveId()) { - sidebarEventHub.$emit('sidebar.closeAll'); - } - if (this.isActive()) { - eventHub.$emit('clearDetailIssue', isMultiSelect); - - if (isMultiSelect) { - eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); - } - } else { - eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); - boardsStore.setListDetail(this.list); - } - }, - }, -}; -</script> - -<template> - <board-card-layout - data-qa-selector="board_card" - :issue="issue" - :list="list" - :is-active="isActive()" - v-bind="$attrs" - @show="showIssue" - /> -</template> diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue index 5658a34e9a6..db80d48239b 100644 --- a/app/assets/javascripts/boards/components/board_card_inner.vue +++ b/app/assets/javascripts/boards/components/board_card_inner.vue @@ -214,10 +214,19 @@ export default { class="confidential-icon gl-mr-2" :aria-label="__('Confidential')" /> + <gl-icon + v-if="item.hidden" + v-gl-tooltip + name="spam" + :title="__('This issue is hidden because its author has been banned')" + class="gl-mr-2 hidden-icon" + data-testid="hidden-icon" + /> <a :href="item.path || item.webUrl || ''" :title="item.title" :class="{ 'gl-text-gray-400!': item.isLoading }" + class="js-no-trigger" @mousemove.stop >{{ item.title }}</a > diff --git a/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue deleted file mode 100644 index 3381e4c3a7d..00000000000 --- a/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue +++ /dev/null @@ -1,101 +0,0 @@ -<script> -import { mapActions, mapGetters } from 'vuex'; -import { ISSUABLE } from '~/boards/constants'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import boardsStore from '../stores/boards_store'; -import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue'; - -export default { - name: 'BoardCardLayout', - components: { - IssueCardInner: IssueCardInnerDeprecated, - }, - mixins: [glFeatureFlagMixin()], - props: { - list: { - type: Object, - default: () => ({}), - required: false, - }, - issue: { - type: Object, - default: () => ({}), - required: false, - }, - disabled: { - type: Boolean, - default: false, - required: false, - }, - index: { - type: Number, - default: 0, - required: false, - }, - isActive: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - showDetail: false, - multiSelect: boardsStore.multiSelect, - }; - }, - computed: { - ...mapGetters(['isSwimlanesOn']), - multiSelectVisible() { - return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1; - }, - }, - methods: { - ...mapActions(['setActiveId']), - mouseDown() { - this.showDetail = true; - }, - mouseMove() { - this.showDetail = false; - }, - showIssue(e) { - // Don't do anything if this happened on a no trigger element - if (e.target.classList.contains('js-no-trigger')) return; - - if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) { - this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE }); - return; - } - - const isMultiSelect = e.ctrlKey || e.metaKey; - - if (this.showDetail || isMultiSelect) { - this.showDetail = false; - this.$emit('show', { event: e, isMultiSelect }); - } - }, - }, -}; -</script> - -<template> - <li - :class="{ - 'multi-select': multiSelectVisible, - 'user-can-drag': !disabled && issue.id, - 'is-disabled': disabled || !issue.id, - 'is-active': isActive, - }" - :index="index" - :data-issue-id="issue.id" - :data-issue-iid="issue.iid" - :data-issue-path="issue.referencePath" - data-testid="board_card" - class="board-card gl-p-5 gl-rounded-base" - @mousedown="mouseDown" - @mousemove="mouseMove" - @mouseup="showIssue($event)" - > - <issue-card-inner :list="list" :issue="issue" :update-filters="true" /> - </li> -</template> diff --git a/app/assets/javascripts/boards/components/board_column_deprecated.vue b/app/assets/javascripts/boards/components/board_column_deprecated.vue deleted file mode 100644 index 7c090dfaa53..00000000000 --- a/app/assets/javascripts/boards/components/board_column_deprecated.vue +++ /dev/null @@ -1,112 +0,0 @@ -<script> -// This component is being replaced in favor of './board_column.vue' for GraphQL boards -import Sortable from 'sortablejs'; -import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_deprecated.vue'; -import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; -import boardsStore from '../stores/boards_store'; -import BoardList from './board_list_deprecated.vue'; - -export default { - components: { - BoardListHeader, - BoardList, - }, - inject: { - boardId: { - default: '', - }, - }, - props: { - list: { - type: Object, - default: () => ({}), - required: false, - }, - disabled: { - type: Boolean, - required: true, - }, - }, - data() { - return { - detailIssue: boardsStore.detail, - filter: boardsStore.filter, - }; - }, - computed: { - listIssues() { - return this.list.issues; - }, - }, - watch: { - filter: { - handler() { - // eslint-disable-next-line vue/no-mutating-props - this.list.page = 1; - this.list.getIssues(true).catch(() => { - // TODO: handle request error - }); - }, - deep: true, - }, - 'list.highlighted': { - handler(highlighted) { - if (highlighted) { - this.$nextTick(() => { - this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }); - } - }, - immediate: true, - }, - }, - mounted() { - const instance = this; - - const sortableOptions = getBoardSortableDefaultOptions({ - disabled: this.disabled, - group: 'boards', - draggable: '.is-draggable', - handle: '.js-board-handle', - onEnd(e) { - sortableEnd(); - - const sortable = this; - - if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { - const order = sortable.toArray(); - const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10)); - - instance.$nextTick(() => { - boardsStore.moveList(list, order); - }); - } - }, - }); - - Sortable.create(this.$el.parentNode, sortableOptions); - }, -}; -</script> - -<template> - <div - :class="{ - 'is-draggable': !list.preset, - 'is-expandable': list.isExpandable, - 'is-collapsed': !list.isExpanded, - 'board-type-assignee': list.type === '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" - data-qa-selector="board_list" - > - <div - class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base" - :class="{ 'board-column-highlighted': list.highlighted }" - > - <board-list-header :list="list" :disabled="disabled" /> - <board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index 4df6ff75249..27ea2e7a608 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -5,31 +5,22 @@ import Draggable from 'vuedraggable'; import { mapState, mapGetters, mapActions } from 'vuex'; import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue'; import defaultSortableConfig from '~/sortable/sortable_config'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { DraggableItemTypes } from '../constants'; import BoardColumn from './board_column.vue'; -import BoardColumnDeprecated from './board_column_deprecated.vue'; export default { draggableItemTypes: DraggableItemTypes, components: { BoardAddNewColumn, BoardColumn, - BoardColumnDeprecated, BoardContentSidebar: () => import('~/boards/components/board_content_sidebar.vue'), EpicBoardContentSidebar: () => import('ee_component/boards/components/epic_board_content_sidebar.vue'), EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), GlAlert, }, - mixins: [glFeatureFlagMixin()], inject: ['canAdminList'], props: { - lists: { - type: Array, - required: false, - default: () => [], - }, disabled: { type: Boolean, required: true, @@ -37,20 +28,15 @@ export default { }, computed: { ...mapState(['boardLists', 'error', 'addColumnForm']), - ...mapGetters(['isSwimlanesOn', 'isEpicBoard']), - useNewBoardColumnComponent() { - return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard; - }, + ...mapGetters(['isSwimlanesOn', 'isEpicBoard', 'isIssueBoard']), addColumnFormVisible() { return this.addColumnForm?.visible; }, boardListsToUse() { - return this.useNewBoardColumnComponent - ? sortBy([...Object.values(this.boardLists)], 'position') - : this.lists; + return sortBy([...Object.values(this.boardLists)], 'position'); }, canDragColumns() { - return (this.isEpicBoard || this.glFeatures.graphqlBoardLists) && this.canAdminList; + return this.canAdminList; }, boardColumnWrapper() { return this.canDragColumns ? Draggable : 'div'; @@ -68,9 +54,6 @@ export default { return this.canDragColumns ? options : {}; }, - boardColumnComponent() { - return this.useNewBoardColumnComponent ? BoardColumn : BoardColumnDeprecated; - }, }, methods: { ...mapActions(['moveList', 'unsetError']), @@ -95,8 +78,7 @@ export default { class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap" @end="moveList" > - <component - :is="boardColumnComponent" + <board-column v-for="(list, index) in boardListsToUse" :key="index" ref="board" @@ -118,10 +100,7 @@ export default { :disabled="disabled" /> - <board-content-sidebar - v-if="isSwimlanesOn || glFeatures.graphqlBoardLists" - data-testid="issue-boards-sidebar" - /> + <board-content-sidebar v-if="isIssueBoard" data-testid="issue-boards-sidebar" /> <epic-board-content-sidebar v-else-if="isEpicBoard" data-testid="epic-boards-sidebar" /> </div> diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index 7a936e75676..e0105d63d99 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -96,7 +96,7 @@ export default { <template #header> <sidebar-todo-widget class="gl-mt-3" - :issuable-id="activeBoardItem.fullId" + :issuable-id="activeBoardItem.id" :issuable-iid="activeBoardItem.iid" :full-path="fullPath" :issuable-type="issuableType" diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index a89f71504a9..e939f0c0ebe 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -1,8 +1,7 @@ <script> import { GlModal, GlAlert } from '@gitlab/ui'; import { mapGetters, mapActions, mapState } from 'vuex'; -import ListLabel from '~/boards/models/label'; -import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants'; +import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { getParameterByName, visitUrl } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; @@ -189,7 +188,9 @@ export default { issueBoardScopeMutationVariables() { return { weight: this.board.weight, - assigneeId: this.board.assignee?.id || null, + assigneeId: this.board.assignee?.id + ? convertToGraphQLId(TYPE_USER, this.board.assignee.id) + : null, milestoneId: this.board.milestone?.id ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id) : null, @@ -289,14 +290,10 @@ export default { setBoardLabels(labels) { labels.forEach((label) => { if (label.set && !this.board.labels.find((l) => l.id === label.id)) { - this.board.labels.push( - new ListLabel({ - id: label.id, - title: label.title, - color: label.color, - textColor: label.text_color, - }), - ); + this.board.labels.push({ + ...label, + textColor: label.text_color, + }); } else if (!label.set) { this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id); } diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 849492effab..47dffc985aa 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -208,7 +208,7 @@ export default { newIndex = children.length; } - const getItemId = (el) => Number(el.dataset.itemId); + const getItemId = (el) => el.dataset.itemId; // If item is being moved within the same list if (from === to) { @@ -234,7 +234,7 @@ export default { } this.moveItem({ - itemId: Number(itemId), + itemId, itemIid, itemPath, fromListId: from.dataset.listId, diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue deleted file mode 100644 index fabaf7a85f5..00000000000 --- a/app/assets/javascripts/boards/components/board_list_deprecated.vue +++ /dev/null @@ -1,459 +0,0 @@ -<script> -import { GlLoadingIcon } from '@gitlab/ui'; -import { Sortable, MultiDrag } from 'sortablejs'; -import createFlash from '~/flash'; -import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; -import { sprintf, __ } from '~/locale'; -import eventHub from '../eventhub'; -import { - getBoardSortableDefaultOptions, - sortableStart, - sortableEnd, -} from '../mixins/sortable_default_options'; -import boardsStore from '../stores/boards_store'; -import boardCard from './board_card_deprecated.vue'; -import boardNewIssue from './board_new_issue_deprecated.vue'; - -// This component is being replaced in favor of './board_list.vue' for GraphQL boards - -Sortable.mount(new MultiDrag()); - -export default { - name: 'BoardList', - components: { - boardCard, - boardNewIssue, - GlLoadingIcon, - }, - props: { - disabled: { - type: Boolean, - required: true, - }, - list: { - type: Object, - required: true, - }, - issues: { - type: Array, - required: true, - }, - }, - data() { - return { - scrollOffset: 250, - filters: boardsStore.state.filters, - showCount: false, - showIssueForm: false, - }; - }, - computed: { - paginatedIssueText() { - return sprintf(__('Showing %{pageSize} of %{total} issues'), { - pageSize: this.list.issues.length, - total: this.list.issuesSize, - }); - }, - issuesSizeExceedsMax() { - return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount; - }, - loading() { - return this.list.loading; - }, - }, - watch: { - filters: { - handler() { - // eslint-disable-next-line vue/no-mutating-props - this.list.loadingMore = false; - this.$refs.list.scrollTop = 0; - }, - deep: true, - }, - issues() { - this.$nextTick(() => { - if ( - this.scrollHeight() <= this.listHeight() && - this.list.issuesSize > this.list.issues.length && - this.list.isExpanded - ) { - // eslint-disable-next-line vue/no-mutating-props - this.list.page += 1; - this.list.getIssues(false).catch(() => { - // TODO: handle request error - }); - } - - if (this.scrollHeight() > Math.ceil(this.listHeight())) { - this.showCount = true; - } else { - this.showCount = false; - } - }); - }, - 'list.id': { - handler(id) { - if (id) { - eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm); - } - }, - }, - }, - created() { - eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm); - eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); - }, - mounted() { - const multiSelectOpts = { - multiDrag: true, - selectedClass: 'js-multi-select', - animation: 500, - }; - - const options = getBoardSortableDefaultOptions({ - scroll: true, - disabled: this.disabled, - filter: '.board-list-count, .is-disabled', - dataIdAttr: 'data-issue-id', - removeCloneOnHide: false, - ...multiSelectOpts, - group: { - name: 'issues', - /** - * Dynamically determine between which containers - * items can be moved or copied as - * Assignee lists (EE feature) require this behavior - */ - pull: (to, from, dragEl, e) => { - // As per Sortable's docs, `to` should provide - // reference to exact sortable container on which - // we're trying to drag element, but either it is - // a library's bug or our markup structure is too complex - // that `to` never points to correct container - // See https://github.com/RubaXa/Sortable/issues/1037 - // - // So we use `e.target` which is always accurate about - // which element we're currently dragging our card upon - // So from there, we can get reference to actual container - // and thus the container type to enable Copy or Move - if (e.target) { - const containerEl = - e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); - const toBoardType = containerEl.dataset.boardType; - const cloneActions = { - label: ['milestone', 'assignee', 'iteration'], - assignee: ['milestone', 'label', 'iteration'], - milestone: ['label', 'assignee', 'iteration'], - iteration: ['label', 'assignee', 'milestone'], - }; - - if (toBoardType) { - const fromBoardType = this.list.type; - // For each list we check if the destination list is - // a the list were we should clone the issue - const shouldClone = Object.entries(cloneActions).some( - (entry) => fromBoardType === entry[0] && entry[1].includes(toBoardType), - ); - - if (shouldClone) { - return 'clone'; - } - } - } - - return true; - }, - revertClone: true, - }, - onStart: (e) => { - const card = this.$refs.issue[e.oldIndex]; - - card.showDetail = false; - - const { list } = card; - - const issue = list.findIssue(Number(e.item.dataset.issueId)); - - boardsStore.startMoving(list, issue); - - this.$root.$emit(BV_HIDE_TOOLTIP); - - sortableStart(); - }, - onAdd: (e) => { - const { items = [], newIndicies = [] } = e; - if (items.length) { - // Not using e.newIndex here instead taking a min of all - // the newIndicies. Basically we have to find that during - // a drop what is the index we're going to start putting - // all the dropped elements from. - const newIndex = Math.min(...newIndicies.map((obj) => obj.index).filter((i) => i !== -1)); - const issues = items.map((item) => - boardsStore.moving.list.findIssue(Number(item.dataset.issueId)), - ); - - boardsStore.moveMultipleIssuesToList({ - listFrom: boardsStore.moving.list, - listTo: this.list, - issues, - newIndex, - }); - } else { - boardsStore.moveIssueToList( - boardsStore.moving.list, - this.list, - boardsStore.moving.issue, - e.newIndex, - ); - this.$nextTick(() => { - e.item.remove(); - }); - } - }, - onUpdate: (e) => { - const sortedArray = this.sortable.toArray().filter((id) => id !== '-1'); - - const { items = [], newIndicies = [], oldIndicies = [] } = e; - if (items.length) { - const newIndex = Math.min(...newIndicies.map((obj) => obj.index)); - const issues = items.map((item) => - boardsStore.moving.list.findIssue(Number(item.dataset.issueId)), - ); - boardsStore.moveMultipleIssuesInList({ - list: this.list, - issues, - oldIndicies: oldIndicies.map((obj) => obj.index), - newIndex, - idArray: sortedArray, - }); - e.items.forEach((el) => { - Sortable.utils.deselect(el); - }); - boardsStore.clearMultiSelect(); - return; - } - - boardsStore.moveIssueInList( - this.list, - boardsStore.moving.issue, - e.oldIndex, - e.newIndex, - sortedArray, - ); - }, - onEnd: (e) => { - const { items = [], clones = [], to } = e; - - // This is not a multi select operation - if (!items.length && !clones.length) { - sortableEnd(); - return; - } - - let toList; - if (to) { - const containerEl = to.closest('.js-board-list'); - toList = boardsStore.findList('id', Number(containerEl.dataset.board)); - } - - /** - * onEnd is called irrespective if the cards were moved in the - * same list or the other list. Don't remove items if it's same list. - */ - const isSameList = toList && toList.id === this.list.id; - if (toList && !isSameList && boardsStore.shouldRemoveIssue(this.list, toList)) { - const issues = items.map((item) => this.list.findIssue(Number(item.dataset.issueId))); - if ( - issues.filter(Boolean).length && - !boardsStore.issuesAreContiguous(this.list, issues) - ) { - const indexes = []; - const ids = this.list.issues.map((i) => i.id); - issues.forEach((issue) => { - const index = ids.indexOf(issue.id); - if (index > -1) { - indexes.push(index); - } - }); - - // Descending sort because splice would cause index discrepancy otherwise - const sortedIndexes = indexes.sort((a, b) => (a < b ? 1 : -1)); - - sortedIndexes.forEach((i) => { - /** - * **setTimeout and splice each element one-by-one in a loop - * is intended.** - * - * The problem here is all the indexes are in the list but are - * non-contiguous. Due to that, when we splice all the indexes, - * at once, Vue -- during a re-render -- is unable to find reference - * nodes and the entire app crashes. - * - * If the indexes are contiguous, this piece of code is not - * executed. If it is, this is a possible regression. Only when - * issue indexes are far apart, this logic should ever kick in. - */ - setTimeout(() => { - // eslint-disable-next-line vue/no-mutating-props - this.list.issues.splice(i, 1); - }, 0); - }); - } - } - - if (!toList) { - createFlash({ - message: __('Something went wrong while performing the action.'), - }); - } - - if (!isSameList) { - boardsStore.clearMultiSelect(); - - // Since Vue's list does not re-render the same keyed item, we'll - // remove `multi-select` class to express it's unselected - if (clones && clones.length) { - clones.forEach((el) => el.classList.remove('multi-select')); - } - - // Due to some bug which I am unable to figure out - // Sortable does not deselect some pending items from the - // source list. - // We'll just do it forcefully here. - Array.from(document.querySelectorAll('.js-multi-select') || []).forEach((item) => { - Sortable.utils.deselect(item); - }); - - /** - * SortableJS leaves all the moving items "as is" on the DOM. - * Vue picks up and rehydrates the DOM, but we need to explicity - * remove the "trash" items from the DOM. - * - * This is in parity to the logic on single item move from a list/in - * a list. For reference, look at the implementation of onAdd method. - */ - this.$nextTick(() => { - if (items && items.length) { - items.forEach((item) => { - item.remove(); - }); - } - }); - } - sortableEnd(); - }, - onMove(e) { - return !e.related.classList.contains('board-list-count'); - }, - onSelect(e) { - const { - item: { classList }, - } = e; - - if ( - classList && - classList.contains('js-multi-select') && - !classList.contains('multi-select') - ) { - Sortable.utils.deselect(e.item); - } - }, - onDeselect: (e) => { - const { - item: { dataset, classList }, - } = e; - - if ( - classList && - classList.contains('multi-select') && - !classList.contains('js-multi-select') - ) { - const issue = this.list.findIssue(Number(dataset.issueId)); - boardsStore.toggleMultiSelect(issue); - } - }, - }); - - this.sortable = Sortable.create(this.$refs.list, options); - - // Scroll event on list to load more - this.$refs.list.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); - }, - methods: { - listHeight() { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight() { - return this.$refs.list.scrollHeight; - }, - scrollTop() { - return this.$refs.list.scrollTop + this.listHeight(); - }, - scrollToTop() { - this.$refs.list.scrollTop = 0; - }, - loadNextPage() { - const getIssues = this.list.nextPage(); - const loadingDone = () => { - // eslint-disable-next-line vue/no-mutating-props - this.list.loadingMore = false; - }; - - if (getIssues) { - // eslint-disable-next-line vue/no-mutating-props - this.list.loadingMore = true; - getIssues.then(loadingDone).catch(loadingDone); - } - }, - toggleForm() { - this.showIssueForm = !this.showIssueForm; - }, - onScroll() { - if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) { - this.loadNextPage(); - } - }, - }, -}; -</script> - -<template> - <div - :class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }" - class="board-list-component position-relative h-100" - data-qa-selector="board_list_cards_area" - > - <div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')"> - <gl-loading-icon size="sm" /> - </div> - <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" /> - <ul - v-show="!loading" - ref="list" - :data-board="list.id" - :data-board-type="list.type" - :class="{ 'is-smaller': showIssueForm, 'bg-danger-100': issuesSizeExceedsMax }" - class="board-list w-100 h-100 list-unstyled mb-0 p-1 js-board-list" - > - <board-card - v-for="(issue, index) in issues" - ref="issue" - :key="issue.id" - :index="index" - :list="list" - :issue="issue" - :disabled="disabled" - /> - <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> - <gl-loading-icon v-show="list.loadingMore" size="sm" label="Loading more issues" /> - <span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span> - <span v-else>{{ paginatedIssueText }}</span> - </li> - </ul> - </div> -</template> diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index 8d5f0f7eb89..dc5313b1bf6 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -201,7 +201,7 @@ export default { }); }, addToLocalStorage() { - if (AccessorUtilities.isLocalStorageAccessSafe()) { + if (AccessorUtilities.canUseLocalStorage()) { localStorage.setItem(`${this.uniqueKey}.collapsed`, this.list.collapsed); } }, diff --git a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue deleted file mode 100644 index bc29728fc55..00000000000 --- a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue +++ /dev/null @@ -1,361 +0,0 @@ -<script> -import { - GlButton, - GlButtonGroup, - GlLabel, - GlTooltip, - GlIcon, - GlSprintf, - GlTooltipDirective, -} from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; -import { isScopedLabel } from '~/lib/utils/common_utils'; -import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; -import { n__, s__ } from '~/locale'; -import sidebarEventHub from '~/sidebar/event_hub'; -import AccessorUtilities from '../../lib/utils/accessor'; -import { inactiveId, LIST, ListType } from '../constants'; -import eventHub from '../eventhub'; -import boardsStore from '../stores/boards_store'; -import IssueCount from './item_count.vue'; - -// This component is being replaced in favor of './board_list_header.vue' for GraphQL boards - -export default { - components: { - GlButtonGroup, - GlButton, - GlLabel, - GlTooltip, - GlIcon, - GlSprintf, - IssueCount, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - inject: { - currentUserId: { - default: null, - }, - boardId: { - default: '', - }, - }, - props: { - list: { - type: Object, - default: () => ({}), - required: false, - }, - disabled: { - type: Boolean, - required: true, - }, - isSwimlanesHeader: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - weightFeatureAvailable: false, - }; - }, - computed: { - ...mapState(['activeId']), - isLoggedIn() { - return Boolean(this.currentUserId); - }, - listType() { - return this.list.type; - }, - listAssignee() { - return this.list?.assignee?.username || ''; - }, - listTitle() { - return this.list?.label?.description || this.list.title || ''; - }, - showListHeaderButton() { - return !this.disabled && this.listType !== ListType.closed; - }, - showMilestoneListDetails() { - return this.list.type === 'milestone' && this.list.milestone && this.showListDetails; - }, - showAssigneeListDetails() { - return this.list.type === 'assignee' && this.showListDetails; - }, - showIterationListDetails() { - return this.listType === ListType.iteration && this.showListDetails; - }, - showListDetails() { - return this.list.isExpanded || !this.isSwimlanesHeader; - }, - showListHeaderActions() { - if (this.isLoggedIn) { - return this.isNewIssueShown || this.isSettingsShown; - } - return false; - }, - issuesCount() { - return this.list.issuesSize; - }, - issuesTooltipLabel() { - return n__(`%d issue`, `%d issues`, this.issuesCount); - }, - chevronTooltip() { - return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand'); - }, - chevronIcon() { - return this.list.isExpanded ? 'chevron-right' : 'chevron-down'; - }, - isNewIssueShown() { - return this.listType === ListType.backlog || this.showListHeaderButton; - }, - isSettingsShown() { - return ( - this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded - ); - }, - uniqueKey() { - // eslint-disable-next-line @gitlab/require-i18n-strings - return `boards.${this.boardId}.${this.listType}.${this.list.id}`; - }, - collapsedTooltipTitle() { - return this.listTitle || this.listAssignee; - }, - }, - methods: { - ...mapActions(['setActiveId']), - openSidebarSettings() { - if (this.activeId === inactiveId) { - sidebarEventHub.$emit('sidebar.closeAll'); - } - - this.setActiveId({ id: this.list.id, sidebarType: LIST }); - }, - showScopedLabels(label) { - return boardsStore.scopedLabels.enabled && isScopedLabel(label); - }, - - showNewIssueForm() { - eventHub.$emit(`toggle-issue-form-${this.list.id}`); - }, - toggleExpanded() { - // eslint-disable-next-line vue/no-mutating-props - this.list.isExpanded = !this.list.isExpanded; - - if (!this.isLoggedIn) { - this.addToLocalStorage(); - } else { - this.updateListFunction(); - } - - // When expanding/collapsing, the tooltip on the caret button sometimes stays open. - // Close all tooltips manually to prevent dangling tooltips. - this.$root.$emit(BV_HIDE_TOOLTIP); - }, - addToLocalStorage() { - if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); - } - }, - updateListFunction() { - this.list.update(); - }, - }, -}; -</script> - -<template> - <header - :class="{ - 'has-border': list.label && list.label.color, - 'gl-h-full': !list.isExpanded, - 'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader, - }" - :style="{ borderTopColor: list.label && list.label.color ? list.label.color : null }" - class="board-header gl-relative" - data-qa-selector="board_list_header" - data-testid="board-list-header" - > - <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, - }" - 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" - category="tertiary" - size="small" - @click="toggleExpanded" - /> - <!-- The following is only true in EE and if it is a milestone --> - <span - v-if="showMilestoneListDetails" - aria-hidden="true" - class="milestone-icon" - :class="{ - 'gl-mt-3 gl-rotate-90': !list.isExpanded, - 'gl-mr-2': list.isExpanded, - }" - > - <gl-icon name="timer" /> - </span> - - <span - v-if="showIterationListDetails" - aria-hidden="true" - :class="{ - 'gl-mt-3 gl-rotate-90': !list.isExpanded, - 'gl-mr-2': list.isExpanded, - }" - > - <gl-icon name="iteration" /> - </span> - - <a - v-if="showAssigneeListDetails" - :href="list.assignee.path" - class="user-avatar-link js-no-trigger" - :class="{ - 'gl-mt-3 gl-rotate-90': !list.isExpanded, - }" - > - <img - v-gl-tooltip.hover.bottom - :title="listAssignee" - :alt="list.assignee.name" - :src="list.assignee.avatar" - class="avatar s20" - height="20" - width="20" - /> - </a> - <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, - }" - > - <span - v-if="list.type !== 'label'" - v-gl-tooltip.hover - :class="{ - 'gl-display-block': !list.isExpanded || list.type === 'milestone', - }" - :title="listTitle" - class="board-title-main-text gl-text-truncate" - > - {{ list.title }} - </span> - <span - v-if="list.type === 'assignee'" - class="gl-ml-2 gl-font-weight-normal gl-text-gray-500" - :class="{ 'gl-display-none': !list.isExpanded }" - > - @{{ listAssignee }} - </span> - <gl-label - v-if="list.type === 'label'" - v-gl-tooltip.hover.bottom - :background-color="list.label.color" - :description="list.label.description" - :scoped="showScopedLabels(list.label)" - :size="!list.isExpanded ? 'sm' : ''" - :title="list.label.title" - /> - </div> - - <span - v-if="isSwimlanesHeader && !list.isExpanded" - ref="collapsedInfo" - aria-hidden="true" - class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-500" - > - <gl-icon name="information" /> - </span> - <gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo"> - <div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div> - <div v-if="list.maxIssueCount !== 0"> - • - <gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')"> - <template #issuesSize>{{ issuesTooltipLabel }}</template> - <template #maxIssueCount>{{ list.maxIssueCount }}</template> - </gl-sprintf> - </div> - <div v-else>• {{ issuesTooltipLabel }}</div> - <div v-if="weightFeatureAvailable"> - • - <gl-sprintf :message="__('%{totalWeight} total weight')"> - <template #totalWeight>{{ list.totalWeight }}</template> - </gl-sprintf> - </div> - </gl-tooltip> - - <div - class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary" - :class="{ - 'gl-display-none!': !list.isExpanded && isSwimlanesHeader, - 'gl-p-0': !list.isExpanded, - }" - > - <span class="gl-display-inline-flex"> - <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" /> - <span ref="issueCount" class="issue-count-badge-count"> - <gl-icon class="gl-mr-2" name="issues" /> - <issue-count :items-size="issuesCount" :max-issue-count="list.maxIssueCount" /> - </span> - <!-- The following is only true in EE. --> - <template v-if="weightFeatureAvailable"> - <gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" /> - <span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3"> - <gl-icon class="gl-mr-2" name="weight" /> - {{ list.totalWeight }} - </span> - </template> - </span> - </div> - <gl-button-group v-if="showListHeaderActions" class="board-list-button-group pl-2"> - <gl-button - v-if="isNewIssueShown" - ref="newIssueBtn" - v-gl-tooltip.hover - :class="{ - 'gl-display-none': !list.isExpanded, - }" - :aria-label="__('New issue')" - :title="__('New issue')" - class="issue-count-badge-add-button no-drag" - icon="plus" - @click="showNewIssueForm" - /> - - <gl-button - v-if="isSettingsShown" - ref="settingsBtn" - v-gl-tooltip.hover - :aria-label="__('List settings')" - class="no-drag js-board-settings-button" - :title="__('List settings')" - icon="settings" - @click="openSidebarSettings" - /> - <gl-tooltip :target="() => $refs.settingsBtn">{{ __('List settings') }}</gl-tooltip> - </gl-button-group> - </h3> - </header> -</template> diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue deleted file mode 100644 index a25b436b8de..00000000000 --- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue +++ /dev/null @@ -1,138 +0,0 @@ -<script> -import { GlButton } from '@gitlab/ui'; -import { mapGetters } from 'vuex'; -import { getMilestone } from 'ee_else_ce/boards/boards_util'; -import ListIssue from 'ee_else_ce/boards/models/issue'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import eventHub from '../eventhub'; -import boardsStore from '../stores/boards_store'; -import ProjectSelect from './project_select_deprecated.vue'; - -// This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards - -export default { - name: 'BoardNewIssueDeprecated', - components: { - ProjectSelect, - GlButton, - }, - mixins: [glFeatureFlagMixin()], - inject: ['groupId'], - props: { - list: { - type: Object, - required: true, - }, - }, - data() { - return { - title: '', - error: false, - selectedProject: {}, - }; - }, - computed: { - ...mapGetters(['isGroupBoard']), - disabled() { - if (this.isGroupBoard) { - return this.title === '' || !this.selectedProject.name; - } - return this.title === ''; - }, - }, - mounted() { - this.$refs.input.focus(); - eventHub.$on('setSelectedProject', this.setSelectedProject); - }, - methods: { - submit(e) { - e.preventDefault(); - if (this.title.trim() === '') return Promise.resolve(); - - this.error = false; - - const labels = this.list.label ? [this.list.label] : []; - const assignees = this.list.assignee ? [this.list.assignee] : []; - const milestone = getMilestone(this.list); - - const { weightFeatureAvailable } = boardsStore; - const { weight } = weightFeatureAvailable ? boardsStore.state.currentBoard : {}; - - const issue = new ListIssue({ - title: this.title, - labels, - subscribed: true, - assignees, - milestone, - project_id: this.selectedProject.id, - weight, - }); - - eventHub.$emit(`scroll-board-list-${this.list.id}`); - this.cancel(); - - return this.list - .newIssue(issue) - .then(() => { - boardsStore.setIssueDetail(issue); - boardsStore.setListDetail(this.list); - }) - .catch(() => { - this.list.removeIssue(issue); - - // Show error message - this.error = true; - }); - }, - cancel() { - this.title = ''; - eventHub.$emit(`toggle-issue-form-${this.list.id}`); - }, - setSelectedProject(selectedProject) { - this.selectedProject = selectedProject; - }, - }, -}; -</script> - -<template> - <div class="board-new-issue-form"> - <div class="board-card position-relative p-3 rounded"> - <form @submit="submit($event)"> - <div v-if="error" class="flash-container"> - <div class="flash-alert">{{ __('An error occurred. Please try again.') }}</div> - </div> - <label :for="list.id + '-title'" class="label-bold">{{ __('Title') }}</label> - <input - :id="list.id + '-title'" - ref="input" - v-model="title" - class="form-control" - type="text" - name="issue_title" - autocomplete="off" - /> - <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" /> - <div class="clearfix gl-mt-3"> - <gl-button - ref="submitButton" - :disabled="disabled" - class="float-left js-no-auto-disable" - variant="success" - category="primary" - type="submit" - >{{ __('Create issue') }}</gl-button - > - <gl-button - ref="cancelButton" - class="float-right" - type="button" - variant="default" - @click="cancel" - >{{ __('Cancel') }}</gl-button - > - </div> - </form> - </div> - </div> -</template> diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index c089a6a39af..6b7c08d05a5 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -3,7 +3,6 @@ import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui'; import { MountingPortal } from 'portal-vue'; import { mapActions, mapState, mapGetters } from 'vuex'; import { LIST, ListType, ListTypeTitles } from '~/boards/constants'; -import boardsStore from '~/boards/stores/boards_store'; import { isScopedLabel } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import eventHub from '~/sidebar/event_hub'; @@ -23,7 +22,7 @@ export default { import('ee_component/boards/components/board_settings_list_types.vue'), }, mixins: [glFeatureFlagMixin(), Tracking.mixin()], - inject: ['canAdminList'], + inject: ['canAdminList', 'scopedLabelsAvailable'], inheritAttrs: false, data() { return { @@ -31,20 +30,13 @@ export default { }; }, computed: { - ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL', 'isEpicBoard']), + ...mapGetters(['isSidebarOpen', 'isEpicBoard']), ...mapState(['activeId', 'sidebarType', 'boardLists']), isWipLimitsOn() { return this.glFeatures.wipLimits && !this.isEpicBoard; }, activeList() { - /* - Warning: Though a computed property it is not reactive because we are - referencing a List Model class. Reactivity only applies to plain JS objects - */ - if (this.shouldUseGraphQL || this.isEpicBoard) { - return this.boardLists[this.activeId]; - } - return boardsStore.state.lists.find(({ id }) => id === this.activeId); + return this.boardLists[this.activeId] || {}; }, activeListLabel() { return this.activeList.label; @@ -68,17 +60,13 @@ export default { methods: { ...mapActions(['unsetActiveId', 'removeList']), showScopedLabels(label) { - return boardsStore.scopedLabels.enabled && isScopedLabel(label); + return this.scopedLabelsAvailable && isScopedLabel(label); }, deleteBoard() { // eslint-disable-next-line no-alert if (window.confirm(__('Are you sure you want to remove this list?'))) { - if (this.shouldUseGraphQL || this.isEpicBoard) { - this.track('click_button', { label: 'remove_list' }); - this.removeList(this.activeId); - } else { - this.activeList.destroy(); - } + this.track('click_button', { label: 'remove_list' }); + this.removeList(this.activeId); this.unsetActiveId(); } }, @@ -93,9 +81,26 @@ export default { v-bind="$attrs" class="js-board-settings-sidebar gl-absolute" :open="isSidebarOpen" + variant="sidebar" @close="unsetActiveId" > - <template #title>{{ $options.listSettingsText }}</template> + <template #title> + <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24"> + {{ $options.listSettingsText }} + </h2> + </template> + <template #header> + <div v-if="canAdminList && activeList.id" class="gl-mt-3"> + <gl-button + variant="danger" + category="secondary" + size="small" + data-testid="remove-list" + @click.stop="deleteBoard" + >{{ __('Remove list') }} + </gl-button> + </div> + </template> <template v-if="isSidebarOpen"> <div v-if="boardListType === ListType.label"> <label class="js-list-label gl-display-block">{{ listTypeTitle }}</label> @@ -115,16 +120,6 @@ export default { v-if="isWipLimitsOn" :max-issue-count="activeList.maxIssueCount" /> - <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-mt-4"> - <gl-button - variant="danger" - category="secondary" - icon="remove" - data-testid="remove-list" - @click.stop="deleteBoard" - >{{ __('Remove list') }} - </gl-button> - </div> </template> </gl-drawer> </mounting-portal> diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js deleted file mode 100644 index 21a34182369..00000000000 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ /dev/null @@ -1,115 +0,0 @@ -// This is a true violation of @gitlab/no-runtime-template-compiler, as it -// relies on app/views/shared/boards/components/_sidebar.html.haml for its -// template. -/* eslint-disable no-new, @gitlab/no-runtime-template-compiler */ - -import { GlLabel } from '@gitlab/ui'; -import $ from 'jquery'; -import Vue from 'vue'; -import DueDateSelectors from '~/due_date_select'; -import IssuableContext from '~/issuable_context'; -import LabelsSelect from '~/labels_select'; -import { isScopedLabel } from '~/lib/utils/common_utils'; -import { sprintf, __ } from '~/locale'; -import MilestoneSelect from '~/milestone_select'; -import Sidebar from '~/right_sidebar'; -import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue'; -import Assignees from '~/sidebar/components/assignees/assignees.vue'; -import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; -import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue'; -import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; -import eventHub from '~/sidebar/event_hub'; -import boardsStore from '../stores/boards_store'; - -export default Vue.extend({ - components: { - AssigneeTitle, - Assignees, - GlLabel, - SidebarEpicsSelect: () => - import('ee_component/sidebar/components/sidebar_item_epics_select.vue'), - Subscriptions, - TimeTracker, - SidebarAssigneesWidget, - }, - props: { - currentUser: { - type: Object, - default: () => ({}), - required: false, - }, - }, - data() { - return { - detail: boardsStore.detail, - issue: {}, - list: {}, - loadingAssignees: false, - timeTrackingLimitToHours: boardsStore.timeTracking.limitToHours, - }; - }, - computed: { - showSidebar() { - return Object.keys(this.issue).length; - }, - milestoneTitle() { - return this.issue.milestone ? this.issue.milestone.title : __('No milestone'); - }, - canRemove() { - return !this.list?.preset; - }, - hasLabels() { - return this.issue.labels && this.issue.labels.length; - }, - labelDropdownTitle() { - return this.hasLabels - ? sprintf(__('%{firstLabel} +%{labelCount} more'), { - firstLabel: this.issue.labels[0].title, - labelCount: this.issue.labels.length - 1, - }) - : __('Label'); - }, - selectedLabels() { - return this.hasLabels ? this.issue.labels.map((l) => l.title).join(',') : ''; - }, - }, - watch: { - detail: { - handler() { - if (this.issue.id !== this.detail.issue.id) { - $('.js-issue-board-sidebar', this.$el).each((i, el) => { - $(el).data('deprecatedJQueryDropdown').clearMenu(); - }); - } - - this.issue = this.detail.issue; - this.list = this.detail.list; - }, - deep: true, - }, - }, - created() { - eventHub.$on('sidebar.closeAll', this.closeSidebar); - }, - beforeDestroy() { - eventHub.$off('sidebar.closeAll', this.closeSidebar); - }, - mounted() { - new IssuableContext(this.currentUser); - new MilestoneSelect(); - new DueDateSelectors(); - new LabelsSelect(); - new Sidebar(); - }, - methods: { - closeSidebar() { - this.detail.issue = {}; - }, - setAssignees({ assignees }) { - boardsStore.detail.issue.setAssignees(assignees); - }, - showScopedLabels(label) { - return boardsStore.scopedLabels.enabled && isScopedLabel(label); - }, - }, -}); diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue deleted file mode 100644 index c1536dff2c6..00000000000 --- a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue +++ /dev/null @@ -1,360 +0,0 @@ -<script> -import { - GlLoadingIcon, - GlSearchBoxByType, - GlDropdown, - GlDropdownDivider, - GlDropdownSectionHeader, - GlDropdownItem, - GlModalDirective, -} from '@gitlab/ui'; -import { throttle } from 'lodash'; -import { mapGetters, mapState } from 'vuex'; - -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import httpStatusCodes from '~/lib/utils/http_status'; - -import groupQuery from '../graphql/group_boards.query.graphql'; -import projectQuery from '../graphql/project_boards.query.graphql'; - -import boardsStore from '../stores/boards_store'; -import BoardForm from './board_form.vue'; - -const MIN_BOARDS_TO_VIEW_RECENT = 10; - -export default { - name: 'BoardsSelector', - components: { - BoardForm, - GlLoadingIcon, - GlSearchBoxByType, - GlDropdown, - GlDropdownDivider, - GlDropdownSectionHeader, - GlDropdownItem, - }, - directives: { - GlModalDirective, - }, - props: { - currentBoard: { - type: Object, - required: true, - }, - throttleDuration: { - type: Number, - default: 200, - required: false, - }, - boardBaseUrl: { - type: String, - required: true, - }, - hasMissingBoards: { - type: Boolean, - required: true, - }, - canAdminBoard: { - type: Boolean, - required: true, - }, - multipleIssueBoardsAvailable: { - type: Boolean, - required: true, - }, - labelsPath: { - type: String, - required: true, - }, - labelsWebUrl: { - type: String, - required: true, - }, - projectId: { - type: Number, - required: true, - }, - groupId: { - type: Number, - required: true, - }, - scopedIssueBoardFeatureEnabled: { - type: Boolean, - required: true, - }, - weights: { - type: Array, - required: true, - }, - enabledScopedLabels: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - hasScrollFade: false, - loadingBoards: 0, - loadingRecentBoards: false, - scrollFadeInitialized: false, - boards: [], - recentBoards: [], - state: boardsStore.state, - throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration), - contentClientHeight: 0, - maxPosition: 0, - store: boardsStore, - filterTerm: '', - }; - }, - computed: { - ...mapState(['boardType']), - ...mapGetters(['isGroupBoard']), - parentType() { - return this.boardType; - }, - loading() { - return this.loadingRecentBoards || Boolean(this.loadingBoards); - }, - currentPage() { - return this.state.currentPage; - }, - filteredBoards() { - return this.boards.filter((board) => - board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), - ); - }, - board() { - return this.state.currentBoard; - }, - showDelete() { - return this.boards.length > 1; - }, - scrollFadeClass() { - return { - 'fade-out': !this.hasScrollFade, - }; - }, - showRecentSection() { - return ( - this.recentBoards.length && - this.boards.length > MIN_BOARDS_TO_VIEW_RECENT && - !this.filterTerm.length - ); - }, - }, - watch: { - filteredBoards() { - this.scrollFadeInitialized = false; - this.$nextTick(this.setScrollFade); - }, - }, - created() { - boardsStore.setCurrentBoard(this.currentBoard); - }, - methods: { - showPage(page) { - boardsStore.showPage(page); - }, - cancel() { - this.showPage(''); - }, - loadBoards(toggleDropdown = true) { - if (toggleDropdown && this.boards.length > 0) { - return; - } - - this.$apollo.addSmartQuery('boards', { - variables() { - return { fullPath: this.state.endpoints.fullPath }; - }, - query() { - return this.isGroupBoard ? groupQuery : projectQuery; - }, - loadingKey: 'loadingBoards', - update(data) { - if (!data?.[this.parentType]) { - return []; - } - return data[this.parentType].boards.edges.map(({ node }) => ({ - id: getIdFromGraphQLId(node.id), - name: node.name, - })); - }, - }); - - this.loadingRecentBoards = true; - boardsStore - .recentBoards() - .then((res) => { - this.recentBoards = res.data; - }) - .catch((err) => { - /** - * If user is unauthorized we'd still want to resolve the - * request to display all boards. - */ - if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) { - this.recentBoards = []; // recent boards are empty - return; - } - throw err; - }) - .then(() => this.$nextTick()) // Wait for boards list in DOM - .then(() => { - this.setScrollFade(); - }) - .catch(() => {}) - .finally(() => { - this.loadingRecentBoards = false; - }); - }, - isScrolledUp() { - const { content } = this.$refs; - - if (!content) { - return false; - } - - const currentPosition = this.contentClientHeight + content.scrollTop; - - return currentPosition < this.maxPosition; - }, - initScrollFade() { - const { content } = this.$refs; - - if (!content) { - return; - } - - this.scrollFadeInitialized = true; - - this.contentClientHeight = content.clientHeight; - this.maxPosition = content.scrollHeight; - }, - setScrollFade() { - if (!this.scrollFadeInitialized) this.initScrollFade(); - - this.hasScrollFade = this.isScrolledUp(); - }, - }, -}; -</script> - -<template> - <div class="boards-switcher js-boards-selector gl-mr-3"> - <span class="boards-selector-wrapper js-boards-selector-wrapper"> - <gl-dropdown - data-qa-selector="boards_dropdown" - toggle-class="dropdown-menu-toggle js-dropdown-toggle" - menu-class="flex-column dropdown-extended-height" - :text="board.name" - @show="loadBoards" - > - <p class="gl-new-dropdown-header-top" @mousedown.prevent> - {{ s__('IssueBoards|Switch board') }} - </p> - <gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" /> - - <div - v-if="!loading" - ref="content" - data-qa-selector="boards_dropdown_content" - class="dropdown-content flex-fill" - @scroll.passive="throttledSetScrollFade" - > - <gl-dropdown-item - v-show="filteredBoards.length === 0" - class="gl-pointer-events-none text-secondary" - > - {{ s__('IssueBoards|No matching boards found') }} - </gl-dropdown-item> - - <gl-dropdown-section-header v-if="showRecentSection"> - {{ __('Recent') }} - </gl-dropdown-section-header> - - <template v-if="showRecentSection"> - <gl-dropdown-item - v-for="recentBoard in recentBoards" - :key="`recent-${recentBoard.id}`" - class="js-dropdown-item" - :href="`${boardBaseUrl}/${recentBoard.id}`" - > - {{ recentBoard.name }} - </gl-dropdown-item> - </template> - - <gl-dropdown-divider v-if="showRecentSection" /> - - <gl-dropdown-section-header v-if="showRecentSection"> - {{ __('All') }} - </gl-dropdown-section-header> - - <gl-dropdown-item - v-for="otherBoard in filteredBoards" - :key="otherBoard.id" - class="js-dropdown-item" - :href="`${boardBaseUrl}/${otherBoard.id}`" - > - {{ otherBoard.name }} - </gl-dropdown-item> - - <gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events"> - {{ - s__( - 'IssueBoards|Some of your boards are hidden, activate a license to see them again.', - ) - }} - </gl-dropdown-item> - </div> - - <div - v-show="filteredBoards.length > 0" - class="dropdown-content-faded-mask" - :class="scrollFadeClass" - ></div> - - <gl-loading-icon v-if="loading" size="sm" /> - - <div v-if="canAdminBoard"> - <gl-dropdown-divider /> - - <gl-dropdown-item - v-if="multipleIssueBoardsAvailable" - v-gl-modal-directive="'board-config-modal'" - data-qa-selector="create_new_board_button" - @click.prevent="showPage('new')" - > - {{ s__('IssueBoards|Create new board') }} - </gl-dropdown-item> - - <gl-dropdown-item - v-if="showDelete" - v-gl-modal-directive="'board-config-modal'" - class="text-danger js-delete-board" - @click.prevent="showPage('delete')" - > - {{ s__('IssueBoards|Delete board') }} - </gl-dropdown-item> - </div> - </gl-dropdown> - - <board-form - v-if="currentPage" - :labels-path="labelsPath" - :labels-web-url="labelsWebUrl" - :project-id="projectId" - :group-id="groupId" - :can-admin-board="canAdminBoard" - :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" - :weights="weights" - :enable-scoped-labels="enabledScopedLabels" - :current-board="currentBoard" - :current-page="state.currentPage" - @cancel="cancel" - /> - </span> - </div> -</template> diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue index 30e304b8a65..f39e4d90357 100644 --- a/app/assets/javascripts/boards/components/config_toggle.vue +++ b/app/assets/javascripts/boards/components/config_toggle.vue @@ -15,11 +15,6 @@ export default { }, mixins: [Tracking.mixin()], props: { - boardsStore: { - type: Object, - required: false, - default: null, - }, canAdminList: { type: Boolean, required: true, @@ -41,9 +36,6 @@ export default { showPage() { this.track('click_button', { label: 'edit_board' }); eventHub.$emit('showBoardModal', formType.edit); - if (this.boardsStore) { - this.boardsStore.showPage(formType.edit); - } }, }, }; diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue index 5206db05410..b6c5ef955c6 100644 --- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue +++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue @@ -6,6 +6,7 @@ import issueBoardFilters from '~/boards/issue_board_filters'; import { TYPE_USER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; +import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; @@ -63,17 +64,17 @@ export default { return [ { - icon: 'labels', - title: label, - type: 'label_name', + icon: 'user', + title: assignee, + type: 'assignee_username', operators: [ { value: '=', description: is }, { value: '!=', description: isNot }, ], - token: LabelToken, - unique: false, - symbol: '~', - fetchLabels, + token: AuthorToken, + unique: true, + fetchAuthors, + preloadedAuthors: this.preloadedAuthors(), }, { icon: 'pencil', @@ -90,17 +91,27 @@ export default { preloadedAuthors: this.preloadedAuthors(), }, { - icon: 'user', - title: assignee, - type: 'assignee_username', + icon: 'labels', + title: label, + type: 'label_name', operators: [ { value: '=', description: is }, { value: '!=', description: isNot }, ], - token: AuthorToken, + token: LabelToken, + unique: false, + symbol: '~', + fetchLabels, + }, + { + type: 'milestone_title', + title: milestone, + icon: 'clock', + symbol: '%', + token: MilestoneToken, unique: true, - fetchAuthors, - preloadedAuthors: this.preloadedAuthors(), + defaultMilestones: DEFAULT_MILESTONES_GRAPHQL, + fetchMilestones: this.fetchMilestones, }, { icon: 'issues', @@ -115,16 +126,6 @@ export default { ], }, { - type: 'milestone_title', - title: milestone, - icon: 'clock', - symbol: '%', - token: MilestoneToken, - unique: true, - defaultMilestones: [], // todo: https://gitlab.com/gitlab-org/gitlab/-/issues/337044#note_640010094 - fetchMilestones: this.fetchMilestones, - }, - { type: 'weight', title: weight, icon: 'weight', diff --git a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue deleted file mode 100644 index 6e90731cc2f..00000000000 --- a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue +++ /dev/null @@ -1,247 +0,0 @@ -<script> -import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import { sortBy } from 'lodash'; -import { mapState } from 'vuex'; -import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner'; -import { isScopedLabel } from '~/lib/utils/common_utils'; -import { sprintf, __, n__ } from '~/locale'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; -import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import boardsStore from '../stores/boards_store'; -import IssueDueDate from './issue_due_date.vue'; -import IssueTimeEstimate from './issue_time_estimate_deprecated.vue'; - -export default { - components: { - GlLabel, - GlIcon, - UserAvatarLink, - TooltipOnTruncate, - IssueDueDate, - IssueTimeEstimate, - IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - mixins: [boardCardInner], - inject: ['groupId', 'rootPath'], - props: { - issue: { - type: Object, - required: true, - }, - list: { - type: Object, - required: false, - default: () => ({}), - }, - updateFilters: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - limitBeforeCounter: 2, - maxRender: 3, - maxCounter: 99, - }; - }, - computed: { - ...mapState(['isShowingLabels']), - numberOverLimit() { - return this.issue.assignees.length - this.limitBeforeCounter; - }, - assigneeCounterTooltip() { - const { numberOverLimit, maxCounter } = this; - const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit; - return sprintf(__('%{count} more assignees'), { count }); - }, - assigneeCounterLabel() { - if (this.numberOverLimit > this.maxCounter) { - return `${this.maxCounter}+`; - } - - return `+${this.numberOverLimit}`; - }, - shouldRenderCounter() { - if (this.issue.assignees.length <= this.maxRender) { - return false; - } - - return this.issue.assignees.length > this.numberOverLimit; - }, - issueId() { - if (this.issue.iid) { - return `#${this.issue.iid}`; - } - return false; - }, - showLabelFooter() { - return this.isShowingLabels && this.issue.labels.find(this.showLabel); - }, - issueReferencePath() { - const { referencePath, groupId } = this.issue; - return !groupId ? referencePath.split('#')[0] : null; - }, - orderedLabels() { - return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title'); - }, - blockedLabel() { - if (this.issue.blockedByCount) { - return n__(`Blocked by %d issue`, `Blocked by %d issues`, this.issue.blockedByCount); - } - return __('Blocked issue'); - }, - assignees() { - return this.issue.assignees.filter((_, index) => this.shouldRenderAssignee(index)); - }, - }, - methods: { - isIndexLessThanlimit(index) { - return index < this.limitBeforeCounter; - }, - shouldRenderAssignee(index) { - // Eg. maxRender is 4, - // Render up to all 4 assignees if there are only 4 assigness - // Otherwise render up to the limitBeforeCounter - if (this.issue.assignees.length <= this.maxRender) { - return index < this.maxRender; - } - - return index < this.limitBeforeCounter; - }, - assigneeUrl(assignee) { - if (!assignee) return ''; - return `${this.rootPath}${assignee.username}`; - }, - avatarUrlTitle(assignee) { - return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name }); - }, - showLabel(label) { - if (!label.id) return false; - return true; - }, - isNonListLabel(label) { - return label.id && !(this.list.type === 'label' && this.list.title === label.title); - }, - filterByLabel(label) { - if (!this.updateFilters) return; - const labelTitle = encodeURIComponent(label.title); - const filter = `label_name[]=${labelTitle}`; - - boardsStore.toggleFilter(filter); - }, - showScopedLabel(label) { - return boardsStore.scopedLabels.enabled && isScopedLabel(label); - }, - }, -}; -</script> -<template> - <div> - <div class="gl-display-flex" dir="auto"> - <h4 class="board-card-title gl-mb-0 gl-mt-0"> - <gl-icon - v-if="issue.blocked" - v-gl-tooltip - name="issue-block" - :title="blockedLabel" - class="issue-blocked-icon gl-mr-2" - :aria-label="blockedLabel" - data-testid="issue-blocked-icon" - /> - <gl-icon - v-if="issue.confidential" - v-gl-tooltip - name="eye-slash" - :title="__('Confidential')" - class="confidential-icon gl-mr-2" - :aria-label="__('Confidential')" - /> - <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"> - <template v-for="label in orderedLabels"> - <gl-label - :key="label.id" - :background-color="label.color" - :title="label.title" - :description="label.description" - size="sm" - :scoped="showScopedLabel(label)" - @click="filterByLabel(label)" - /> - </template> - </div> - <div - class="board-card-footer gl-display-flex gl-justify-content-space-between gl-align-items-flex-end" - > - <div - class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container" - > - <span - v-if="issue.referencePath" - class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3" - > - <tooltip-on-truncate - v-if="issueReferencePath" - :title="issueReferencePath" - placement="bottom" - class="board-issue-path gl-text-truncate gl-font-weight-bold" - >{{ issueReferencePath }}</tooltip-on-truncate - > - #{{ 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 || Boolean(issue.closedAt)" - /> - <issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" /> - <issue-card-weight - v-if="validIssueWeight(issue)" - :weight="issue.weight" - @click="filterByWeight(issue.weight)" - /> - </span> - </div> - <div class="board-card-assignee gl-display-flex"> - <user-avatar-link - v-for="assignee in assignees" - :key="assignee.id" - :link-href="assigneeUrl(assignee)" - :img-alt="avatarUrlTitle(assignee)" - :img-src="assignee.avatarUrl || assignee.avatar || assignee.avatar_url" - :img-size="24" - class="js-no-trigger" - tooltip-placement="bottom" - > - <span class="js-assignee-tooltip"> - <span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span> - {{ assignee.name }} - <span class="text-white-50">@{{ assignee.username }}</span> - </span> - </user-avatar-link> - <span - v-if="shouldRenderCounter" - v-gl-tooltip - :title="assigneeCounterTooltip" - class="avatar-counter" - data-placement="bottom" - >{{ assigneeCounterLabel }}</span - > - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue b/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue deleted file mode 100644 index 8ddf50cb357..00000000000 --- a/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -import { GlTooltip, GlIcon } from '@gitlab/ui'; -import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; -import boardsStore from '../stores/boards_store'; - -export default { - components: { - GlIcon, - GlTooltip, - }, - props: { - estimate: { - type: [Number, String], - required: true, - }, - }, - data() { - return { - limitToHours: boardsStore.timeTracking.limitToHours, - }; - }, - computed: { - title() { - return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true); - }, - timeEstimate() { - return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours })); - }, - }, -}; -</script> - -<template> - <span> - <span ref="issueTimeEstimate" class="board-card-info card-number"> - <gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{ - timeEstimate - }}</time> - </span> - <gl-tooltip - :target="() => $refs.issueTimeEstimate" - placement="bottom" - class="js-issue-time-estimate" - > - <span class="bold d-block">{{ __('Time estimate') }}</span> {{ title }} - </gl-tooltip> - </span> -</template> diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js deleted file mode 100644 index 6eb1dbfb46a..00000000000 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-disable func-names, no-new */ - -import $ from 'jquery'; -import store from '~/boards/stores'; -import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; -import createFlash from '~/flash'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import axios from '~/lib/utils/axios_utils'; -import { __ } from '~/locale'; -import CreateLabelDropdown from '../../create_label'; -import { fullLabelId } from '../boards_util'; -import boardsStore from '../stores/boards_store'; - -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) => { - if (!addNewList) { - return; - } - - if (shouldCreateListGraphQL(label)) { - store.dispatch('createList', { labelId: fullLabelId(label) }); - } else { - boardsStore.new({ - title: label.title, - position: boardsStore.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, - title: label.title, - color: label.color, - }, - }); - } - }); - -export default function initNewListDropdown() { - $('.js-new-board-list').each(function () { - const $dropdownToggle = $(this); - const $dropdown = $dropdownToggle.closest('.dropdown'); - new CreateLabelDropdown( - $dropdown.find('.dropdown-new-label'), - $dropdownToggle.data('namespacePath'), - $dropdownToggle.data('projectPath'), - ); - - initDeprecatedJQueryDropdown($dropdownToggle, { - data(term, callback) { - const reqFailed = () => { - $dropdownToggle.data('bs.dropdown').hide(); - createFlash({ - message: __('Error fetching labels.'), - }); - }; - - if (store.getters.shouldUseGraphQL) { - store - .dispatch('fetchLabels') - .then((data) => callback(data)) - .catch(reqFailed); - } else { - axios - .get($dropdownToggle.attr('data-list-labels-path')) - .then(({ data }) => callback(data)) - .catch(reqFailed); - } - }, - renderRow(label) { - const active = store.getters.shouldUseGraphQL - ? store.getters.getListByLabelId(label.id) - : boardsStore.findListByLabelId(label.id); - const $li = $('<li />'); - const $a = $('<a />', { - class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '', - text: label.title, - href: '#', - }); - const $labelColor = $('<span />', { - class: 'dropdown-label-box', - style: `background-color: ${label.color}`, - }); - - return $li.append($a.prepend($labelColor)); - }, - search: { - fields: ['title'], - }, - filterable: true, - selectable: true, - multiSelect: true, - containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', - clicked(options) { - const { e } = options; - const label = options.selectedObj; - e.preventDefault(); - - if (shouldCreateListGraphQL(label)) { - store.dispatch('createList', { labelId: label.id }); - } else if (!boardsStore.findListByLabelId(label.id)) { - boardsStore.new({ - title: label.title, - position: boardsStore.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, - title: label.title, - color: label.color, - }, - }); - } - }, - }); - }); -} diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue deleted file mode 100644 index fc95ba0461d..00000000000 --- a/app/assets/javascripts/boards/components/project_select_deprecated.vue +++ /dev/null @@ -1,146 +0,0 @@ -<script> -import { - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlSearchBoxByType, - GlLoadingIcon, -} from '@gitlab/ui'; -import { s__ } from '~/locale'; -import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; -import Api from '../../api'; -import { ListType } from '../constants'; -import eventHub from '../eventhub'; - -export default { - name: 'ProjectSelect', - i18n: { - headerTitle: s__(`BoardNewIssue|Projects`), - dropdownText: s__(`BoardNewIssue|Select a project`), - searchPlaceholder: s__(`BoardNewIssue|Search projects`), - emptySearchResult: s__(`BoardNewIssue|No matching results`), - }, - defaultFetchOptions: { - with_issues_enabled: true, - with_shared: false, - include_subgroups: true, - order_by: 'similarity', - archived: false, - }, - components: { - GlLoadingIcon, - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlSearchBoxByType, - }, - inject: ['groupId'], - props: { - list: { - type: Object, - required: true, - }, - }, - data() { - return { - initialLoading: true, - isFetching: false, - projects: [], - selectedProject: {}, - searchTerm: '', - }; - }, - computed: { - selectedProjectName() { - return this.selectedProject.name || this.$options.i18n.dropdownText; - }, - fetchOptions() { - const additionalAttrs = {}; - if (this.list.type && this.list.type !== ListType.backlog) { - additionalAttrs.min_access_level = featureAccessLevel.EVERYONE; - } - - return { - ...this.$options.defaultFetchOptions, - ...additionalAttrs, - }; - }, - isFetchResultEmpty() { - return this.projects.length === 0; - }, - }, - watch: { - searchTerm() { - this.fetchProjects(); - }, - }, - async mounted() { - await this.fetchProjects(); - - this.initialLoading = false; - }, - methods: { - async fetchProjects() { - this.isFetching = true; - try { - const projects = await Api.groupProjects(this.groupId, this.searchTerm, this.fetchOptions); - - this.projects = projects.map((project) => { - return { - id: project.id, - name: project.name, - namespacedName: project.name_with_namespace, - path: project.path_with_namespace, - }; - }); - } catch (err) { - /* Handled in Api.groupProjects */ - } finally { - this.isFetching = false; - } - }, - selectProject(projectId) { - this.selectedProject = this.projects.find((project) => project.id === projectId); - - eventHub.$emit('setSelectedProject', this.selectedProject); - }, - }, -}; -</script> - -<template> - <div> - <label class="gl-font-weight-bold gl-mt-3" data-testid="header-label">{{ - $options.i18n.headerTitle - }}</label> - <gl-dropdown - data-testid="project-select-dropdown" - :text="selectedProjectName" - :header-text="$options.i18n.headerTitle" - block - menu-class="gl-w-full!" - :loading="initialLoading" - > - <gl-search-box-by-type - v-model.trim="searchTerm" - debounce="250" - :placeholder="$options.i18n.searchPlaceholder" - /> - <gl-dropdown-item - v-for="project in projects" - v-show="!isFetching" - :key="project.id" - :name="project.name" - @click="selectProject(project.id)" - > - {{ project.namespacedName }} - </gl-dropdown-item> - <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon"> - <gl-loading-icon class="gl-mx-auto" size="sm" /> - </gl-dropdown-text> - <gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message"> - <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> - </gl-dropdown-text> - </gl-dropdown> - </div> -</template> diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js index 41938d8e284..945a508c55d 100644 --- a/app/assets/javascripts/boards/config_toggle.js +++ b/app/assets/javascripts/boards/config_toggle.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import ConfigToggle from './components/config_toggle.vue'; -export default (boardsStore = undefined) => { +export default () => { const el = document.querySelector('.js-board-config'); if (!el) { @@ -15,7 +15,6 @@ export default (boardsStore = undefined) => { render(h) { return h(ConfigToggle, { props: { - boardsStore, canAdminList: parseBoolean(el.dataset.canAdminList), hasScope: parseBoolean(el.dataset.hasScope), }, diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 16fb4596726..391e0d1fb0a 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -119,6 +119,11 @@ export const DraggableItemTypes = { list: 'list', }; +export const MilestoneIDs = { + NONE: 0, + ANY: -1, +}; + export default { BoardType, ListType, diff --git a/app/assets/javascripts/boards/ee_functions.js b/app/assets/javascripts/boards/ee_functions.js deleted file mode 100644 index 62a0d930ec0..00000000000 --- a/app/assets/javascripts/boards/ee_functions.js +++ /dev/null @@ -1,4 +0,0 @@ -export const setWeightFetchingState = () => {}; -export const setEpicFetchingState = () => {}; - -export const getMilestoneTitle = () => ({}); diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index c6040f1e4aa..72586970008 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -4,7 +4,6 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable import { updateHistory } from '~/lib/utils/url_utility'; import FilteredSearchContainer from '../filtered_search/container'; import vuexstore from './stores'; -import boardsStore from './stores/boards_store'; export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { @@ -26,7 +25,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { this.cantEdit = cantEdit.filter((i) => typeof i === 'string'); this.cantEditWithValue = cantEdit.filter((i) => typeof i === 'object'); - if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) { + if (vuexstore.state.boardConfig) { const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig); // TODO Refactor: https://gitlab.com/gitlab-org/gitlab/-/issues/329274 // here we are using "window.location.search" as a temporary store @@ -45,14 +44,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager { const groupByParam = new URLSearchParams(window.location.search).get('group_by'); this.store.path = `${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`; - if (vuexstore.getters.shouldUseGraphQL) { - updateHistory({ - url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`, - }); - vuexstore.dispatch('performSearch'); - } else if (this.updateUrl) { - boardsStore.updateFiltersUrl(); - } + updateHistory({ + url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`, + }); + vuexstore.dispatch('performSearch'); } removeTokens() { diff --git a/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql new file mode 100644 index 00000000000..1c382c4747b --- /dev/null +++ b/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql @@ -0,0 +1,10 @@ +query GroupBoardIterations($fullPath: ID!, $title: String) { + group(fullPath: $fullPath) { + iterations(includeAncestors: true, title: $title) { + nodes { + id + title + } + } + } +} diff --git a/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql index 0ff70703e1a..1b14396fb5c 100644 --- a/app/assets/javascripts/boards/graphql/issue.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql @@ -12,6 +12,7 @@ fragment IssueNode on Issue { humanTotalTimeSpent emailsDisabled confidential + hidden webUrl relativePosition assignees { diff --git a/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql new file mode 100644 index 00000000000..078151a275a --- /dev/null +++ b/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql @@ -0,0 +1,10 @@ +query ProjectBoardIterations($fullPath: ID!, $title: String) { + project(fullPath: $fullPath) { + iterations(includeAncestors: true, title: $title) { + nodes { + id + title + } + } + } +} diff --git a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql index 776530ebb83..724b7f5a34c 100644 --- a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql +++ b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql @@ -1,4 +1,4 @@ -query groupMilestones( +query projectMilestones( $fullPath: ID! $state: MilestoneStateEnum $includeAncestors: Boolean diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index de7c8a3bd6b..21c1bb23dc6 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -2,41 +2,20 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import PortalVue from 'portal-vue'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { mapActions, mapGetters } from 'vuex'; -import 'ee_else_ce/boards/models/issue'; -import 'ee_else_ce/boards/models/list'; -import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar'; -import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; -import { - setWeightFetchingState, - setEpicFetchingState, - getMilestoneTitle, -} from 'ee_else_ce/boards/ee_functions'; import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes'; import toggleLabels from 'ee_else_ce/boards/toggle_labels'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; -import BoardContent from '~/boards/components/board_content.vue'; -import './models/label'; -import './models/assignee'; -import '~/boards/models/milestone'; -import '~/boards/models/project'; +import BoardApp from '~/boards/components/board_app.vue'; import '~/boards/filters/due_date_filters'; import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import FilteredSearchBoards from '~/boards/filtered_search_boards'; import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards'; import store from '~/boards/stores'; -import boardsStore from '~/boards/stores/boards_store'; import toggleFocusMode from '~/boards/toggle_focus'; import createDefaultClient from '~/lib/graphql'; -import { - NavigationType, - convertObjectPropsToCamelCase, - parseBoolean, -} from '~/lib/utils/common_utils'; -import { __ } from '~/locale'; -import sidebarEventHub from '~/sidebar/event_hub'; +import { NavigationType, parseBoolean } from '~/lib/utils/common_utils'; import introspectionQueryResultData from '~/sidebar/fragmentTypes.json'; import { fullBoardId } from './boards_util'; import boardConfigToggle from './config_toggle'; @@ -61,10 +40,75 @@ const apolloProvider = new VueApollo({ ), }); -let issueBoardsApp; +function mountBoardApp(el) { + const { boardId, groupId, fullPath, rootPath } = el.dataset; + + store.dispatch('setInitialBoardData', { + boardId, + fullBoardId: fullBoardId(boardId), + fullPath, + boardType: el.dataset.parent, + disabled: parseBoolean(el.dataset.disabled) || true, + issuableType: issuableTypes.issue, + boardConfig: { + milestoneId: parseInt(el.dataset.boardMilestoneId, 10), + milestoneTitle: el.dataset.boardMilestoneTitle || '', + iterationId: parseInt(el.dataset.boardIterationId, 10), + iterationTitle: el.dataset.boardIterationTitle || '', + assigneeId: el.dataset.boardAssigneeId, + assigneeUsername: el.dataset.boardAssigneeUsername, + labels: el.dataset.labels ? JSON.parse(el.dataset.labels) : [], + labelIds: el.dataset.labelIds ? JSON.parse(el.dataset.labelIds) : [], + weight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null, + }, + }); + + if (!gon?.features?.issueBoardsFilteredSearch) { + // Warning: FilteredSearchBoards has an implicit dependency on the Vuex state 'boardConfig' + // Improve this situation in the future. + const filterManager = new FilteredSearchBoards({ path: '' }, true, []); + filterManager.setup(); + + eventHub.$on('updateTokens', () => { + filterManager.updateTokens(); + }); + } + + // eslint-disable-next-line no-new + new Vue({ + el, + store, + apolloProvider, + provide: { + disabled: parseBoolean(el.dataset.disabled), + boardId, + groupId: Number(groupId), + rootPath, + currentUserId: gon.current_user_id || null, + canUpdate: parseBoolean(el.dataset.canUpdate), + canAdminList: parseBoolean(el.dataset.canAdminList), + labelsManagePath: el.dataset.labelsManagePath, + labelsFilterBasePath: el.dataset.labelsFilterBasePath, + timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours), + multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable), + epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable), + iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable), + weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable), + boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null, + scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels), + milestoneListsAvailable: parseBoolean(el.dataset.milestoneListsAvailable), + assigneeListsAvailable: parseBoolean(el.dataset.assigneeListsAvailable), + iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable), + issuableType: issuableTypes.issue, + emailsDisabled: parseBoolean(el.dataset.emailsDisabled), + }, + render: (createComponent) => createComponent(BoardApp), + }); +} export default () => { - const $boardApp = document.getElementById('board-app'); + const $boardApp = document.getElementById('js-issuable-board-app'); + // check for browser back and trigger a hard reload to circumvent browser caching. window.addEventListener('pageshow', (event) => { const isNavTypeBackForward = @@ -75,257 +119,11 @@ export default () => { } }); - if (issueBoardsApp) { - issueBoardsApp.$destroy(true); - } - if (gon?.features?.issueBoardsFilteredSearch) { initBoardsFilteredSearch(apolloProvider); } - if (!gon?.features?.graphqlBoardLists) { - boardsStore.create(); - boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours); - } - - // eslint-disable-next-line @gitlab/no-runtime-template-compiler - issueBoardsApp = new Vue({ - el: $boardApp, - components: { - BoardContent, - BoardSidebar, - BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'), - }, - provide: { - boardId: $boardApp.dataset.boardId, - groupId: Number($boardApp.dataset.groupId), - rootPath: $boardApp.dataset.rootPath, - currentUserId: gon.current_user_id || null, - canUpdate: parseBoolean($boardApp.dataset.canUpdate), - canAdminList: parseBoolean($boardApp.dataset.canAdminList), - labelsManagePath: $boardApp.dataset.labelsManagePath, - labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath, - timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours), - multipleAssigneesFeatureAvailable: parseBoolean( - $boardApp.dataset.multipleAssigneesFeatureAvailable, - ), - epicFeatureAvailable: parseBoolean($boardApp.dataset.epicFeatureAvailable), - iterationFeatureAvailable: parseBoolean($boardApp.dataset.iterationFeatureAvailable), - weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable), - boardWeight: $boardApp.dataset.boardWeight - ? parseInt($boardApp.dataset.boardWeight, 10) - : null, - scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels), - milestoneListsAvailable: parseBoolean($boardApp.dataset.milestoneListsAvailable), - assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable), - iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable), - issuableType: issuableTypes.issue, - emailsDisabled: parseBoolean($boardApp.dataset.emailsDisabled), - }, - store, - apolloProvider, - data() { - return { - state: boardsStore.state, - loading: 0, - boardsEndpoint: $boardApp.dataset.boardsEndpoint, - recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, - listsEndpoint: $boardApp.dataset.listsEndpoint, - disabled: parseBoolean($boardApp.dataset.disabled), - bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, - detailIssue: boardsStore.detail, - parent: $boardApp.dataset.parent, - }; - }, - computed: { - ...mapGetters(['shouldUseGraphQL']), - detailIssueVisible() { - return Object.keys(this.detailIssue.issue).length; - }, - }, - created() { - this.setInitialBoardData({ - boardId: $boardApp.dataset.boardId, - fullBoardId: fullBoardId($boardApp.dataset.boardId), - fullPath: $boardApp.dataset.fullPath, - boardType: this.parent, - disabled: this.disabled, - issuableType: issuableTypes.issue, - boardConfig: { - milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10), - milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '', - iterationId: parseInt($boardApp.dataset.boardIterationId, 10), - iterationTitle: $boardApp.dataset.boardIterationTitle || '', - assigneeId: $boardApp.dataset.boardAssigneeId, - assigneeUsername: $boardApp.dataset.boardAssigneeUsername, - labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels) : [], - labelIds: $boardApp.dataset.labelIds ? JSON.parse($boardApp.dataset.labelIds) : [], - weight: $boardApp.dataset.boardWeight - ? parseInt($boardApp.dataset.boardWeight, 10) - : null, - }, - }); - boardsStore.setEndpoints({ - boardsEndpoint: this.boardsEndpoint, - recentBoardsEndpoint: this.recentBoardsEndpoint, - listsEndpoint: this.listsEndpoint, - bulkUpdatePath: this.bulkUpdatePath, - boardId: $boardApp.dataset.boardId, - fullPath: $boardApp.dataset.fullPath, - }); - boardsStore.rootPath = this.boardsEndpoint; - - eventHub.$on('updateTokens', this.updateTokens); - eventHub.$on('newDetailIssue', this.updateDetailIssue); - eventHub.$on('clearDetailIssue', this.clearDetailIssue); - sidebarEventHub.$on('toggleSubscription', this.toggleSubscription); - eventHub.$on('initialBoardLoad', this.initialBoardLoad); - }, - beforeDestroy() { - eventHub.$off('updateTokens', this.updateTokens); - eventHub.$off('newDetailIssue', this.updateDetailIssue); - eventHub.$off('clearDetailIssue', this.clearDetailIssue); - sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); - eventHub.$off('initialBoardLoad', this.initialBoardLoad); - }, - mounted() { - if (!gon?.features?.issueBoardsFilteredSearch) { - this.filterManager = new FilteredSearchBoards( - boardsStore.filter, - true, - boardsStore.cantEdit, - ); - this.filterManager.setup(); - } - - this.performSearch(); - - boardsStore.disabled = this.disabled; - - if (!this.shouldUseGraphQL) { - this.initialBoardLoad(); - } - }, - methods: { - ...mapActions(['setInitialBoardData', 'performSearch', 'setError']), - initialBoardLoad() { - boardsStore - .all() - .then((res) => res.data) - .then((lists) => { - lists.forEach((list) => boardsStore.addList(list)); - this.loading = false; - }) - .catch((error) => { - this.setError({ - error, - message: __('An error occurred while fetching the board lists. Please try again.'), - }); - }); - }, - updateTokens() { - this.filterManager.updateTokens(); - }, - updateDetailIssue(newIssue, multiSelect = false) { - const { sidebarInfoEndpoint } = newIssue; - if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { - newIssue.setFetchingState('subscriptions', true); - setWeightFetchingState(newIssue, true); - setEpicFetchingState(newIssue, true); - boardsStore - .getIssueInfo(sidebarInfoEndpoint) - .then((res) => res.data) - .then((data) => { - const { - subscribed, - totalTimeSpent, - timeEstimate, - humanTimeEstimate, - humanTotalTimeSpent, - weight, - epic, - assignees, - } = convertObjectPropsToCamelCase(data); - - newIssue.setFetchingState('subscriptions', false); - setWeightFetchingState(newIssue, false); - setEpicFetchingState(newIssue, false); - newIssue.updateData({ - humanTimeSpent: humanTotalTimeSpent, - timeSpent: totalTimeSpent, - humanTimeEstimate, - timeEstimate, - subscribed, - weight, - epic, - assignees, - }); - }) - .catch(() => { - newIssue.setFetchingState('subscriptions', false); - setWeightFetchingState(newIssue, false); - this.setError({ message: __('An error occurred while fetching sidebar data') }); - }); - } - - if (multiSelect) { - boardsStore.toggleMultiSelect(newIssue); - - if (boardsStore.detail.issue) { - boardsStore.clearDetailIssue(); - return; - } - - return; - } - - boardsStore.setIssueDetail(newIssue); - }, - clearDetailIssue(multiSelect = false) { - if (multiSelect) { - boardsStore.clearMultiSelect(); - } - boardsStore.clearDetailIssue(); - }, - toggleSubscription(id) { - const { issue } = boardsStore.detail; - if (issue.id === id && issue.toggleSubscriptionEndpoint) { - issue.setFetchingState('subscriptions', true); - boardsStore - .toggleIssueSubscription(issue.toggleSubscriptionEndpoint) - .then(() => { - issue.setFetchingState('subscriptions', false); - issue.updateData({ - subscribed: !issue.subscribed, - }); - }) - .catch(() => { - issue.setFetchingState('subscriptions', false); - this.setError({ - message: __('An error occurred when toggling the notification subscription'), - }); - }); - } - }, - getNodes(data) { - return data[this.parent]?.board?.lists.nodes; - }, - }, - }); - - // eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler - new Vue({ - el: document.getElementById('js-add-list'), - data() { - return { - filters: boardsStore.state.filters, - ...getMilestoneTitle($boardApp), - }; - }, - mounted() { - initNewListDropdown(); - }, - }); + mountBoardApp($boardApp); const createColumnTriggerEl = document.querySelector('.js-create-column-trigger'); if (createColumnTriggerEl) { @@ -342,7 +140,7 @@ export default () => { }); } - boardConfigToggle(boardsStore); + boardConfigToggle(); toggleFocusMode(); toggleLabels(); diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js deleted file mode 100644 index 1e822d06bfd..00000000000 --- a/app/assets/javascripts/boards/models/assignee.js +++ /dev/null @@ -1,13 +0,0 @@ -export default class ListAssignee { - constructor(obj) { - this.id = obj.id; - this.name = obj.name; - this.username = obj.username; - this.avatar = obj.avatarUrl || obj.avatar_url || obj.avatar || gon.default_avatar_url; - this.path = obj.path; - this.state = obj.state; - this.webUrl = obj.web_url || obj.webUrl; - } -} - -window.ListAssignee = ListAssignee; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js deleted file mode 100644 index 46d1239457d..00000000000 --- a/app/assets/javascripts/boards/models/issue.js +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable no-unused-vars */ -/* global ListLabel */ -/* global ListMilestone */ -/* global ListAssignee */ - -import axios from '~/lib/utils/axios_utils'; -import './label'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import boardsStore from '../stores/boards_store'; -import IssueProject from './project'; - -class ListIssue { - constructor(obj) { - this.subscribed = obj.subscribed; - this.labels = []; - this.assignees = []; - this.selected = false; - this.position = obj.position || obj.relative_position || obj.relativePosition || Infinity; - this.isFetching = { - subscriptions: true, - }; - this.closed = obj.closed; - this.isLoading = {}; - - this.refreshData(obj); - } - - refreshData(obj) { - boardsStore.refreshIssueData(this, obj); - } - - addLabel(label) { - boardsStore.addIssueLabel(this, label); - } - - findLabel(findLabel) { - return boardsStore.findIssueLabel(this, findLabel); - } - - removeLabel(removeLabel) { - boardsStore.removeIssueLabel(this, removeLabel); - } - - removeLabels(labels) { - boardsStore.removeIssueLabels(this, labels); - } - - addAssignee(assignee) { - boardsStore.addIssueAssignee(this, assignee); - } - - findAssignee(findAssignee) { - return boardsStore.findIssueAssignee(this, findAssignee); - } - - setAssignees(assignees) { - boardsStore.setIssueAssignees(this, assignees); - } - - removeAssignee(removeAssignee) { - boardsStore.removeIssueAssignee(this, removeAssignee); - } - - removeAllAssignees() { - boardsStore.removeAllIssueAssignees(this); - } - - addMilestone(milestone) { - boardsStore.addIssueMilestone(this, milestone); - } - - removeMilestone(removeMilestone) { - boardsStore.removeIssueMilestone(this, removeMilestone); - } - - getLists() { - return boardsStore.state.lists.filter((list) => list.findIssue(this.id)); - } - - updateData(newData) { - boardsStore.updateIssueData(this, newData); - } - - setFetchingState(key, value) { - boardsStore.setIssueFetchingState(this, key, value); - } - - setLoadingState(key, value) { - boardsStore.setIssueLoadingState(this, key, value); - } - - update() { - return boardsStore.updateIssue(this); - } -} - -window.ListIssue = ListIssue; - -export default ListIssue; diff --git a/app/assets/javascripts/boards/models/iteration.js b/app/assets/javascripts/boards/models/iteration.js deleted file mode 100644 index b7bdc204f7c..00000000000 --- a/app/assets/javascripts/boards/models/iteration.js +++ /dev/null @@ -1,9 +0,0 @@ -export default class ListIteration { - constructor(obj) { - this.id = obj.id; - this.title = obj.title; - this.state = obj.state; - this.webUrl = obj.web_url || obj.webUrl; - this.description = obj.description; - } -} diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js deleted file mode 100644 index cd2a2c0137f..00000000000 --- a/app/assets/javascripts/boards/models/label.js +++ /dev/null @@ -1,11 +0,0 @@ -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; - -export default class ListLabel { - constructor(obj) { - Object.assign(this, convertObjectPropsToCamelCase(obj, { dropKeys: ['priority'] }), { - priority: obj.priority !== null ? obj.priority : Infinity, - }); - } -} - -window.ListLabel = ListLabel; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js deleted file mode 100644 index ab24532d87f..00000000000 --- a/app/assets/javascripts/boards/models/list.js +++ /dev/null @@ -1,182 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import createFlash from '~/flash'; -import { __ } from '~/locale'; -import boardsStore from '../stores/boards_store'; -import ListAssignee from './assignee'; -import ListIteration from './iteration'; -import ListLabel from './label'; -import ListMilestone from './milestone'; -import 'ee_else_ce/boards/models/issue'; - -const TYPES = { - backlog: { - isPreset: true, - isExpandable: true, - isBlank: false, - }, - closed: { - isPreset: true, - isExpandable: true, - isBlank: false, - }, - blank: { - isPreset: true, - isExpandable: false, - isBlank: true, - }, - default: { - // includes label, assignee, and milestone lists - isPreset: false, - isExpandable: true, - isBlank: false, - }, -}; - -class List { - constructor(obj) { - this.id = obj.id; - this.position = obj.position; - this.title = obj.title; - this.type = obj.list_type || obj.listType; - - const typeInfo = this.getTypeInfo(this.type); - this.preset = Boolean(typeInfo.isPreset); - this.isExpandable = Boolean(typeInfo.isExpandable); - this.isExpanded = !obj.collapsed; - this.page = 1; - this.highlighted = obj.highlighted; - this.loading = true; - this.loadingMore = false; - this.issues = obj.issues || []; - this.issuesSize = obj.issuesSize || obj.issuesCount || 0; - this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0; - - if (obj.label) { - this.label = new ListLabel(obj.label); - } else if (obj.user || obj.assignee) { - this.assignee = new ListAssignee(obj.user || obj.assignee); - this.title = this.assignee.name; - } else if (IS_EE && obj.milestone) { - this.milestone = new ListMilestone(obj.milestone); - this.title = this.milestone.title; - } else if (IS_EE && obj.iteration) { - this.iteration = new ListIteration(obj.iteration); - this.title = this.iteration.title; - } - - // doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards - // Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/229416 - if (!typeInfo.isBlank && this.id && !obj.doNotFetchIssues) { - this.getIssues().catch(() => { - // TODO: handle request error - }); - } - } - - guid() { - const s4 = () => - Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; - } - - save() { - return boardsStore.saveList(this); - } - - destroy() { - boardsStore.destroy(this); - } - - update() { - return boardsStore.updateListFunc(this); - } - - nextPage() { - return boardsStore.goToNextPage(this); - } - - getIssues(emptyIssues = true) { - return boardsStore.getListIssues(this, emptyIssues); - } - - newIssue(issue) { - return boardsStore.newListIssue(this, issue); - } - - addMultipleIssues(issues, listFrom, newIndex) { - boardsStore.addMultipleListIssues(this, issues, listFrom, newIndex); - } - - addIssue(issue, listFrom, newIndex) { - boardsStore.addListIssue(this, issue, listFrom, newIndex); - } - - moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { - boardsStore.moveListIssues(this, issue, oldIndex, newIndex, moveBeforeId, moveAfterId); - } - - moveMultipleIssues({ issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) { - boardsStore - .moveListMultipleIssues({ - list: this, - issues, - oldIndicies, - newIndex, - moveBeforeId, - moveAfterId, - }) - .catch(() => - createFlash({ - message: __('Something went wrong while moving issues.'), - }), - ); - } - - updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) { - boardsStore.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId).catch(() => { - // TODO: handle request error - }); - } - - updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) { - boardsStore - .moveMultipleIssues({ - ids: issues.map((issue) => issue.id), - fromListId: listFrom.id, - toListId: this.id, - moveBeforeId, - moveAfterId, - }) - .catch(() => - createFlash({ - message: __('Something went wrong while moving issues.'), - }), - ); - } - - findIssue(id) { - return boardsStore.findListIssue(this, id); - } - - removeMultipleIssues(removeIssues) { - return boardsStore.removeListMultipleIssues(this, removeIssues); - } - - removeIssue(removeIssue) { - return boardsStore.removeListIssues(this, removeIssue); - } - - getTypeInfo(type) { - return TYPES[type] || TYPES.default; - } - - onNewIssueResponse(issue, data) { - boardsStore.onNewListIssueResponse(this, issue, data); - } -} - -window.List = List; - -export default List; diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js deleted file mode 100644 index 7201b6e91f5..00000000000 --- a/app/assets/javascripts/boards/models/milestone.js +++ /dev/null @@ -1,15 +0,0 @@ -export default class ListMilestone { - constructor(obj) { - this.id = obj.id; - this.title = obj.title; - - if (IS_EE) { - this.path = obj.path; - this.state = obj.state; - this.webUrl = obj.web_url || obj.webUrl; - this.description = obj.description; - } - } -} - -window.ListMilestone = ListMilestone; diff --git a/app/assets/javascripts/boards/models/project.js b/app/assets/javascripts/boards/models/project.js deleted file mode 100644 index 9468a02856e..00000000000 --- a/app/assets/javascripts/boards/models/project.js +++ /dev/null @@ -1,7 +0,0 @@ -export default class IssueProject { - constructor(obj) { - this.id = obj.id; - this.path = obj.path; - this.fullPath = obj.path_with_namespace; - } -} diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index 7d6179a8547..a3a8ad06c43 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -1,12 +1,9 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { mapGetters } from 'vuex'; import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue'; -import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue'; import store from '~/boards/stores'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; Vue.use(VueApollo); @@ -25,9 +22,7 @@ export default (params = {}) => { el: boardsSwitcherElement, components: { BoardsSelector, - BoardsSelectorDeprecated, }, - mixins: [glFeatureFlagMixin()], apolloProvider, store, provide: { @@ -52,16 +47,8 @@ export default (params = {}) => { return { boardsSelectorProps }; }, - computed: { - ...mapGetters(['shouldUseGraphQL', 'isEpicBoard']), - }, render(createElement) { - if (this.shouldUseGraphQL || this.isEpicBoard) { - return createElement(BoardsSelector, { - props: this.boardsSelectorProps, - }); - } - return createElement(BoardsSelectorDeprecated, { + 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 970d00841bd..dc06b62cebb 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -36,11 +36,13 @@ import { filterVariables, } from '../boards_util'; import boardLabelsQuery from '../graphql/board_labels.query.graphql'; +import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql'; import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql'; import listsIssuesQuery from '../graphql/lists_issues.query.graphql'; +import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql'; import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql'; import * as types from './mutation_types'; @@ -82,11 +84,8 @@ export default { 'setFilters', convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })), ); - - if (gon.features.graphqlBoardLists) { - dispatch('fetchLists'); - dispatch('resetIssues'); - } + dispatch('fetchLists'); + dispatch('resetIssues'); }, fetchLists: ({ commit, state, dispatch }) => { @@ -182,7 +181,7 @@ export default { }); }, - fetchLabels: ({ state, commit, getters }, searchTerm) => { + fetchLabels: ({ state, commit }, searchTerm) => { const { fullPath, boardType } = state; const variables = { @@ -200,14 +199,7 @@ export default { variables, }) .then(({ data }) => { - let labels = data[boardType]?.labels.nodes; - - if (!getters.shouldUseGraphQL && !getters.isEpicBoard) { - labels = labels.map((label) => ({ - ...label, - id: getIdFromGraphQLId(label.id), - })); - } + const labels = data[boardType]?.labels.nodes; commit(types.RECEIVE_LABELS_SUCCESS, labels); return labels; @@ -218,6 +210,52 @@ export default { }); }, + fetchIterations({ state, commit }, title) { + commit(types.RECEIVE_ITERATIONS_REQUEST); + + const { fullPath, boardType } = state; + + const variables = { + fullPath, + title, + }; + + let query; + if (boardType === BoardType.project) { + query = projectBoardIterationsQuery; + } + if (boardType === BoardType.group) { + query = groupBoardIterationsQuery; + } + + if (!query) { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('Unknown board type'); + } + + return gqlClient + .query({ + query, + variables, + }) + .then(({ data }) => { + const errors = data[boardType]?.errors; + const iterations = data[boardType]?.iterations.nodes; + + if (errors?.[0]) { + throw new Error(errors[0]); + } + + commit(types.RECEIVE_ITERATIONS_SUCCESS, iterations); + + return iterations; + }) + .catch((e) => { + commit(types.RECEIVE_ITERATIONS_FAILURE); + throw e; + }); + }, + fetchMilestones({ state, commit }, searchTerm) { commit(types.RECEIVE_MILESTONES_REQUEST); @@ -536,8 +574,8 @@ export default { boardId: fullBoardId, fromListId: getIdFromGraphQLId(fromListId), toListId: getIdFromGraphQLId(toListId), - moveBeforeId, - moveAfterId, + moveBeforeId: moveBeforeId ? getIdFromGraphQLId(moveBeforeId) : undefined, + moveAfterId: moveAfterId ? getIdFromGraphQLId(moveAfterId) : undefined, // 'mutationVariables' allows EE code to pass in extra parameters. ...mutationVariables, }, @@ -604,7 +642,7 @@ export default { } const rawIssue = data.createIssue?.issue; - const formattedIssue = formatIssue({ ...rawIssue, id: getIdFromGraphQLId(rawIssue.id) }); + const formattedIssue = formatIssue(rawIssue); dispatch('removeListItem', { listId: list.id, itemId: placeholderId }); dispatch('addListItem', { list, item: formattedIssue, position: 0 }); }) @@ -640,7 +678,7 @@ export default { } commit(types.UPDATE_BOARD_ITEM_BY_ID, { - itemId: getIdFromGraphQLId(data.updateIssue?.issue?.id) || activeBoardItem.id, + itemId: data.updateIssue?.issue?.id || activeBoardItem.id, prop: 'labels', value: data.updateIssue.issue.labels.nodes, }); diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js deleted file mode 100644 index 857b0912c57..00000000000 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ /dev/null @@ -1,883 +0,0 @@ -/* eslint-disable no-shadow, no-param-reassign,consistent-return */ -/* global List */ -/* global ListIssue */ -import { sortBy } from 'lodash'; -import Vue from 'vue'; -import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import createDefaultClient from '~/lib/graphql'; -import axios from '~/lib/utils/axios_utils'; -import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { mergeUrlParams, queryToObject, getUrlParamsArray } from '~/lib/utils/url_utility'; -import { ListType, flashAnimationDuration } from '../constants'; -import eventHub from '../eventhub'; -import ListAssignee from '../models/assignee'; -import ListLabel from '../models/label'; -import ListMilestone from '../models/milestone'; -import IssueProject from '../models/project'; - -const PER_PAGE = 20; -export const gqlClient = createDefaultClient(); - -const boardsStore = { - disabled: false, - timeTracking: { - limitToHours: false, - }, - scopedLabels: { - enabled: false, - }, - filter: { - path: '', - }, - state: { - currentBoard: { - labels: [], - }, - currentPage: '', - endpoints: {}, - }, - detail: { - issue: {}, - list: {}, - }, - moving: { - issue: {}, - list: {}, - }, - multiSelect: { list: [] }, - - setEndpoints({ - boardsEndpoint, - listsEndpoint, - bulkUpdatePath, - boardId, - recentBoardsEndpoint, - fullPath, - }) { - const listsEndpointGenerate = `${listsEndpoint}/generate.json`; - this.state.endpoints = { - boardsEndpoint, - boardId, - listsEndpoint, - listsEndpointGenerate, - bulkUpdatePath, - fullPath, - recentBoardsEndpoint: `${recentBoardsEndpoint}.json`, - }; - }, - create() { - this.state.lists = []; - this.filter.path = getUrlParamsArray().join('&'); - this.detail = { - issue: {}, - list: {}, - }; - }, - showPage(page) { - this.state.currentPage = page; - }, - updateListPosition(listObj) { - const listType = listObj.listType || listObj.list_type; - let { position } = listObj; - if (listType === ListType.closed) { - position = Infinity; - } else if (listType === ListType.backlog) { - position = -1; - } - - const list = new List({ ...listObj, position }); - return list; - }, - addList(listObj) { - const list = this.updateListPosition(listObj); - this.state.lists = sortBy([...this.state.lists, list], 'position'); - return list; - }, - new(listObj) { - const list = this.addList(listObj); - const backlogList = this.findList('type', 'backlog'); - - list - .save() - .then(() => { - list.highlighted = true; - setTimeout(() => { - list.highlighted = false; - }, flashAnimationDuration); - - // Remove any new issues from the backlog - // as they will be visible in the new list - list.issues.forEach(backlogList.removeIssue.bind(backlogList)); - this.state.lists = sortBy(this.state.lists, 'position'); - }) - .catch(() => { - // https://gitlab.com/gitlab-org/gitlab-foss/issues/30821 - }); - }, - - updateNewListDropdown(listId) { - document - .querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`) - ?.classList.remove('is-active'); - }, - - findIssueLabel(issue, findLabel) { - return issue.labels.find((label) => label.id === findLabel.id); - }, - - goToNextPage(list) { - if (list.issuesSize > list.issues.length) { - if (list.issues.length / PER_PAGE >= 1) { - list.page += 1; - } - - return list.getIssues(false); - } - }, - - addListIssue(list, issue, listFrom, newIndex) { - let moveBeforeId = null; - let moveAfterId = null; - - if (!list.findIssue(issue.id)) { - if (newIndex !== undefined) { - list.issues.splice(newIndex, 0, issue); - - if (list.issues[newIndex - 1]) { - moveBeforeId = list.issues[newIndex - 1].id; - } - - if (list.issues[newIndex + 1]) { - moveAfterId = list.issues[newIndex + 1].id; - } - } else { - list.issues.push(issue); - } - - if (list.label) { - issue.addLabel(list.label); - } - - if (list.assignee) { - if (listFrom && listFrom.type === 'assignee') { - issue.removeAssignee(listFrom.assignee); - } - issue.addAssignee(list.assignee); - } - - if (IS_EE && list.milestone) { - if (listFrom && listFrom.type === 'milestone') { - issue.removeMilestone(listFrom.milestone); - } - issue.addMilestone(list.milestone); - } - - if (listFrom) { - list.issuesSize += 1; - - list.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId); - } - } - }, - findListIssue(list, id) { - return list.issues.find((issue) => issue.id === id); - }, - - removeList(id) { - const list = this.findList('id', id); - - if (!list) return; - - this.state.lists = this.state.lists.filter((list) => list.id !== id); - }, - moveList(listFrom, orderLists) { - orderLists.forEach((id, i) => { - const list = this.findList('id', parseInt(id, 10)); - - list.position = i; - }); - listFrom.update(); - }, - - addMultipleListIssues(list, issues, listFrom, newIndex) { - let moveBeforeId = null; - let moveAfterId = null; - - const listHasIssues = issues.every((issue) => list.findIssue(issue.id)); - - if (!listHasIssues) { - if (newIndex !== undefined) { - if (list.issues[newIndex - 1]) { - moveBeforeId = list.issues[newIndex - 1].id; - } - - if (list.issues[newIndex]) { - moveAfterId = list.issues[newIndex].id; - } - - list.issues.splice(newIndex, 0, ...issues); - } else { - list.issues.push(...issues); - } - - if (list.label) { - issues.forEach((issue) => issue.addLabel(list.label)); - } - - if (list.assignee) { - if (listFrom && listFrom.type === 'assignee') { - issues.forEach((issue) => issue.removeAssignee(listFrom.assignee)); - } - issues.forEach((issue) => issue.addAssignee(list.assignee)); - } - - if (IS_EE && list.milestone) { - if (listFrom && listFrom.type === 'milestone') { - issues.forEach((issue) => issue.removeMilestone(listFrom.milestone)); - } - issues.forEach((issue) => issue.addMilestone(list.milestone)); - } - - if (listFrom) { - list.issuesSize += issues.length; - - list.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId); - } - } - }, - - removeListIssues(list, removeIssue) { - list.issues = list.issues.filter((issue) => { - const matchesRemove = removeIssue.id === issue.id; - - if (matchesRemove) { - list.issuesSize -= 1; - issue.removeLabel(list.label); - } - - return !matchesRemove; - }); - }, - removeListMultipleIssues(list, removeIssues) { - const ids = removeIssues.map((issue) => issue.id); - - list.issues = list.issues.filter((issue) => { - const matchesRemove = ids.includes(issue.id); - - if (matchesRemove) { - list.issuesSize -= 1; - issue.removeLabel(list.label); - } - - return !matchesRemove; - }); - }, - - startMoving(list, issue) { - Object.assign(this.moving, { list, issue }); - }, - - onNewListIssueResponse(list, issue, data) { - issue.refreshData(data); - - if (list.issues.length > 1) { - const moveBeforeId = list.issues[1].id; - this.moveIssue(issue.id, null, null, null, moveBeforeId); - } - }, - - moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) { - const issueTo = issues.map((issue) => listTo.findIssue(issue.id)); - const issueLists = issues.map((issue) => issue.getLists()).flat(); - const listLabels = issueLists.map((list) => list.label); - const hasMoveableIssues = issueTo.filter(Boolean).length > 0; - - if (!hasMoveableIssues) { - // Check if target list assignee is already present in this issue - if ( - listTo.type === ListType.assignee && - listFrom.type === ListType.assignee && - issues.some((issue) => issue.findAssignee(listTo.assignee)) - ) { - const targetIssues = issues.map((issue) => listTo.findIssue(issue.id)); - targetIssues.forEach((targetIssue) => targetIssue.removeAssignee(listFrom.assignee)); - } else if (listTo.type === 'milestone') { - const currentMilestones = issues.map((issue) => issue.milestone); - const currentLists = this.state.lists - .filter((list) => list.type === 'milestone' && list.id !== listTo.id) - .filter((list) => - list.issues.some((listIssue) => issues.some((issue) => listIssue.id === issue.id)), - ); - - issues.forEach((issue) => { - currentMilestones.forEach((milestone) => { - issue.removeMilestone(milestone); - }); - }); - - issues.forEach((issue) => { - issue.addMilestone(listTo.milestone); - }); - - currentLists.forEach((currentList) => { - issues.forEach((issue) => { - currentList.removeIssue(issue); - }); - }); - - listTo.addMultipleIssues(issues, listFrom, newIndex); - } else { - // Add to new lists issues if it doesn't already exist - listTo.addMultipleIssues(issues, listFrom, newIndex); - } - } else { - listTo.updateMultipleIssues(issues, listFrom); - issues.forEach((issue) => { - issue.removeLabel(listFrom.label); - }); - } - - if (listTo.type === ListType.closed && listFrom.type !== ListType.backlog) { - issueLists.forEach((list) => { - issues.forEach((issue) => { - list.removeIssue(issue); - }); - }); - - issues.forEach((issue) => { - issue.removeLabels(listLabels); - }); - } else if (listTo.type === ListType.backlog && listFrom.type === ListType.assignee) { - issues.forEach((issue) => { - issue.removeAssignee(listFrom.assignee); - }); - issueLists.forEach((list) => { - issues.forEach((issue) => { - list.removeIssue(issue); - }); - }); - } else if (listTo.type === ListType.backlog && listFrom.type === ListType.milestone) { - issues.forEach((issue) => { - issue.removeMilestone(listFrom.milestone); - }); - issueLists.forEach((list) => { - issues.forEach((issue) => { - list.removeIssue(issue); - }); - }); - } else if ( - this.shouldRemoveIssue(listFrom, listTo) && - this.issuesAreContiguous(listFrom, issues) - ) { - listFrom.removeMultipleIssues(issues); - } - }, - - issuesAreContiguous(list, issues) { - // When there's only 1 issue selected, we can return early. - if (issues.length === 1) return true; - - // Create list of ids for issues involved. - const listIssueIds = list.issues.map((issue) => issue.id); - const movedIssueIds = issues.map((issue) => issue.id); - - // Check if moved issue IDs is sub-array - // of source list issue IDs (i.e. contiguous selection). - return listIssueIds.join('|').includes(movedIssueIds.join('|')); - }, - - moveIssueToList(listFrom, listTo, issue, newIndex) { - const issueTo = listTo.findIssue(issue.id); - const issueLists = issue.getLists(); - const listLabels = issueLists.map((listIssue) => listIssue.label); - - if (!issueTo) { - // Check if target list assignee is already present in this issue - if ( - listTo.type === 'assignee' && - listFrom.type === 'assignee' && - issue.findAssignee(listTo.assignee) - ) { - const targetIssue = listTo.findIssue(issue.id); - targetIssue.removeAssignee(listFrom.assignee); - } else if (listTo.type === 'milestone') { - const currentMilestone = issue.milestone; - const currentLists = this.state.lists - .filter((list) => list.type === 'milestone' && list.id !== listTo.id) - .filter((list) => list.issues.some((listIssue) => issue.id === listIssue.id)); - - issue.removeMilestone(currentMilestone); - issue.addMilestone(listTo.milestone); - currentLists.forEach((currentList) => currentList.removeIssue(issue)); - listTo.addIssue(issue, listFrom, newIndex); - } else { - // Add to new lists issues if it doesn't already exist - listTo.addIssue(issue, listFrom, newIndex); - } - } else { - listTo.updateIssueLabel(issue, listFrom); - issueTo.removeLabel(listFrom.label); - } - - if (listTo.type === 'closed' && listFrom.type !== 'backlog') { - issueLists.forEach((list) => { - list.removeIssue(issue); - }); - issue.removeLabels(listLabels); - } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') { - issue.removeAssignee(listFrom.assignee); - listFrom.removeIssue(issue); - } else if (listTo.type === 'backlog' && listFrom.type === 'milestone') { - issue.removeMilestone(listFrom.milestone); - listFrom.removeIssue(issue); - } else if (this.shouldRemoveIssue(listFrom, listTo)) { - listFrom.removeIssue(issue); - } - }, - shouldRemoveIssue(listFrom, listTo) { - return ( - (listTo.type !== 'label' && listFrom.type === 'assignee') || - (listTo.type !== 'assignee' && listFrom.type === 'label') || - listFrom.type === 'backlog' || - listFrom.type === 'closed' - ); - }, - moveIssueInList(list, issue, oldIndex, newIndex, idArray) { - const beforeId = parseInt(idArray[newIndex - 1], 10) || null; - const afterId = parseInt(idArray[newIndex + 1], 10) || null; - - list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); - }, - moveMultipleIssuesInList({ list, issues, oldIndicies, newIndex, idArray }) { - const beforeId = parseInt(idArray[newIndex - 1], 10) || null; - const afterId = parseInt(idArray[newIndex + issues.length], 10) || null; - list.moveMultipleIssues({ - issues, - oldIndicies, - newIndex, - moveBeforeId: beforeId, - moveAfterId: afterId, - }); - }, - findList(key, val) { - return this.state.lists.find((list) => list[key] === val); - }, - findListByLabelId(id) { - return this.state.lists.find((list) => list.type === 'label' && list.label.id === id); - }, - - toggleFilter(filter) { - const filterPath = this.filter.path.split('&'); - const filterIndex = filterPath.indexOf(filter); - - if (filterIndex === -1) { - filterPath.push(filter); - } else { - filterPath.splice(filterIndex, 1); - } - - this.filter.path = filterPath.join('&'); - - this.updateFiltersUrl(); - - eventHub.$emit('updateTokens'); - }, - - setListDetail(newList) { - this.detail.list = newList; - }, - - updateFiltersUrl() { - window.history.pushState(null, null, `?${this.filter.path}`); - }, - - clearDetailIssue() { - this.setIssueDetail({}); - }, - - setIssueDetail(issueDetail) { - this.detail.issue = issueDetail; - }, - - setTimeTrackingLimitToHours(limitToHours) { - this.timeTracking.limitToHours = parseBoolean(limitToHours); - }, - - generateBoardGid(boardId) { - return `gid://gitlab/Board/${boardId}`; - }, - - generateBoardsPath(id) { - return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`; - }, - - generateIssuesPath(id) { - return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`; - }, - - generateIssuePath(boardId, id) { - return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ - id ? `/${id}` : '' - }`; - }, - - generateMultiDragPath(boardId) { - return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues/bulk_move`; - }, - - all() { - return axios.get(this.state.endpoints.listsEndpoint); - }, - - createList(entityId, entityType) { - const list = { - [entityType]: entityId, - }; - - return axios.post(this.state.endpoints.listsEndpoint, { - list, - }); - }, - - updateList(id, position, collapsed) { - return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, { - list: { - position, - collapsed, - }, - }); - }, - - updateListFunc(list) { - const collapsed = !list.isExpanded; - return this.updateList(list.id, list.position, collapsed).catch(() => { - // TODO: handle request error - }); - }, - - destroyList(id) { - return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`); - }, - destroy(list) { - const index = this.state.lists.indexOf(list); - this.state.lists.splice(index, 1); - this.updateNewListDropdown(list.id); - - this.destroyList(list.id).catch(() => { - // TODO: handle request error - }); - }, - - saveList(list) { - const entity = list.label || list.assignee || list.milestone || list.iteration; - let entityType = ''; - if (list.label) { - entityType = 'label_id'; - } else if (list.assignee) { - entityType = 'assignee_id'; - } else if (IS_EE && list.milestone) { - entityType = 'milestone_id'; - } else if (IS_EE && list.iteration) { - entityType = 'iteration_id'; - } - - return this.createList(entity.id, entityType) - .then((res) => res.data) - .then((data) => { - list.id = data.id; - list.type = data.list_type; - list.position = data.position; - list.label = data.label; - - return list.getIssues(); - }); - }, - - getListIssues(list, emptyIssues = true) { - const data = { - ...queryToObject(this.filter.path, { gatherArrays: true }), - page: list.page, - }; - - if (list.label && data.label_name) { - data.label_name = data.label_name.filter((label) => label !== list.label.title); - } - - if (emptyIssues) { - list.loading = true; - } - - return this.getIssuesForList(list.id, data) - .then((res) => res.data) - .then((data) => { - list.loading = false; - list.issuesSize = data.size; - - if (emptyIssues) { - list.issues = []; - } - - data.issues.forEach((issueObj) => { - list.addIssue(new ListIssue(issueObj)); - }); - - return data; - }); - }, - - getIssuesForList(id, filter = {}) { - const data = { id }; - Object.keys(filter).forEach((key) => { - data[key] = filter[key]; - }); - - return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); - }, - - moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { - return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), { - from_list_id: fromListId, - to_list_id: toListId, - move_before_id: moveBeforeId, - move_after_id: moveAfterId, - }); - }, - - moveListIssues(list, issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { - list.issues.splice(oldIndex, 1); - list.issues.splice(newIndex, 0, issue); - - this.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => { - // TODO: handle request error - }); - }, - - moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }) { - return axios.put(this.generateMultiDragPath(this.state.endpoints.boardId), { - from_list_id: fromListId, - to_list_id: toListId, - move_before_id: moveBeforeId, - move_after_id: moveAfterId, - ids, - }); - }, - - moveListMultipleIssues({ list, issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) { - oldIndicies.reverse().forEach((index) => { - list.issues.splice(index, 1); - }); - list.issues.splice(newIndex, 0, ...issues); - - return this.moveMultipleIssues({ - ids: issues.map((issue) => issue.id), - fromListId: null, - toListId: null, - moveBeforeId, - moveAfterId, - }); - }, - - newIssue(id, issue) { - if (typeof id === 'string') { - id = getIdFromGraphQLId(id); - } - - return axios.post(this.generateIssuesPath(id), { - issue, - }); - }, - - newListIssue(list, issue) { - list.addIssue(issue, null, 0); - list.issuesSize += 1; - let listId = list.id; - if (typeof listId === 'string') { - listId = getIdFromGraphQLId(listId); - } - - return this.newIssue(list.id, issue) - .then((res) => res.data) - .then((data) => list.onNewIssueResponse(issue, data)); - }, - - getBacklog(data) { - return axios.get( - mergeUrlParams( - data, - `${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`, - ), - ); - }, - removeIssueLabel(issue, removeLabel) { - if (removeLabel) { - issue.labels = issue.labels.filter((label) => removeLabel.id !== label.id); - } - }, - - addIssueAssignee(issue, assignee) { - if (!issue.findAssignee(assignee)) { - issue.assignees.push(new ListAssignee(assignee)); - } - }, - - setIssueAssignees(issue, assignees) { - issue.assignees = [...assignees]; - }, - - removeIssueLabels(issue, labels) { - labels.forEach(issue.removeLabel.bind(issue)); - }, - - bulkUpdate(issueIds, extraData = {}) { - const data = { - update: Object.assign(extraData, { - issuable_ids: issueIds.join(','), - }), - }; - - return axios.post(this.state.endpoints.bulkUpdatePath, data); - }, - - getIssueInfo(endpoint) { - return axios.get(endpoint); - }, - - toggleIssueSubscription(endpoint) { - return axios.post(endpoint); - }, - - recentBoards() { - return axios.get(this.state.endpoints.recentBoardsEndpoint); - }, - - setCurrentBoard(board) { - this.state.currentBoard = board; - }, - - toggleMultiSelect(issue) { - const selectedIssueIds = this.multiSelect.list.map((issue) => issue.id); - const index = selectedIssueIds.indexOf(issue.id); - - if (index === -1) { - this.multiSelect.list.push(issue); - return; - } - - this.multiSelect.list = [ - ...this.multiSelect.list.slice(0, index), - ...this.multiSelect.list.slice(index + 1), - ]; - }, - removeIssueAssignee(issue, removeAssignee) { - if (removeAssignee) { - issue.assignees = issue.assignees.filter((assignee) => assignee.id !== removeAssignee.id); - } - }, - - findIssueAssignee(issue, findAssignee) { - return issue.assignees.find((assignee) => assignee.id === findAssignee.id); - }, - - clearMultiSelect() { - this.multiSelect.list = []; - }, - - removeAllIssueAssignees(issue) { - issue.assignees = []; - }, - - addIssueMilestone(issue, milestone) { - const miletoneId = issue.milestone ? issue.milestone.id : null; - if (IS_EE && milestone.id !== miletoneId) { - issue.milestone = new ListMilestone(milestone); - } - }, - - setIssueLoadingState(issue, key, value) { - issue.isLoading[key] = value; - }, - - updateIssueData(issue, newData) { - Object.assign(issue, newData); - }, - - setIssueFetchingState(issue, key, value) { - issue.isFetching[key] = value; - }, - - removeIssueMilestone(issue, removeMilestone) { - if (IS_EE && removeMilestone && removeMilestone.id === issue.milestone.id) { - issue.milestone = {}; - } - }, - - refreshIssueData(issue, obj) { - const convertedObj = convertObjectPropsToCamelCase(obj, { - dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'], - }); - convertedObj.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; - issue.path = obj.real_path || obj.webUrl; - issue.project_id = obj.project_id; - Object.assign(issue, convertedObj); - - if (obj.project) { - issue.project = new IssueProject(obj.project); - } - - if (obj.milestone) { - issue.milestone = new ListMilestone(obj.milestone); - issue.milestone_id = obj.milestone.id; - } - - if (obj.labels) { - issue.labels = obj.labels.map((label) => new ListLabel(label)); - } - - if (obj.assignees) { - issue.assignees = obj.assignees.map((a) => new ListAssignee(a)); - } - }, - addIssueLabel(issue, label) { - if (!issue.findLabel(label)) { - issue.labels.push(new ListLabel(label)); - } - }, - updateIssue(issue) { - const data = { - issue: { - milestone_id: issue.milestone ? issue.milestone.id : null, - due_date: issue.dueDate, - assignee_ids: issue.assignees.length > 0 ? issue.assignees.map(({ id }) => id) : [0], - label_ids: issue.labels.length > 0 ? issue.labels.map(({ id }) => id) : [''], - }, - }; - - return axios.patch(`${issue.path}.json`, data).then(({ data: body = {} } = {}) => { - /** - * Since post implementation of Scoped labels, server can reject - * same key-ed labels. To keep the UI and server Model consistent, - * we're just assigning labels that server echo's back to us when we - * PATCH the said object. - */ - if (body) { - issue.labels = convertObjectPropsToCamelCase(body.labels, { deep: true }); - } - }); - }, -}; - -BoardsStoreEE.initEESpecific(boardsStore); - -// hacks added in order to allow milestone_select to function properly -// TODO: remove these - -export function boardStoreIssueSet(...args) { - Vue.set(boardsStore.detail.issue, ...args); -} - -export function boardStoreIssueDelete(...args) { - Vue.delete(boardsStore.detail.issue, ...args); -} - -export default boardsStore; diff --git a/app/assets/javascripts/boards/stores/boards_store_ee.js b/app/assets/javascripts/boards/stores/boards_store_ee.js deleted file mode 100644 index 2a289ce5d0a..00000000000 --- a/app/assets/javascripts/boards/stores/boards_store_ee.js +++ /dev/null @@ -1,5 +0,0 @@ -// this is just to make ee_else_ce happy and will be cleaned up in https://gitlab.com/gitlab-org/gitlab-foss/issues/59807 - -export default { - initEESpecific() {}, -}; diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index 140c9ef7ac4..cb31eb4b008 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -16,7 +16,7 @@ export default { }, activeBoardItem: (state) => { - return state.boardItems[state.activeId] || { iid: '', id: '', fullId: '' }; + return state.boardItems[state.activeId] || { iid: '', id: '' }; }, groupPathForActiveIssue: (_, getters) => { @@ -51,8 +51,4 @@ export default { isEpicBoard: () => { return false; }, - - shouldUseGraphQL: () => { - return gon?.features?.graphqlBoardLists; - }, }; diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 31b78014525..928cece19f7 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -41,3 +41,7 @@ export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS'; export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS'; export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION'; export const SET_ERROR = 'SET_ERROR'; + +export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST'; +export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS'; +export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 668a3dbaa7e..ef5b84b4575 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -1,6 +1,5 @@ import { cloneDeep, pull, union } from 'lodash'; import Vue from 'vue'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { s__, __ } from '~/locale'; import { formatIssue } from '../boards_util'; import { issuableTypes } from '../constants'; @@ -65,6 +64,20 @@ export default { ); }, + [mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) { + state.iterationsLoading = true; + }, + + [mutationTypes.RECEIVE_ITERATIONS_SUCCESS](state, iterations) { + state.iterations = iterations; + state.iterationsLoading = false; + }, + + [mutationTypes.RECEIVE_ITERATIONS_FAILURE](state) { + state.iterationsLoading = false; + state.error = __('Failed to load iterations.'); + }, + [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) { state.activeId = id; state.sidebarType = sidebarType; @@ -187,8 +200,7 @@ export default { }, [mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => { - const issueId = getIdFromGraphQLId(issue.id); - Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId })); + Vue.set(state.boardItems, issue.id, formatIssue(issue)); }, [mutationTypes.ADD_BOARD_ITEM_TO_LIST]: ( diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index 264a03ff39d..80c51c966d2 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -31,6 +31,8 @@ export default () => ({ }, selectedProject: {}, error: undefined, + iterations: [], + iterationsLoading: false, addColumnForm: { visible: false, columnType: ListType.label, |