diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 13:18:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 13:18:24 +0000 |
commit | 0653e08efd039a5905f3fa4f6e9cef9f5d2f799c (patch) | |
tree | 4dcc884cf6d81db44adae4aa99f8ec1233a41f55 /app/assets/javascripts/boards/components | |
parent | 744144d28e3e7fddc117924fef88de5d9674fe4c (diff) | |
download | gitlab-ce-0653e08efd039a5905f3fa4f6e9cef9f5d2f799c.tar.gz |
Add latest changes from gitlab-org/gitlab@14-3-stable-eev14.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards/components')
23 files changed, 106 insertions, 2397 deletions
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> |