summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/boards/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-20 13:18:24 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-20 13:18:24 +0000
commit0653e08efd039a5905f3fa4f6e9cef9f5d2f799c (patch)
tree4dcc884cf6d81db44adae4aa99f8ec1233a41f55 /app/assets/javascripts/boards/components
parent744144d28e3e7fddc117924fef88de5d9674fe4c (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column.vue32
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue29
-rw-r--r--app/assets/javascripts/boards/components/board_card_deprecated.vue61
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout_deprecated.vue101
-rw-r--r--app/assets/javascripts/boards/components/board_column_deprecated.vue112
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue31
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue19
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue459
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_deprecated.vue361
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue138
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue53
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js115
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue360
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue8
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue47
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue247
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue48
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js119
-rw-r--r--app/assets/javascripts/boards/components/project_select_deprecated.vue146
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">
- &#8226;
- <gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')">
- <template #issuesSize>{{ issuesTooltipLabel }}</template>
- <template #maxIssueCount>{{ list.maxIssueCount }}</template>
- </gl-sprintf>
- </div>
- <div v-else>&#8226; {{ issuesTooltipLabel }}</div>
- <div v-if="weightFeatureAvailable">
- &#8226;
- <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>