summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/boards
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/boards_util.js28
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column.vue143
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue131
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_trigger.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue83
-rw-r--r--app/assets/javascripts/boards/components/board_card_deprecated.vue61
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue (renamed from app/assets/javascripts/boards/components/issue_card_inner.vue)84
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout.vue98
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout_deprecated.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue21
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue31
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue75
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue91
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue51
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js6
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue43
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue7
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue64
-rw-r--r--app/assets/javascripts/boards/components/filtered_search.vue54
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue2
-rw-r--r--app/assets/javascripts/boards/components/item_count.vue (renamed from app/assets/javascripts/boards/components/issue_count.vue)10
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue6
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue11
-rw-r--r--app/assets/javascripts/boards/components/project_select_deprecated.vue1
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue88
-rw-r--r--app/assets/javascripts/boards/config_toggle.js25
-rw-r--r--app/assets/javascripts/boards/constants.js5
-rw-r--r--app/assets/javascripts/boards/filtered_search.js25
-rw-r--r--app/assets/javascripts/boards/graphql/board_labels.query.graphql4
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql20
-rw-r--r--app/assets/javascripts/boards/graphql/group_projects.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/users_search.query.graphql11
-rw-r--r--app/assets/javascripts/boards/index.js29
-rw-r--r--app/assets/javascripts/boards/mixins/board_card_inner.js (renamed from app/assets/javascripts/boards/mixins/issue_card_inner.js)4
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js6
-rw-r--r--app/assets/javascripts/boards/stores/actions.js107
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js4
-rw-r--r--app/assets/javascripts/boards/stores/getters.js24
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js10
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js113
-rw-r--r--app/assets/javascripts/boards/stores/state.js14
48 files changed, 1083 insertions, 546 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 13ad820477f..2cd25f58770 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -36,11 +36,11 @@ export function formatIssue(issue) {
}
export function formatListIssues(listIssues) {
- const issues = {};
- let listIssuesCount;
+ const boardItems = {};
+ let listItemsCount;
const listData = listIssues.nodes.reduce((map, list) => {
- listIssuesCount = list.issues.count;
+ listItemsCount = list.issues.count;
let sortedIssues = list.issues.edges.map((issueNode) => ({
...issueNode.node,
}));
@@ -58,14 +58,14 @@ export function formatListIssues(listIssues) {
assignees: i.assignees?.nodes || [],
};
- issues[id] = listIssue;
+ boardItems[id] = listIssue;
return id;
}),
};
}, {});
- return { listData, issues, listIssuesCount };
+ return { listData, boardItems, listItemsCount };
}
export function formatListsPageInfo(lists) {
@@ -113,31 +113,31 @@ export function formatIssueInput(issueInput, boardConfig) {
};
}
-export function moveIssueListHelper(issue, fromList, toList) {
- const updatedIssue = issue;
+export function moveItemListHelper(item, fromList, toList) {
+ const updatedItem = item;
if (
toList.listType === ListType.label &&
- !updatedIssue.labels.find((label) => label.id === toList.label.id)
+ !updatedItem.labels.find((label) => label.id === toList.label.id)
) {
- updatedIssue.labels.push(toList.label);
+ updatedItem.labels.push(toList.label);
}
if (fromList?.label && fromList.listType === ListType.label) {
- updatedIssue.labels = updatedIssue.labels.filter((label) => fromList.label.id !== label.id);
+ updatedItem.labels = updatedItem.labels.filter((label) => fromList.label.id !== label.id);
}
if (
toList.listType === ListType.assignee &&
- !updatedIssue.assignees.find((assignee) => assignee.id === toList.assignee.id)
+ !updatedItem.assignees.find((assignee) => assignee.id === toList.assignee.id)
) {
- updatedIssue.assignees.push(toList.assignee);
+ updatedItem.assignees.push(toList.assignee);
}
if (fromList?.assignee && fromList.listType === ListType.assignee) {
- updatedIssue.assignees = updatedIssue.assignees.filter(
+ updatedItem.assignees = updatedItem.assignees.filter(
(assignee) => assignee.id !== fromList.assignee.id,
);
}
- return updatedIssue;
+ return updatedItem;
}
export function isListDraggable(list) {
diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue
new file mode 100644
index 00000000000..3c7c792b787
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_add_new_column.vue
@@ -0,0 +1,143 @@
+<script>
+import {
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlLabel,
+ 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';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+export default {
+ components: {
+ BoardAddNewColumnForm,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlLabel,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['scopedLabelsAvailable'],
+ data() {
+ return {
+ selectedId: null,
+ };
+ },
+ computed: {
+ ...mapState(['labels', 'labelsLoading']),
+ ...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
+ selectedLabel() {
+ if (!this.selectedId) {
+ return null;
+ }
+ return this.labels.find(({ id }) => id === this.selectedId);
+ },
+ columnForSelected() {
+ return this.getListByLabelId(this.selectedId);
+ },
+ },
+ created() {
+ this.filterItems();
+ },
+ 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;
+ }
+
+ this.setAddColumnFormVisibility(false);
+
+ if (this.columnForSelected) {
+ const listId = this.columnForSelected.id;
+ this.highlight(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);
+ }
+ },
+
+ filterItems(searchTerm) {
+ this.fetchLabels(searchTerm);
+ },
+
+ showScopedLabels(label) {
+ return this.scopedLabelsAvailable && isScopedLabel(label);
+ },
+ },
+};
+</script>
+
+<template>
+ <board-add-new-column-form
+ :loading="labelsLoading"
+ :form-description="__('A label list displays issues with the selected label.')"
+ :search-label="__('Select label')"
+ :search-placeholder="__('Search labels')"
+ :selected-id="selectedId"
+ @filter-items="filterItems"
+ @add-list="addList"
+ >
+ <template slot="selected">
+ <gl-label
+ v-if="selectedLabel"
+ v-gl-tooltip
+ :title="selectedLabel.title"
+ :description="selectedLabel.description"
+ :background-color="selectedLabel.color"
+ :scoped="showScopedLabels(selectedLabel)"
+ />
+ </template>
+
+ <template slot="items">
+ <gl-form-radio-group
+ v-if="labels.length > 0"
+ v-model="selectedId"
+ class="gl-overflow-y-auto gl-px-5 gl-pt-3"
+ >
+ <label
+ v-for="label in labels"
+ :key="label.id"
+ class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
+ >
+ <gl-form-radio :value="label.id" class="gl-mb-0" />
+ <span
+ class="dropdown-label-box gl-top-0"
+ :style="{
+ backgroundColor: label.color,
+ }"
+ ></span>
+ <span>{{ label.title }}</span>
+ </label>
+ </gl-form-radio-group>
+ </template>
+ </board-add-new-column-form>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
new file mode 100644
index 00000000000..d85343a5390
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
@@ -0,0 +1,131 @@
+<script>
+import { GlButton, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
+import { mapActions } from 'vuex';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ add: __('Add to board'),
+ cancel: __('Cancel'),
+ newList: __('New list'),
+ noneSelected: __('None'),
+ noResults: __('No matching results'),
+ selected: __('Selected'),
+ },
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlSearchBoxByType,
+ GlSkeletonLoader,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ formDescription: {
+ type: String,
+ required: true,
+ },
+ searchLabel: {
+ type: String,
+ required: true,
+ },
+ searchPlaceholder: {
+ type: String,
+ required: true,
+ },
+ selectedId: {
+ type: [Number, String],
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchValue: '',
+ };
+ },
+ methods: {
+ ...mapActions(['setAddColumnFormVisibility']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="board-add-new-list board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0"
+ data-testid="board-add-new-column"
+ data-qa-selector="board_add_new_list"
+ >
+ <div
+ class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white"
+ >
+ <h3
+ class="gl-font-base gl-px-5 gl-py-5 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ data-testid="board-add-column-form-title"
+ >
+ {{ $options.i18n.newList }}
+ </h3>
+
+ <div class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-hidden">
+ <slot name="select-list-type">
+ <div class="gl-mb-5"></div>
+ </slot>
+
+ <p class="gl-px-5">{{ formDescription }}</p>
+
+ <div class="gl-px-5 gl-pb-4">
+ <label class="gl-mb-2">{{ $options.i18n.selected }}</label>
+ <slot name="selected">
+ <div class="gl-text-gray-500">{{ $options.i18n.noneSelected }}</div>
+ </slot>
+ </div>
+
+ <gl-form-group
+ class="gl-mx-5 gl-mb-3"
+ :label="searchLabel"
+ label-for="board-available-column-entities"
+ >
+ <gl-search-box-by-type
+ id="board-available-column-entities"
+ v-model="searchValue"
+ debounce="250"
+ :placeholder="searchPlaceholder"
+ @input="$emit('filter-items', $event)"
+ />
+ </gl-form-group>
+
+ <div v-if="loading" class="gl-px-5">
+ <gl-skeleton-loader :width="500" :height="172">
+ <rect width="480" height="20" x="10" y="15" rx="4" />
+ <rect width="380" height="20" x="10" y="50" rx="4" />
+ <rect width="430" height="20" x="10" y="85" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+
+ <slot v-else name="items">
+ <p class="gl-mx-5">{{ $options.i18n.noResults }}</p>
+ </slot>
+ </div>
+ <div
+ class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ >
+ <gl-button
+ data-testid="cancelAddNewColumn"
+ class="gl-ml-auto gl-mr-3"
+ @click="setAddColumnFormVisibility(false)"
+ >{{ $options.i18n.cancel }}</gl-button
+ >
+ <gl-button
+ data-testid="addNewColumnButton"
+ :disabled="!selectedId"
+ variant="confirm"
+ class="gl-mr-4"
+ @click="$emit('add-list')"
+ >{{ $options.i18n.add }}</gl-button
+ >
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
index 85fca589279..7c08e33be7e 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
@@ -13,7 +13,7 @@ export default {
</script>
<template>
- <span class="gl-ml-3 gl-display-flex gl-align-items-center">
+ <span class="gl-ml-3 gl-display-flex gl-align-items-center" data-testid="boards-create-list">
<gl-button variant="confirm" @click="setAddColumnFormVisibility(true)"
>{{ __('Create list') }}
</gl-button>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index e6009343626..aacea0b970c 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,14 +1,11 @@
<script>
-import sidebarEventHub from '~/sidebar/event_hub';
-import eventHub from '../eventhub';
-import boardsStore from '../stores/boards_store';
-import BoardCardLayout from './board_card_layout.vue';
-import BoardCardLayoutDeprecated from './board_card_layout_deprecated.vue';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import BoardCardInner from './board_card_inner.vue';
export default {
- name: 'BoardsIssueCard',
+ name: 'BoardCard',
components: {
- BoardCardLayout: gon.features?.graphqlBoardLists ? BoardCardLayout : BoardCardLayoutDeprecated,
+ BoardCardInner,
},
props: {
list: {
@@ -16,34 +13,46 @@ export default {
default: () => ({}),
required: false,
},
- issue: {
+ item: {
type: Object,
default: () => ({}),
required: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ required: false,
+ },
},
- methods: {
- // These are methods instead of computed's, because boardsStore is not reactive.
+ computed: {
+ ...mapState(['selectedBoardItems', 'activeId']),
+ ...mapGetters(['isSwimlanesOn']),
isActive() {
- return this.getActiveId() === this.issue.id;
+ return this.item.id === this.activeId;
},
- getActiveId() {
- return boardsStore.detail?.issue?.id;
+ multiSelectVisible() {
+ return (
+ !this.activeId &&
+ this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1
+ );
},
- 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);
+ },
+ methods: {
+ ...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']),
+ toggleIssue(e) {
+ // Don't do anything if this happened on a no trigger element
+ if (e.target.classList.contains('js-no-trigger')) return;
- if (isMultiSelect) {
- eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
- }
+ const isMultiSelect = e.ctrlKey || e.metaKey;
+ if (isMultiSelect) {
+ this.toggleBoardItemMultiSelection(this.item);
} else {
- eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
- boardsStore.setListDetail(this.list);
+ this.toggleBoardItem({ boardItem: this.item });
}
},
},
@@ -51,12 +60,22 @@ export default {
</script>
<template>
- <board-card-layout
+ <li
data-qa-selector="board_card"
- :issue="issue"
- :list="list"
- :is-active="isActive()"
- v-bind="$attrs"
- @show="showIssue"
- />
+ :class="{
+ 'multi-select': multiSelectVisible,
+ 'user-can-drag': !disabled && item.id,
+ 'is-disabled': disabled || !item.id,
+ 'is-active': isActive,
+ }"
+ :index="index"
+ :data-item-id="item.id"
+ :data-item-iid="item.iid"
+ :data-item-path="item.referencePath"
+ data-testid="board_card"
+ class="board-card gl-p-5 gl-rounded-base"
+ @mouseup="toggleIssue($event)"
+ >
+ <board-card-inner :list="list" :item="item" :update-filters="true" />
+ </li>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card_deprecated.vue b/app/assets/javascripts/boards/components/board_card_deprecated.vue
new file mode 100644
index 00000000000..e12a2836f67
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card_deprecated.vue
@@ -0,0 +1,61 @@
+<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/issue_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index e5ea30df767..d4d6b17a589 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -1,8 +1,8 @@
<script>
import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
-import { mapActions, mapState } from 'vuex';
-import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
@@ -26,10 +26,10 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [issueCardInner],
- inject: ['groupId', 'rootPath', 'scopedLabelsAvailable'],
+ mixins: [boardCardInner],
+ inject: ['rootPath', 'scopedLabelsAvailable'],
props: {
- issue: {
+ item: {
type: Object,
required: true,
},
@@ -53,18 +53,19 @@ export default {
},
computed: {
...mapState(['isShowingLabels']),
+ ...mapGetters(['isEpicBoard']),
cappedAssignees() {
// e.g. 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 this.issue.assignees.slice(0, this.maxRender);
+ if (this.item.assignees.length <= this.maxRender) {
+ return this.item.assignees.slice(0, this.maxRender);
}
- return this.issue.assignees.slice(0, this.limitBeforeCounter);
+ return this.item.assignees.slice(0, this.limitBeforeCounter);
},
numberOverLimit() {
- return this.issue.assignees.length - this.limitBeforeCounter;
+ return this.item.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
const { numberOverLimit, maxCounter } = this;
@@ -79,31 +80,35 @@ export default {
return `+${this.numberOverLimit}`;
},
shouldRenderCounter() {
- if (this.issue.assignees.length <= this.maxRender) {
+ if (this.item.assignees.length <= this.maxRender) {
return false;
}
- return this.issue.assignees.length > this.numberOverLimit;
+ return this.item.assignees.length > this.numberOverLimit;
},
- issueId() {
- if (this.issue.iid) {
- return `#${this.issue.iid}`;
+ itemPrefix() {
+ return this.isEpicBoard ? '&' : '#';
+ },
+
+ itemId() {
+ if (this.item.iid) {
+ return `${this.itemPrefix}${this.item.iid}`;
}
return false;
},
showLabelFooter() {
- return this.isShowingLabels && this.issue.labels.find(this.showLabel);
+ return this.isShowingLabels && this.item.labels.find(this.showLabel);
},
- issueReferencePath() {
- const { referencePath, groupId } = this.issue;
- return !groupId ? referencePath.split('#')[0] : null;
+ itemReferencePath() {
+ const { referencePath } = this.item;
+ return referencePath.split(this.itemPrefix)[0];
},
orderedLabels() {
- return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title');
+ return sortBy(this.item.labels.filter(this.isNonListLabel), 'title');
},
blockedLabel() {
- if (this.issue.blockedByCount) {
- return n__(`Blocked by %d issue`, `Blocked by %d issues`, this.issue.blockedByCount);
+ if (this.item.blockedByCount) {
+ return n__(`Blocked by %d issue`, `Blocked by %d issues`, this.item.blockedByCount);
}
return __('Blocked issue');
},
@@ -160,7 +165,7 @@ export default {
<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-if="item.blocked"
v-gl-tooltip
name="issue-block"
:title="blockedLabel"
@@ -169,7 +174,7 @@ export default {
data-testid="issue-blocked-icon"
/>
<gl-icon
- v-if="issue.confidential"
+ v-if="item.confidential"
v-gl-tooltip
name="eye-slash"
:title="__('Confidential')"
@@ -177,11 +182,11 @@ export default {
:aria-label="__('Confidential')"
/>
<a
- :href="issue.path || issue.webUrl || ''"
- :title="issue.title"
+ :href="item.path || item.webUrl || ''"
+ :title="item.title"
class="js-no-trigger"
@mousemove.stop
- >{{ issue.title }}</a
+ >{{ item.title }}</a
>
</h4>
</div>
@@ -205,29 +210,30 @@ export default {
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"
+ v-if="item.referencePath"
class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
+ :class="{ 'gl-font-base': isEpicBoard }"
>
<tooltip-on-truncate
- v-if="issueReferencePath"
- :title="issueReferencePath"
+ v-if="itemReferencePath"
+ :title="itemReferencePath"
placement="bottom"
- class="board-issue-path gl-text-truncate gl-font-weight-bold"
- >{{ issueReferencePath }}</tooltip-on-truncate
+ class="board-item-path gl-text-truncate gl-font-weight-bold"
+ >{{ itemReferencePath }}</tooltip-on-truncate
>
- #{{ issue.iid }}
+ {{ itemId }}
</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)"
+ v-if="item.dueDate"
+ :date="item.dueDate"
+ :closed="item.closed || Boolean(item.closedAt)"
/>
- <issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
+ <issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
<issue-card-weight
- v-if="validIssueWeight"
- :weight="issue.weight"
- @click="filterByWeight(issue.weight)"
+ v-if="validIssueWeight(item)"
+ :weight="item.weight"
+ @click="filterByWeight(item.weight)"
/>
</span>
</div>
diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue
deleted file mode 100644
index 5e3c3702519..00000000000
--- a/app/assets/javascripts/boards/components/board_card_layout.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
-import { ISSUABLE } from '~/boards/constants';
-import IssueCardInner from './issue_card_inner.vue';
-
-export default {
- name: 'BoardCardLayout',
- components: {
- IssueCardInner,
- },
- 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,
- };
- },
- computed: {
- ...mapState(['selectedBoardItems']),
- ...mapGetters(['isSwimlanesOn']),
- multiSelectVisible() {
- return this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.issue.id) > -1;
- },
- },
- methods: {
- ...mapActions(['setActiveId', 'toggleBoardItemMultiSelection']),
- 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;
-
- const isMultiSelect = e.ctrlKey || e.metaKey;
-
- if (!isMultiSelect) {
- this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
- } else {
- this.toggleBoardItemMultiSelection(this.issue);
- }
-
- if (this.showDetail || isMultiSelect) {
- this.showDetail = false;
- }
- },
- },
-};
-</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_card_layout_deprecated.vue b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
index f9a726134a3..3381e4c3a7d 100644
--- a/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
@@ -3,13 +3,12 @@ import { mapActions, mapGetters } from 'vuex';
import { ISSUABLE } from '~/boards/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import boardsStore from '../stores/boards_store';
-import IssueCardInner from './issue_card_inner.vue';
import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
export default {
name: 'BoardCardLayout',
components: {
- IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
+ IssueCardInner: IssueCardInnerDeprecated,
},
mixins: [glFeatureFlagMixin()],
props: {
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 41b9ee795eb..c9e667d526c 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -32,12 +32,12 @@ export default {
},
computed: {
...mapState(['filterParams', 'highlightedLists']),
- ...mapGetters(['getIssuesByList']),
+ ...mapGetters(['getBoardItemsByList']),
highlighted() {
return this.highlightedLists.includes(this.list.id);
},
- listIssues() {
- return this.getIssuesByList(this.list.id);
+ listItems() {
+ return this.getBoardItemsByList(this.list.id);
},
isListDraggable() {
return isListDraggable(this.list);
@@ -46,11 +46,20 @@ export default {
watch: {
filterParams: {
handler() {
- this.fetchIssuesForList({ listId: this.list.id });
+ if (this.list.id) {
+ this.fetchItemsForList({ listId: this.list.id });
+ }
},
deep: true,
immediate: true,
},
+ 'list.id': {
+ handler(id) {
+ if (id) {
+ this.fetchItemsForList({ listId: this.list.id });
+ }
+ },
+ },
highlighted: {
handler(highlighted) {
if (highlighted) {
@@ -63,7 +72,7 @@ export default {
},
},
methods: {
- ...mapActions(['fetchIssuesForList']),
+ ...mapActions(['fetchItemsForList']),
},
};
</script>
@@ -87,7 +96,7 @@ export default {
<board-list
ref="board-list"
:disabled="disabled"
- :issues="listIssues"
+ :board-items="listItems"
:list="list"
:can-admin-list="canAdminList"
/>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 9b10e7d7db5..e9c4237d759 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -3,6 +3,7 @@ import { GlAlert } from '@gitlab/ui';
import { sortBy } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
+import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
import defaultSortableConfig from '~/sortable/sortable_config';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -11,7 +12,11 @@ import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
components: {
- BoardColumn: gon.features?.graphqlBoardLists ? BoardColumn : BoardColumnDeprecated,
+ BoardAddNewColumn,
+ BoardColumn:
+ gon.features?.graphqlBoardLists || gon.features?.epicBoards
+ ? BoardColumn
+ : BoardColumnDeprecated,
BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'),
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
@@ -33,15 +38,18 @@ export default {
},
},
computed: {
- ...mapState(['boardLists', 'error']),
- ...mapGetters(['isSwimlanesOn']),
+ ...mapState(['boardLists', 'error', 'addColumnForm']),
+ ...mapGetters(['isSwimlanesOn', 'isEpicBoard']),
+ addColumnFormVisible() {
+ return this.addColumnForm?.visible;
+ },
boardListsToUse() {
- return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn
+ return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard
? sortBy([...Object.values(this.boardLists)], 'position')
: this.lists;
},
canDragColumns() {
- return this.glFeatures.graphqlBoardLists && this.canAdminList;
+ return !this.isEpicBoard && this.glFeatures.graphqlBoardLists && this.canAdminList;
},
boardColumnWrapper() {
return this.canDragColumns ? Draggable : 'div';
@@ -62,12 +70,17 @@ export default {
},
methods: {
...mapActions(['moveList']),
+ afterFormEnters() {
+ const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
+ el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
+ },
handleDragOnStart() {
sortableStart();
},
handleDragOnEnd(params) {
sortableEnd();
+ if (this.isEpicBoard) return;
const { item, newIndex, oldIndex, to } = params;
@@ -100,13 +113,17 @@ export default {
@end="handleDragOnEnd"
>
<board-column
- v-for="list in boardListsToUse"
- :key="list.id"
+ v-for="(list, index) in boardListsToUse"
+ :key="index"
ref="board"
:can-admin-list="canAdminList"
:list="list"
:disabled="disabled"
/>
+
+ <transition name="slide" @after-enter="afterFormEnters">
+ <board-add-new-column v-if="addColumnFormVisible" />
+ </transition>
</component>
<epics-swimlanes
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index f65f00bcccc..d8504dcfb0f 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,5 +1,6 @@
<script>
import { GlModal } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName } from '~/lib/utils/common_utils';
@@ -106,6 +107,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['isEpicBoard', 'isGroupBoard', 'isProjectBoard']),
isNewForm() {
return this.currentPage === formType.new;
},
@@ -161,42 +163,49 @@ export default {
currentMutation() {
return this.board.id ? updateBoardMutation : createBoardMutation;
},
- mutationVariables() {
+ baseMutationVariables() {
const { board } = this;
- /* eslint-disable @gitlab/require-i18n-strings */
- let baseMutationVariables = {
+ const variables = {
name: board.name,
hideBacklogList: board.hide_backlog_list,
hideClosedList: board.hide_closed_list,
};
- if (this.scopedIssueBoardFeatureEnabled) {
- baseMutationVariables = {
- ...baseMutationVariables,
- weight: board.weight,
- assigneeId: board.assignee?.id ? convertToGraphQLId('User', board.assignee.id) : null,
- milestoneId:
- board.milestone?.id || board.milestone?.id === 0
- ? convertToGraphQLId('Milestone', board.milestone.id)
- : null,
- labelIds: board.labels.map(fullLabelId),
- iterationId: board.iteration_id
- ? convertToGraphQLId('Iteration', board.iteration_id)
- : null,
- };
- }
- /* eslint-enable @gitlab/require-i18n-strings */
return board.id
? {
- ...baseMutationVariables,
+ ...variables,
id: fullBoardId(board.id),
}
: {
- ...baseMutationVariables,
- projectPath: this.projectId ? this.fullPath : null,
- groupPath: this.groupId ? this.fullPath : null,
+ ...variables,
+ projectPath: this.isProjectBoard ? this.fullPath : undefined,
+ groupPath: this.isGroupBoard ? this.fullPath : undefined,
};
},
+ boardScopeMutationVariables() {
+ /* eslint-disable @gitlab/require-i18n-strings */
+ return {
+ weight: this.board.weight,
+ assigneeId: this.board.assignee?.id
+ ? convertToGraphQLId('User', this.board.assignee.id)
+ : null,
+ milestoneId:
+ this.board.milestone?.id || this.board.milestone?.id === 0
+ ? convertToGraphQLId('Milestone', this.board.milestone.id)
+ : null,
+ labelIds: this.board.labels.map(fullLabelId),
+ iterationId: this.board.iteration_id
+ ? convertToGraphQLId('Iteration', this.board.iteration_id)
+ : null,
+ };
+ /* eslint-enable @gitlab/require-i18n-strings */
+ },
+ mutationVariables() {
+ return {
+ ...this.baseMutationVariables,
+ ...(this.scopedIssueBoardFeatureEnabled ? this.boardScopeMutationVariables : {}),
+ };
+ },
},
mounted() {
this.resetFormState();
@@ -208,6 +217,16 @@ export default {
setIteration(iterationId) {
this.board.iteration_id = iterationId;
},
+ boardCreateResponse(data) {
+ return data.createBoard.board.webPath;
+ },
+ boardUpdateResponse(data) {
+ const path = data.updateBoard.board.webPath;
+ const param = getParameterByName('group_by')
+ ? `?group_by=${getParameterByName('group_by')}`
+ : '';
+ return `${path}${param}`;
+ },
async createOrUpdateBoard() {
const response = await this.$apollo.mutate({
mutation: this.currentMutation,
@@ -215,14 +234,10 @@ export default {
});
if (!this.board.id) {
- return response.data.createBoard.board.webPath;
+ return this.boardCreateResponse(response.data);
}
- const path = response.data.updateBoard.board.webPath;
- const param = getParameterByName('group_by')
- ? `?group_by=${getParameterByName('group_by')}`
- : '';
- return `${path}${param}`;
+ return this.boardUpdateResponse(response.data);
},
async submit() {
if (this.board.name.length === 0) return;
@@ -309,7 +324,7 @@ export default {
/>
<board-scope
- v-if="scopedIssueBoardFeatureEnabled"
+ v-if="scopedIssueBoardFeatureEnabled && !isEpicBoard"
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 7495b1163be..ae8434be312 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import Draggable from 'vuedraggable';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
@@ -12,9 +12,10 @@ import BoardNewIssue from './board_new_issue.vue';
export default {
name: 'BoardList',
i18n: {
- loadingIssues: __('Loading issues'),
- loadingMoreissues: __('Loading more issues'),
+ loading: __('Loading'),
+ loadingMoreboardItems: __('Loading more'),
showingAllIssues: __('Showing all issues'),
+ showingAllEpics: __('Showing all epics'),
},
components: {
BoardCard,
@@ -30,7 +31,7 @@ export default {
type: Object,
required: true,
},
- issues: {
+ boardItems: {
type: Array,
required: true,
},
@@ -49,14 +50,19 @@ export default {
},
computed: {
...mapState(['pageInfoByListId', 'listsFlags']),
+ ...mapGetters(['isEpicBoard']),
+ listItemsCount() {
+ return this.isEpicBoard ? this.list.epicsCount : this.list.issuesCount;
+ },
paginatedIssueText() {
- return sprintf(__('Showing %{pageSize} of %{total} issues'), {
- pageSize: this.issues.length,
- total: this.list.issuesCount,
+ return sprintf(__('Showing %{pageSize} of %{total} %{issuableType}'), {
+ pageSize: this.boardItems.length,
+ total: this.listItemsCount,
+ issuableType: this.isEpicBoard ? 'epics' : 'issues',
});
},
- issuesSizeExceedsMax() {
- return this.list.maxIssueCount > 0 && this.list.issuesCount > this.list.maxIssueCount;
+ boardItemsSizeExceedsMax() {
+ return this.list.maxIssueCount > 0 && this.listItemsCount > this.list.maxIssueCount;
},
hasNextPage() {
return this.pageInfoByListId[this.list.id].hasNextPage;
@@ -71,8 +77,13 @@ export default {
// When list is draggable, the reference to the list needs to be accessed differently
return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
},
- showingAllIssues() {
- return this.issues.length === this.list.issuesCount;
+ showingAllItems() {
+ return this.boardItems.length === this.listItemsCount;
+ },
+ showingAllItemsText() {
+ return this.isEpicBoard
+ ? this.$options.i18n.showingAllEpics
+ : this.$options.i18n.showingAllIssues;
},
treeRootWrapper() {
return this.canAdminList ? Draggable : 'ul';
@@ -85,14 +96,14 @@ export default {
tag: 'ul',
'ghost-class': 'board-card-drag-active',
'data-list-id': this.list.id,
- value: this.issues,
+ value: this.boardItems,
};
return this.canAdminList ? options : {};
},
},
watch: {
- issues() {
+ boardItems() {
this.$nextTick(() => {
this.showCount = this.scrollHeight() > Math.ceil(this.listHeight());
});
@@ -112,7 +123,7 @@ export default {
this.listRef.removeEventListener('scroll', this.onScroll);
},
methods: {
- ...mapActions(['fetchIssuesForList', 'moveIssue']),
+ ...mapActions(['fetchItemsForList', 'moveItem']),
listHeight() {
return this.listRef.getBoundingClientRect().height;
},
@@ -126,7 +137,7 @@ export default {
this.listRef.scrollTop = 0;
},
loadNextPage() {
- this.fetchIssuesForList({ listId: this.list.id, fetchNext: true });
+ this.fetchItemsForList({ listId: this.list.id, fetchNext: true });
},
toggleForm() {
this.showIssueForm = !this.showIssueForm;
@@ -148,40 +159,40 @@ export default {
handleDragOnEnd(params) {
sortableEnd();
const { newIndex, oldIndex, from, to, item } = params;
- const { issueId, issueIid, issuePath } = item.dataset;
+ const { itemId, itemIid, itemPath } = item.dataset;
const { children } = to;
let moveBeforeId;
let moveAfterId;
- const getIssueId = (el) => Number(el.dataset.issueId);
+ const getItemId = (el) => Number(el.dataset.itemId);
- // If issue is being moved within the same list
+ // If item is being moved within the same list
if (from === to) {
if (newIndex > oldIndex && children.length > 1) {
- // If issue is being moved down we look for the issue that ends up before
- moveBeforeId = getIssueId(children[newIndex]);
+ // If item is being moved down we look for the item that ends up before
+ moveBeforeId = getItemId(children[newIndex]);
} else if (newIndex < oldIndex && children.length > 1) {
- // If issue is being moved up we look for the issue that ends up after
- moveAfterId = getIssueId(children[newIndex]);
+ // If item is being moved up we look for the item that ends up after
+ moveAfterId = getItemId(children[newIndex]);
} else {
- // If issue remains in the same list at the same position we do nothing
+ // If item remains in the same list at the same position we do nothing
return;
}
} else {
- // We look for the issue that ends up before the moved issue if it exists
+ // We look for the item that ends up before the moved item if it exists
if (children[newIndex - 1]) {
- moveBeforeId = getIssueId(children[newIndex - 1]);
+ moveBeforeId = getItemId(children[newIndex - 1]);
}
- // We look for the issue that ends up after the moved issue if it exists
+ // We look for the item that ends up after the moved item if it exists
if (children[newIndex]) {
- moveAfterId = getIssueId(children[newIndex]);
+ moveAfterId = getItemId(children[newIndex]);
}
}
- this.moveIssue({
- issueId,
- issueIid,
- issuePath,
+ this.moveItem({
+ itemId,
+ itemIid,
+ itemPath,
fromListId: from.dataset.listId,
toListId: to.dataset.listId,
moveBeforeId,
@@ -201,7 +212,7 @@ export default {
<div
v-if="loading"
class="gl-mt-4 gl-text-center"
- :aria-label="$options.i18n.loadingIssues"
+ :aria-label="$options.i18n.loading"
data-testid="board_list_loading"
>
<gl-loading-icon />
@@ -214,24 +225,28 @@ export default {
v-bind="treeRootOptions"
:data-board="list.id"
:data-board-type="list.listType"
- :class="{ 'bg-danger-100': issuesSizeExceedsMax }"
+ :class="{ 'bg-danger-100': boardItemsSizeExceedsMax }"
class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list"
data-testid="tree-root-wrapper"
@start="handleDragOnStart"
@end="handleDragOnEnd"
>
<board-card
- v-for="(issue, index) in issues"
+ v-for="(item, index) in boardItems"
ref="issue"
- :key="issue.id"
+ :key="item.id"
:index="index"
:list="list"
- :issue="issue"
+ :item="item"
:disabled="disabled"
/>
<li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
- <gl-loading-icon v-if="loadingMore" :label="$options.i18n.loadingMoreissues" />
- <span v-if="showingAllIssues">{{ $options.i18n.showingAllIssues }}</span>
+ <gl-loading-icon
+ v-if="loadingMore"
+ :label="$options.i18n.loadingMoreboardItems"
+ data-testid="count-loading-icon"
+ />
+ <span v-if="showingAllItems">{{ showingAllItemsText }}</span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
</component>
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index 9b4961d362d..d59fbcc1b31 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -11,7 +11,7 @@ import {
sortableEnd,
} from '../mixins/sortable_default_options';
import boardsStore from '../stores/boards_store';
-import boardCard from './board_card.vue';
+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
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index a933370427c..6ccaec4a633 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -8,16 +8,16 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { isListDraggable } from '~/boards/boards_util';
-import { isScopedLabel } from '~/lib/utils/common_utils';
+import { isScopedLabel, parseBoolean } 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 IssueCount from './issue_count.vue';
+import ItemCount from './item_count.vue';
export default {
i18n: {
@@ -33,7 +33,7 @@ export default {
GlTooltip,
GlIcon,
GlSprintf,
- IssueCount,
+ ItemCount,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -70,6 +70,7 @@ export default {
},
computed: {
...mapState(['activeId']),
+ ...mapGetters(['isEpicBoard']),
isLoggedIn() {
return Boolean(this.currentUserId);
},
@@ -97,11 +98,14 @@ export default {
showListDetails() {
return !this.list.collapsed || !this.isSwimlanesHeader;
},
- issuesCount() {
+ itemsCount() {
return this.list.issuesCount;
},
- issuesTooltipLabel() {
- return n__(`%d issue`, `%d issues`, this.issuesCount);
+ countIcon() {
+ return 'issues';
+ },
+ itemsTooltipLabel() {
+ return n__(`%d issue`, `%d issues`, this.itemsCount);
},
chevronTooltip() {
return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse;
@@ -110,7 +114,7 @@ export default {
return this.list.collapsed ? 'chevron-down' : 'chevron-right';
},
isNewIssueShown() {
- return this.listType === ListType.backlog || this.showListHeaderButton;
+ return (this.listType === ListType.backlog || this.showListHeaderButton) && !this.isEpicBoard;
},
isSettingsShown() {
return (
@@ -131,8 +135,14 @@ export default {
return !this.disabled && isListDraggable(this.list);
},
},
+ created() {
+ const localCollapsed = parseBoolean(localStorage.getItem(`${this.uniqueKey}.collapsed`));
+ if ((!this.isLoggedIn || this.isEpicBoard) && localCollapsed) {
+ this.toggleListCollapsed({ listId: this.list.id, collapsed: true });
+ }
+ },
methods: {
- ...mapActions(['updateList', 'setActiveId']),
+ ...mapActions(['updateList', 'setActiveId', 'toggleListCollapsed']),
openSidebarSettings() {
if (this.activeId === inactiveId) {
sidebarEventHub.$emit('sidebar.closeAll');
@@ -148,10 +158,10 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
- // eslint-disable-next-line vue/no-mutating-props
- this.list.collapsed = !this.list.collapsed;
+ const collapsed = !this.list.collapsed;
+ this.toggleListCollapsed({ listId: this.list.id, collapsed });
- if (!this.isLoggedIn) {
+ if (!this.isLoggedIn || this.isEpicBoard) {
this.addToLocalStorage();
} else {
this.updateListFunction();
@@ -163,7 +173,7 @@ export default {
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(`${this.uniqueKey}.expanded`, !this.list.collapsed);
+ localStorage.setItem(`${this.uniqueKey}.collapsed`, this.list.collapsed);
}
},
updateListFunction() {
@@ -203,6 +213,7 @@ export default {
class="board-title-caret no-drag gl-cursor-pointer"
category="tertiary"
size="small"
+ data-testid="board-title-caret"
@click="toggleExpanded"
/>
<!-- EE start -->
@@ -301,11 +312,11 @@ export default {
<div v-if="list.maxIssueCount !== 0">
•
<gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')">
- <template #issuesSize>{{ issuesTooltipLabel }}</template>
+ <template #issuesSize>{{ itemsTooltipLabel }}</template>
<template #maxIssueCount>{{ list.maxIssueCount }}</template>
</gl-sprintf>
</div>
- <div v-else>• {{ issuesTooltipLabel }}</div>
+ <div v-else>• {{ itemsTooltipLabel }}</div>
<div v-if="weightFeatureAvailable">
•
<gl-sprintf :message="__('%{totalWeight} total weight')">
@@ -323,13 +334,13 @@ export default {
}"
>
<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 :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" />
+ <gl-tooltip :target="() => $refs.itemCount" :title="itemsTooltipLabel" />
+ <span ref="itemCount" class="issue-count-badge-count">
+ <gl-icon class="gl-mr-2" :name="countIcon" />
+ <item-count :items-size="itemsCount" :max-issue-count="list.maxIssueCount" />
</span>
<!-- EE start -->
- <template v-if="weightFeatureAvailable">
+ <template v-if="weightFeatureAvailable && !isEpicBoard">
<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" />
diff --git a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
index ff043d3aa01..429ffd4cd06 100644
--- a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
@@ -17,7 +17,7 @@ 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 './issue_count.vue';
+import IssueCount from './item_count.vue';
// This component is being replaced in favor of './board_list_header.vue' for GraphQL boards
@@ -308,7 +308,7 @@ export default {
<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 :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" />
+ <issue-count :items-size="issuesCount" :max-issue-count="list.maxIssueCount" />
</span>
<!-- The following is only true in EE. -->
<template v-if="weightFeatureAvailable">
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 1df154688c8..a81c28733cd 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -32,8 +32,9 @@ export default {
},
computed: {
...mapState(['selectedProject']),
+ ...mapGetters(['isGroupBoard']),
disabled() {
- if (this.groupId) {
+ if (this.isGroupBoard) {
return this.title === '' || !this.selectedProject.name;
}
return this.title === '';
@@ -98,7 +99,7 @@ export default {
name="issue_title"
autocomplete="off"
/>
- <project-select v-if="groupId" :group-id="groupId" :list="list" />
+ <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3">
<gl-button
ref="submitButton"
diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
index eff87ff110e..16f23dfff0e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
@@ -1,5 +1,6 @@
<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';
@@ -31,8 +32,9 @@ export default {
};
},
computed: {
+ ...mapGetters(['isGroupBoard']),
disabled() {
- if (this.groupId) {
+ if (this.isGroupBoard) {
return this.title === '' || !this.selectedProject.name;
}
return this.title === '';
@@ -110,7 +112,7 @@ export default {
name="issue_title"
autocomplete="off"
/>
- <project-select v-if="groupId" :group-id="groupId" :list="list" />
+ <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3">
<gl-button
ref="submitButton"
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 6d5a13be3ac..55bc91cbcff 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -20,7 +20,6 @@ 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';
-import RemoveBtn from './sidebar/remove_issue.vue';
export default Vue.extend({
components: {
@@ -29,7 +28,6 @@ export default Vue.extend({
GlLabel,
SidebarEpicsSelect: () =>
import('ee_component/sidebar/components/sidebar_item_epics_select.vue'),
- RemoveBtn,
Subscriptions,
TimeTracker,
SidebarAssigneesWidget,
@@ -107,8 +105,8 @@ export default Vue.extend({
closeSidebar() {
this.detail.issue = {};
},
- setAssignees(data) {
- boardsStore.detail.issue.setAssignees(data.issueSetAssignees.issue.assignees.nodes);
+ 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.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 2a064aaa885..5124467136e 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -9,6 +9,9 @@ import {
GlModalDirective,
} from '@gitlab/ui';
import { throttle } from 'lodash';
+import { mapGetters, mapState } from 'vuex';
+
+import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
@@ -18,8 +21,6 @@ import eventHub from '../eventhub';
import groupQuery from '../graphql/group_boards.query.graphql';
import projectQuery from '../graphql/project_boards.query.graphql';
-import BoardForm from './board_form.vue';
-
const MIN_BOARDS_TO_VIEW_RECENT = 10;
export default {
@@ -109,8 +110,10 @@ export default {
};
},
computed: {
+ ...mapState(['boardType']),
+ ...mapGetters(['isGroupBoard']),
parentType() {
- return this.groupId ? 'group' : 'project';
+ return this.boardType;
},
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
@@ -123,6 +126,9 @@ export default {
board() {
return this.currentBoard;
},
+ showCreate() {
+ return this.multipleIssueBoardsAvailable;
+ },
showDelete() {
return this.boards.length > 1;
},
@@ -158,6 +164,18 @@ export default {
cancel() {
this.showPage('');
},
+ boardUpdate(data) {
+ if (!data?.[this.parentType]) {
+ return [];
+ }
+ return data[this.parentType].boards.edges.map(({ node }) => ({
+ id: getIdFromGraphQLId(node.id),
+ name: node.name,
+ }));
+ },
+ boardQuery() {
+ return this.isGroupBoard ? groupQuery : projectQuery;
+ },
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
return;
@@ -167,21 +185,14 @@ export default {
variables() {
return { fullPath: this.fullPath };
},
- query() {
- return this.groupId ? groupQuery : projectQuery;
- },
+ query: this.boardQuery,
loadingKey: 'loadingBoards',
- update(data) {
- if (!data?.[this.parentType]) {
- return [];
- }
- return data[this.parentType].boards.edges.map(({ node }) => ({
- id: getIdFromGraphQLId(node.id),
- name: node.name,
- }));
- },
+ update: this.boardUpdate,
});
+ this.loadRecentBoards();
+ },
+ loadRecentBoards() {
this.loadingRecentBoards = true;
// Follow up to fetch recent boards using GraphQL
// https://gitlab.com/gitlab-org/gitlab/-/issues/300985
@@ -322,7 +333,7 @@ export default {
<gl-dropdown-divider />
<gl-dropdown-item
- v-if="multipleIssueBoardsAvailable"
+ v-if="showCreate"
v-gl-modal-directive="'board-config-modal'"
data-qa-selector="create_new_board_button"
@click.prevent="showPage('new')"
diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
index 33ad46a0d29..85c7b27336b 100644
--- a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
+++ b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
@@ -9,6 +9,7 @@ import {
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';
@@ -108,8 +109,10 @@ export default {
};
},
computed: {
+ ...mapState(['boardType']),
+ ...mapGetters(['isGroupBoard']),
parentType() {
- return this.groupId ? 'group' : 'project';
+ return this.boardType;
},
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
@@ -167,7 +170,7 @@ export default {
return { fullPath: this.state.endpoints.fullPath };
},
query() {
- return this.groupId ? groupQuery : projectQuery;
+ return this.isGroupBoard ? groupQuery : projectQuery;
},
loadingKey: 'loadingBoards',
update(data) {
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
new file mode 100644
index 00000000000..7ec99e51f5b
--- /dev/null
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -0,0 +1,64 @@
+<script>
+import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { formType } from '~/boards/constants';
+import eventHub from '~/boards/eventhub';
+import { s__, __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlModalDirective,
+ },
+ props: {
+ boardsStore: {
+ type: Object,
+ required: true,
+ },
+ canAdminList: {
+ type: Boolean,
+ required: true,
+ },
+ hasScope: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ state: this.boardsStore.state,
+ };
+ },
+ computed: {
+ buttonText() {
+ return this.canAdminList ? s__('Boards|Edit board') : s__('Boards|View scope');
+ },
+ tooltipTitle() {
+ return this.hasScope ? __("This board's scope is reduced") : '';
+ },
+ },
+ methods: {
+ showPage() {
+ eventHub.$emit('showBoardModal', formType.edit);
+ return this.boardsStore.showPage(formType.edit);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-ml-3 gl-display-flex gl-align-items-center">
+ <gl-button
+ v-gl-modal-directive="'board-config-modal'"
+ v-gl-tooltip
+ :title="tooltipTitle"
+ :class="{ 'dot-highlight': hasScope }"
+ data-qa-selector="boards_config_button"
+ @click.prevent="showPage"
+ >
+ {{ buttonText }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/filtered_search.vue b/app/assets/javascripts/boards/components/filtered_search.vue
new file mode 100644
index 00000000000..8505ea39a6b
--- /dev/null
+++ b/app/assets/javascripts/boards/components/filtered_search.vue
@@ -0,0 +1,54 @@
+<script>
+import { mapActions } from 'vuex';
+import { historyPushState } from '~/lib/utils/common_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+export default {
+ i18n: {
+ search: __('Search'),
+ },
+ components: { FilteredSearch },
+ props: {
+ search: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ initialSearch() {
+ return [{ type: 'filtered-search-term', value: { data: this.search } }];
+ },
+ },
+ methods: {
+ ...mapActions(['performSearch']),
+ handleSearch(filters) {
+ let itemValue = '';
+ const [item] = filters;
+
+ if (filters.length === 0) {
+ itemValue = '';
+ } else {
+ itemValue = item?.value?.data;
+ }
+
+ historyPushState(setUrlParams({ search: itemValue }, window.location.href));
+
+ this.performSearch();
+ },
+ },
+};
+</script>
+
+<template>
+ <filtered-search
+ class="gl-w-full"
+ namespace=""
+ :tokens="[]"
+ :search-input-placeholder="$options.i18n.search"
+ :initial-filter-value="initialSearch"
+ @onFilter="handleSearch"
+ />
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
index 069cc2cda22..2652fac1818 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
@@ -2,7 +2,7 @@
import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapState } from 'vuex';
-import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
+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';
@@ -24,7 +24,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [issueCardInner],
+ mixins: [boardCardInner],
inject: ['groupId', 'rootPath'],
props: {
issue: {
@@ -207,7 +207,7 @@ export default {
/>
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
- v-if="validIssueWeight"
+ v-if="validIssueWeight(issue)"
:weight="issue.weight"
@click="filterByWeight(issue.weight)"
/>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 7e3f36c8a17..73ec008c2b6 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -86,7 +86,11 @@ export default {
<template>
<span>
<span ref="issueDueDate" :class="cssClass" class="board-card-info card-number">
- <gl-icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" />
+ <gl-icon
+ :class="{ 'text-danger': isPastDue }"
+ class="board-card-info-icon gl-mr-2"
+ name="calendar"
+ />
<time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{
body
}}</time>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index 42d187b9b40..1ab7deebfaf 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -37,7 +37,7 @@ export default {
<template>
<span>
<span ref="issueTimeEstimate" class="board-card-info card-number">
- <gl-icon name="hourglass" class="board-card-info-icon" />
+ <gl-icon name="hourglass" class="board-card-info-icon gl-mr-2" />
<time class="board-card-info-text">{{ timeEstimate }}</time>
</span>
<gl-tooltip
diff --git a/app/assets/javascripts/boards/components/issue_count.vue b/app/assets/javascripts/boards/components/item_count.vue
index d55f7151d7e..9b1ff254766 100644
--- a/app/assets/javascripts/boards/components/issue_count.vue
+++ b/app/assets/javascripts/boards/components/item_count.vue
@@ -7,7 +7,7 @@ export default {
required: false,
default: 0,
},
- issuesSize: {
+ itemsSize: {
type: Number,
required: false,
default: 0,
@@ -18,16 +18,16 @@ export default {
return this.maxIssueCount !== 0;
},
issuesExceedMax() {
- return this.isMaxLimitSet && this.issuesSize > this.maxIssueCount;
+ return this.isMaxLimitSet && this.itemsSize > this.maxIssueCount;
},
},
};
</script>
<template>
- <div class="issue-count text-nowrap">
- <span class="js-issue-size" :class="{ 'text-danger': issuesExceedMax }">
- {{ issuesSize }}
+ <div class="item-count text-nowrap">
+ <span :class="{ 'text-danger': issuesExceedMax }" data-testid="board-items-count">
+ {{ itemsSize }}
</span>
<span v-if="isMaxLimitSet" class="js-max-issue-size">
{{ maxIssueCount }}
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index bf69f8140d5..e66cae0ce18 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -2,11 +2,11 @@
import { GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import ModalStore from '../../stores/modal_store';
-import IssueCardInner from '../issue_card_inner.vue';
+import BoardCardInner from '../board_card_inner.vue';
export default {
components: {
- IssueCardInner,
+ BoardCardInner,
GlIcon,
},
props: {
@@ -126,7 +126,7 @@ export default {
class="board-card position-relative p-3 rounded"
@click="toggleIssue($event, issue)"
>
- <issue-card-inner :issue="issue" />
+ <board-card-inner :item="issue" />
<gl-icon
v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'"
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index cfc1752a828..77b6af77652 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -7,7 +7,7 @@ import {
GlIntersectionObserver,
GlLoadingIcon,
} from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import { ListType } from '../constants';
@@ -49,7 +49,8 @@ export default {
};
},
computed: {
- ...mapState(['groupProjects', 'groupProjectsFlags']),
+ ...mapState(['groupProjectsFlags']),
+ ...mapGetters(['activeGroupProjects']),
selectedProjectName() {
return this.selectedProject.name || this.$options.i18n.dropdownText;
},
@@ -65,7 +66,7 @@ export default {
};
},
isFetchResultEmpty() {
- return this.groupProjects.length === 0;
+ return this.activeGroupProjects.length === 0;
},
hasNextPage() {
return this.groupProjectsFlags.pageInfo?.hasNextPage;
@@ -84,7 +85,7 @@ export default {
methods: {
...mapActions(['fetchGroupProjects', 'setSelectedProject']),
selectProject(projectId) {
- this.selectedProject = this.groupProjects.find((project) => project.id === projectId);
+ this.selectedProject = this.activeGroupProjects.find((project) => project.id === projectId);
this.setSelectedProject(this.selectedProject);
},
loadMoreProjects() {
@@ -113,7 +114,7 @@ export default {
:placeholder="$options.i18n.searchPlaceholder"
/>
<gl-dropdown-item
- v-for="project in groupProjects"
+ v-for="project in activeGroupProjects"
v-show="!groupProjectsFlags.isLoading"
:key="project.id"
:name="project.name"
diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue
index 5605e9945ea..afe161d9c54 100644
--- a/app/assets/javascripts/boards/components/project_select_deprecated.vue
+++ b/app/assets/javascripts/boards/components/project_select_deprecated.vue
@@ -25,6 +25,7 @@ export default {
with_shared: false,
include_subgroups: true,
order_by: 'similarity',
+ archived: false,
},
components: {
GlLoadingIcon,
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
index aa4fdcf9a94..f01c8e8fa20 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
@@ -64,6 +64,8 @@ export default {
v-if="!activeIssue.emailsDisabled"
:value="activeIssue.subscribed"
:is-loading="loading"
+ :label="$options.i18n.header.title"
+ label-position="hidden"
data-testid="notification-subscribe-toggle"
@change="handleToggleSubscription"
/>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
deleted file mode 100644
index 8d65f3240c8..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as Flash } from '../../../flash';
-import { __ } from '../../../locale';
-import boardsStore from '../../stores/boards_store';
-
-export default {
- components: {
- GlButton,
- },
- props: {
- issue: {
- type: Object,
- required: true,
- },
- list: {
- type: Object,
- required: true,
- },
- },
- computed: {
- updateUrl() {
- return this.issue.path;
- },
- },
- methods: {
- removeIssue() {
- const { issue } = this;
- const lists = issue.getLists();
- const req = this.buildPatchRequest(issue, lists);
-
- const data = {
- issue: this.seedPatchRequest(issue, req),
- };
-
- if (data.issue.label_ids.length === 0) {
- data.issue.label_ids = [''];
- }
-
- // Post the remove data
- axios.patch(this.updateUrl, data).catch(() => {
- Flash(__('Failed to remove issue from board, please try again.'));
-
- lists.forEach((list) => {
- list.addIssue(issue);
- });
- });
-
- // Remove from the frontend store
- lists.forEach((list) => {
- list.removeIssue(issue);
- });
-
- boardsStore.clearDetailIssue();
- },
- /**
- * Build the default patch request.
- */
- buildPatchRequest(issue, lists) {
- const listLabelIds = lists.map((list) => list.label.id);
-
- const labelIds = issue.labels
- .map((label) => label.id)
- .filter((id) => !listLabelIds.includes(id));
-
- return {
- label_ids: labelIds,
- };
- },
- /**
- * Seed the given patch request.
- *
- * (This is overridden in EE)
- */
- seedPatchRequest(issue, req) {
- return req;
- },
- },
-};
-</script>
-<template>
- <div class="block list">
- <gl-button variant="default" category="secondary" block="block" @click="removeIssue">
- {{ __('Remove from board') }}
- </gl-button>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js
index 2d1ec238274..7f327c5764d 100644
--- a/app/assets/javascripts/boards/config_toggle.js
+++ b/app/assets/javascripts/boards/config_toggle.js
@@ -1 +1,24 @@
-export default () => {};
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import ConfigToggle from './components/config_toggle.vue';
+
+export default (boardsStore) => {
+ const el = document.querySelector('.js-board-config');
+
+ if (!el) {
+ return;
+ }
+
+ gl.boardConfigToggle = new Vue({
+ el,
+ render(h) {
+ return h(ConfigToggle, {
+ props: {
+ boardsStore,
+ canAdminList: parseBoolean(el.dataset.canAdminList),
+ hasScope: parseBoolean(el.dataset.hasScope),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 3ab89b2c9da..65ebfe7be6c 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,5 +1,10 @@
import { __ } from '~/locale';
+export const issuableTypes = {
+ issue: 'issue',
+ epic: 'epic',
+};
+
export const BoardType = {
project: 'project',
group: 'group',
diff --git a/app/assets/javascripts/boards/filtered_search.js b/app/assets/javascripts/boards/filtered_search.js
new file mode 100644
index 00000000000..182a2cf3724
--- /dev/null
+++ b/app/assets/javascripts/boards/filtered_search.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import store from '~/boards/stores';
+import { queryToObject } from '~/lib/utils/url_utility';
+import FilteredSearch from './components/filtered_search.vue';
+
+export default () => {
+ const queryParams = queryToObject(window.location.search);
+ const el = document.getElementById('js-board-filtered-search');
+
+ /*
+ When https://github.com/vuejs/vue-apollo/pull/1153 is merged and deployed
+ we can remove apolloProvider option from here. Currently without it its causing
+ an error
+ */
+
+ return new Vue({
+ el,
+ store,
+ apolloProvider: {},
+ render: (createElement) =>
+ createElement(FilteredSearch, {
+ props: { search: queryParams.search },
+ }),
+ });
+};
diff --git a/app/assets/javascripts/boards/graphql/board_labels.query.graphql b/app/assets/javascripts/boards/graphql/board_labels.query.graphql
index 42a94419a97..b19a24e8808 100644
--- a/app/assets/javascripts/boards/graphql/board_labels.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_labels.query.graphql
@@ -7,14 +7,14 @@ query BoardLabels(
$isProject: Boolean = false
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
- labels(searchTerm: $searchTerm) {
+ labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) {
nodes {
...Label
}
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
- labels(searchTerm: $searchTerm) {
+ labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes {
...Label
}
diff --git a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
index f78a21baa7f..3eb23f62940 100644
--- a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
@@ -1,21 +1,7 @@
-#import "ee_else_ce/boards/graphql/board_list.fragment.graphql"
+#import "./board_list.fragment.graphql"
-mutation CreateBoardList(
- $boardId: BoardID!
- $backlog: Boolean
- $labelId: LabelID
- $milestoneId: MilestoneID
- $assigneeId: UserID
-) {
- boardListCreate(
- input: {
- boardId: $boardId
- backlog: $backlog
- labelId: $labelId
- milestoneId: $milestoneId
- assigneeId: $assigneeId
- }
- ) {
+mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean, $labelId: LabelID) {
+ boardListCreate(input: { boardId: $boardId, backlog: $backlog, labelId: $labelId }) {
list {
...BoardListFragment
}
diff --git a/app/assets/javascripts/boards/graphql/group_projects.query.graphql b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
index 1afa6e48547..80a37c9943d 100644
--- a/app/assets/javascripts/boards/graphql/group_projects.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
@@ -8,6 +8,7 @@ query getGroupProjects($fullPath: ID!, $search: String, $after: String) {
name
fullPath
nameWithNamespace
+ archived
}
pageInfo {
...PageInfo
diff --git a/app/assets/javascripts/boards/graphql/users_search.query.graphql b/app/assets/javascripts/boards/graphql/users_search.query.graphql
deleted file mode 100644
index ca016495d79..00000000000
--- a/app/assets/javascripts/boards/graphql/users_search.query.graphql
+++ /dev/null
@@ -1,11 +0,0 @@
-query usersSearch($search: String!) {
- users(search: $search) {
- nodes {
- username
- name
- webUrl
- avatarUrl
- id
- }
- }
-}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 859295318ed..ceca5b0a451 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -6,7 +6,6 @@ import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
-import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
import {
setWeightFetchingState,
setEpicFetchingState,
@@ -24,6 +23,7 @@ import '~/boards/models/milestone';
import '~/boards/models/project';
import '~/boards/filters/due_date_filters';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
+import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import modalMixin from '~/boards/mixins/modal_mixins';
@@ -40,6 +40,7 @@ import {
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
+import boardConfigToggle from './config_toggle';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo);
@@ -52,7 +53,6 @@ let issueBoardsApp;
export default () => {
const $boardApp = document.getElementById('board-app');
-
// check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => {
const isNavTypeBackForward =
@@ -72,6 +72,14 @@ export default () => {
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
}
+ if (gon?.features?.boardsFilteredSearch) {
+ import('~/boards/filtered_search')
+ .then(({ default: initFilteredSearch }) => {
+ initFilteredSearch(apolloProvider);
+ })
+ .catch(() => {});
+ }
+
// eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
el: $boardApp,
@@ -96,6 +104,9 @@ export default () => {
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels),
+ milestoneListsAvailable: parseBoolean($boardApp.dataset.milestoneListsAvailable),
+ assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable),
+ iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable),
},
store,
apolloProvider,
@@ -124,6 +135,7 @@ export default () => {
fullPath: $boardApp.dataset.fullPath,
boardType: this.parent,
disabled: this.disabled,
+ issuableType: issuableTypes.issue,
boardConfig: {
milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
@@ -162,8 +174,15 @@ export default () => {
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
},
mounted() {
- this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
- this.filterManager.setup();
+ if (!gon.features?.boardsFilteredSearch) {
+ this.filterManager = new FilteredSearchBoards(
+ boardsStore.filter,
+ true,
+ boardsStore.cantEdit,
+ );
+
+ this.filterManager.setup();
+ }
this.performSearch();
@@ -349,7 +368,7 @@ export default () => {
toggleFocusMode(ModalStore, boardsStore);
toggleLabels();
- if (gon.features?.swimlanes) {
+ if (gon.licensed_features?.swimlanes) {
toggleEpicsSwimlanes();
}
diff --git a/app/assets/javascripts/boards/mixins/issue_card_inner.js b/app/assets/javascripts/boards/mixins/board_card_inner.js
index 04e971b756d..a6f278f3bc9 100644
--- a/app/assets/javascripts/boards/mixins/issue_card_inner.js
+++ b/app/assets/javascripts/boards/mixins/board_card_inner.js
@@ -1,10 +1,8 @@
export default {
- computed: {
+ methods: {
validIssueWeight() {
return false;
},
- },
- methods: {
filterByWeight() {},
},
};
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index fa58af24ba2..7f655091cd0 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapGetters } from 'vuex';
-import BoardsSelector from '~/boards/components/boards_selector.vue';
+import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
@@ -48,10 +48,10 @@ export default (params = {}) => {
return { boardsSelectorProps };
},
computed: {
- ...mapGetters(['shouldUseGraphQL']),
+ ...mapGetters(['shouldUseGraphQL', 'isEpicBoard']),
},
render(createElement) {
- if (this.shouldUseGraphQL) {
+ if (this.shouldUseGraphQL || this.isEpicBoard) {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
});
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index a7cf1e9e647..19b31ee7291 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,6 +1,13 @@
import { pick } from 'lodash';
+import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
-import { BoardType, ListType, inactiveId, flashAnimationDuration } from '~/boards/constants';
+import {
+ BoardType,
+ ListType,
+ inactiveId,
+ flashAnimationDuration,
+ ISSUABLE,
+} from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
@@ -15,7 +22,6 @@ import {
transformNotFilters,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
-import createBoardListMutation from '../graphql/board_list_create.mutation.graphql';
import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
@@ -79,7 +85,11 @@ export default {
}
},
- fetchLists: ({ commit, state, dispatch }) => {
+ fetchLists: ({ dispatch }) => {
+ dispatch('fetchIssueLists');
+ },
+
+ fetchIssueLists: ({ commit, state, dispatch }) => {
const { boardType, filterParams, fullPath, boardId } = state;
const variables = {
@@ -118,9 +128,13 @@ export default {
}, flashAnimationDuration);
},
- createList: (
+ createList: ({ dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
+ dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
+ },
+
+ createIssueList: (
{ state, commit, dispatch, getters },
- { backlog, labelId, milestoneId, assigneeId },
+ { backlog, labelId, milestoneId, assigneeId, iterationId },
) => {
const { boardId } = state;
@@ -140,25 +154,29 @@ export default {
labelId,
milestoneId,
assigneeId,
+ iterationId,
},
})
.then(({ data }) => {
- if (data?.boardListCreate?.errors.length) {
- commit(types.CREATE_LIST_FAILURE);
+ if (data.boardListCreate?.errors.length) {
+ commit(types.CREATE_LIST_FAILURE, data.boardListCreate.errors[0]);
} else {
const list = data.boardListCreate?.list;
dispatch('addList', list);
dispatch('highlightList', list.id);
}
})
- .catch(() => commit(types.CREATE_LIST_FAILURE));
+ .catch((e) => {
+ commit(types.CREATE_LIST_FAILURE);
+ throw e;
+ });
},
addList: ({ commit }, list) => {
commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list));
},
- fetchLabels: ({ state, commit }, searchTerm) => {
+ fetchLabels: ({ state, commit, getters }, searchTerm) => {
const { fullPath, boardType } = state;
const variables = {
@@ -168,15 +186,29 @@ export default {
isProject: boardType === BoardType.project,
};
+ commit(types.RECEIVE_LABELS_REQUEST);
+
return gqlClient
.query({
query: boardLabelsQuery,
variables,
})
.then(({ data }) => {
- const labels = data[boardType]?.labels.nodes;
+ let labels = data[boardType]?.labels.nodes;
+
+ if (!getters.shouldUseGraphQL && !getters.isEpicBoard) {
+ labels = labels.map((label) => ({
+ ...label,
+ id: getIdFromGraphQLId(label.id),
+ }));
+ }
+
commit(types.RECEIVE_LABELS_SUCCESS, labels);
return labels;
+ })
+ .catch((e) => {
+ commit(types.RECEIVE_LABELS_FAILURE);
+ throw e;
});
},
@@ -225,6 +257,10 @@ export default {
});
},
+ toggleListCollapsed: ({ commit }, { listId, collapsed }) => {
+ commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed });
+ },
+
removeList: ({ state, commit }, listId) => {
const listsBackup = { ...state.boardLists };
@@ -253,8 +289,8 @@ export default {
});
},
- fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false }) => {
- commit(types.REQUEST_ISSUES_FOR_LIST, { listId, fetchNext });
+ fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
+ commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
const { fullPath, boardId, boardType, filterParams } = state;
@@ -279,28 +315,32 @@ export default {
})
.then(({ data }) => {
const { lists } = data[boardType]?.board;
- const listIssues = formatListIssues(lists);
+ const listItems = formatListIssues(lists);
const listPageInfo = formatListsPageInfo(lists);
- commit(types.RECEIVE_ISSUES_FOR_LIST_SUCCESS, { listIssues, listPageInfo, listId });
+ commit(types.RECEIVE_ITEMS_FOR_LIST_SUCCESS, { listItems, listPageInfo, listId });
})
- .catch(() => commit(types.RECEIVE_ISSUES_FOR_LIST_FAILURE, listId));
+ .catch(() => commit(types.RECEIVE_ITEMS_FOR_LIST_FAILURE, listId));
},
resetIssues: ({ commit }) => {
commit(types.RESET_ISSUES);
},
+ moveItem: ({ dispatch }) => {
+ dispatch('moveIssue');
+ },
+
moveIssue: (
{ state, commit },
- { issueId, issueIid, issuePath, fromListId, toListId, moveBeforeId, moveAfterId },
+ { itemId, itemIid, itemPath, fromListId, toListId, moveBeforeId, moveAfterId },
) => {
- const originalIssue = state.issues[issueId];
- const fromList = state.issuesByListId[fromListId];
- const originalIndex = fromList.indexOf(Number(issueId));
+ const originalIssue = state.boardItems[itemId];
+ const fromList = state.boardItemsByListId[fromListId];
+ const originalIndex = fromList.indexOf(Number(itemId));
commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId });
const { boardId } = state;
- const [fullProjectPath] = issuePath.split(/[#]/);
+ const [fullProjectPath] = itemPath.split(/[#]/);
gqlClient
.mutate({
@@ -308,7 +348,7 @@ export default {
variables: {
projectPath: fullProjectPath,
boardId: fullBoardId(boardId),
- iid: issueIid,
+ iid: itemIid,
fromListId: getIdFromGraphQLId(fromListId),
toListId: getIdFromGraphQLId(toListId),
moveBeforeId,
@@ -317,7 +357,7 @@ export default {
})
.then(({ data }) => {
if (data?.issueMoveList?.errors.length) {
- commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex });
+ throw new Error();
} else {
const issue = data.issueMoveList?.issue;
commit(types.MOVE_ISSUE_SUCCESS, { issue });
@@ -532,10 +572,17 @@ export default {
commit(types.SET_SELECTED_PROJECT, project);
},
- toggleBoardItemMultiSelection: ({ commit, state }, boardItem) => {
+ toggleBoardItemMultiSelection: ({ commit, state, dispatch, getters }, boardItem) => {
const { selectedBoardItems } = state;
const index = selectedBoardItems.indexOf(boardItem);
+ // If user already selected an item (activeIssue) without using mult-select,
+ // include that item in the selection and unset state.ActiveId to hide the sidebar.
+ if (getters.activeIssue) {
+ commit(types.ADD_BOARD_ITEM_TO_SELECTION, getters.activeIssue);
+ dispatch('unsetActiveId');
+ }
+
if (index === -1) {
commit(types.ADD_BOARD_ITEM_TO_SELECTION, boardItem);
} else {
@@ -547,6 +594,20 @@ export default {
commit(types.SET_ADD_COLUMN_FORM_VISIBLE, visible);
},
+ resetBoardItemMultiSelection: ({ commit }) => {
+ commit(types.RESET_BOARD_ITEM_SELECTION);
+ },
+
+ toggleBoardItem: ({ state, dispatch }, { boardItem, sidebarType = ISSUABLE }) => {
+ dispatch('resetBoardItemMultiSelection');
+
+ if (boardItem.id === state.activeId) {
+ dispatch('unsetActiveId');
+ } else {
+ dispatch('setActiveId', { id: boardItem.id, sidebarType });
+ }
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index fbff736c7e1..092f81ad279 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -575,7 +575,7 @@ const boardsStore = {
},
saveList(list) {
- const entity = list.label || list.assignee || list.milestone;
+ const entity = list.label || list.assignee || list.milestone || list.iteration;
let entityType = '';
if (list.label) {
entityType = 'label_id';
@@ -583,6 +583,8 @@ const boardsStore = {
entityType = 'assignee_id';
} else if (IS_EE && list.milestone) {
entityType = 'milestone_id';
+ } else if (IS_EE && list.iteration) {
+ entityType = 'iteration_id';
}
return this.createList(entity.id, entityType)
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index cab97088bc6..caa518f91ce 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -1,20 +1,22 @@
import { find } from 'lodash';
-import { inactiveId } from '../constants';
+import { BoardType, inactiveId } from '../constants';
export default {
+ isGroupBoard: (state) => state.boardType === BoardType.group,
+ isProjectBoard: (state) => state.boardType === BoardType.project,
isSidebarOpen: (state) => state.activeId !== inactiveId,
isSwimlanesOn: () => false,
- getIssueById: (state) => (id) => {
- return state.issues[id] || {};
+ getBoardItemById: (state) => (id) => {
+ return state.boardItems[id] || {};
},
- getIssuesByList: (state, getters) => (listId) => {
- const listIssueIds = state.issuesByListId[listId] || [];
- return listIssueIds.map((id) => getters.getIssueById(id));
+ getBoardItemsByList: (state, getters) => (listId) => {
+ const listItemsIds = state.boardItemsByListId[listId] || [];
+ return listItemsIds.map((id) => getters.getBoardItemById(id));
},
activeIssue: (state) => {
- return state.issues[state.activeId] || {};
+ return state.boardItems[state.activeId] || {};
},
groupPathForActiveIssue: (_, getters) => {
@@ -27,6 +29,10 @@ export default {
return referencePath.slice(0, referencePath.indexOf('#'));
},
+ activeGroupProjects: (state) => {
+ return state.groupProjects.filter((p) => !p.archived);
+ },
+
getListByLabelId: (state) => (labelId) => {
if (!labelId) {
return null;
@@ -38,6 +44,10 @@ export default {
return find(state.boardLists, (l) => l.title === title);
},
+ isEpicBoard: () => {
+ return false;
+ },
+
shouldUseGraphQL: () => {
return gon?.features?.graphqlBoardLists;
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index a89e961ae2d..e7c034fb087 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -2,7 +2,9 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
+export const RECEIVE_LABELS_REQUEST = 'RECEIVE_LABELS_REQUEST';
export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
+export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
@@ -12,11 +14,12 @@ export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
export const MOVE_LIST = 'MOVE_LIST';
export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
+export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED';
export const REMOVE_LIST = 'REMOVE_LIST';
export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
-export const REQUEST_ISSUES_FOR_LIST = 'REQUEST_ISSUES_FOR_LIST';
-export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE';
-export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS';
+export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
+export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
+export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
export const CREATE_ISSUE_FAILURE = 'CREATE_ISSUE_FAILURE';
export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
@@ -45,3 +48,4 @@ export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_SELECTIO
export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
+export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 79c98c3d90c..75b60366b6a 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -2,7 +2,8 @@ import { pull, union } from 'lodash';
import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
-import { formatIssue, moveIssueListHelper } from '../boards_util';
+import { formatIssue, moveItemListHelper } from '../boards_util';
+import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types';
const notImplemented = () => {
@@ -10,34 +11,42 @@ const notImplemented = () => {
throw new Error('Not implemented!');
};
-export const removeIssueFromList = ({ state, listId, issueId }) => {
- Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
+const updateListItemsCount = ({ state, listId, value }) => {
const list = state.boardLists[listId];
- Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount - 1 });
+ if (state.issuableType === issuableTypes.epic) {
+ Vue.set(state.boardLists, listId, { ...list, epicsCount: list.epicsCount + value });
+ } else {
+ Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount + value });
+ }
+};
+
+export const removeItemFromList = ({ state, listId, itemId }) => {
+ Vue.set(state.boardItemsByListId, listId, pull(state.boardItemsByListId[listId], itemId));
+ updateListItemsCount({ state, listId, value: -1 });
};
-export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
- const listIssues = state.issuesByListId[listId];
+export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId, atIndex }) => {
+ const listIssues = state.boardItemsByListId[listId];
let newIndex = atIndex || 0;
if (moveBeforeId) {
newIndex = listIssues.indexOf(moveBeforeId) + 1;
} else if (moveAfterId) {
newIndex = listIssues.indexOf(moveAfterId);
}
- listIssues.splice(newIndex, 0, issueId);
- Vue.set(state.issuesByListId, listId, listIssues);
- const list = state.boardLists[listId];
- Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount + 1 });
+ listIssues.splice(newIndex, 0, itemId);
+ Vue.set(state.boardItemsByListId, listId, listIssues);
+ updateListItemsCount({ state, listId, value: 1 });
};
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
- const { boardType, disabled, boardId, fullPath, boardConfig } = data;
+ const { boardType, disabled, boardId, fullPath, boardConfig, issuableType } = data;
state.boardId = boardId;
state.fullPath = fullPath;
state.boardType = boardType;
state.disabled = disabled;
state.boardConfig = boardConfig;
+ state.issuableType = issuableType;
},
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
@@ -59,12 +68,25 @@ export default {
state.filterParams = filterParams;
},
- [mutationTypes.CREATE_LIST_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while creating the list. Please try again.');
+ [mutationTypes.CREATE_LIST_FAILURE]: (
+ state,
+ error = s__('Boards|An error occurred while creating the list. Please try again.'),
+ ) => {
+ state.error = error;
+ },
+
+ [mutationTypes.RECEIVE_LABELS_REQUEST]: (state) => {
+ state.labelsLoading = true;
},
[mutationTypes.RECEIVE_LABELS_SUCCESS]: (state, labels) => {
state.labels = labels;
+ state.labelsLoading = false;
+ },
+
+ [mutationTypes.RECEIVE_LABELS_FAILURE]: (state) => {
+ state.error = s__('Boards|An error occurred while fetching labels. Please reload the page.');
+ state.labelsLoading = false;
},
[mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: (state) => {
@@ -94,6 +116,10 @@ export default {
Vue.set(state, 'boardLists', backupList);
},
+ [mutationTypes.TOGGLE_LIST_COLLAPSED]: (state, { listId, collapsed }) => {
+ Vue.set(state.boardLists[listId], 'collapsed', collapsed);
+ },
+
[mutationTypes.REMOVE_LIST]: (state, listId) => {
Vue.delete(state.boardLists, listId);
},
@@ -103,26 +129,23 @@ export default {
state.boardLists = listsBackup;
},
- [mutationTypes.REQUEST_ISSUES_FOR_LIST]: (state, { listId, fetchNext }) => {
+ [mutationTypes.REQUEST_ITEMS_FOR_LIST]: (state, { listId, fetchNext }) => {
Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
},
- [mutationTypes.RECEIVE_ISSUES_FOR_LIST_SUCCESS]: (
- state,
- { listIssues, listPageInfo, listId },
- ) => {
- const { listData, issues } = listIssues;
- Vue.set(state, 'issues', { ...state.issues, ...issues });
+ [mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listItems, listPageInfo, listId }) => {
+ const { listData, boardItems } = listItems;
+ Vue.set(state, 'boardItems', { ...state.boardItems, ...boardItems });
Vue.set(
- state.issuesByListId,
+ state.boardItemsByListId,
listId,
- union(state.issuesByListId[listId] || [], listData[listId]),
+ union(state.boardItemsByListId[listId] || [], listData[listId]),
);
Vue.set(state.pageInfoByListId, listId, listPageInfo[listId]);
Vue.set(state.listsFlags, listId, { isLoading: false, isLoadingMore: false });
},
- [mutationTypes.RECEIVE_ISSUES_FOR_LIST_FAILURE]: (state, listId) => {
+ [mutationTypes.RECEIVE_ITEMS_FOR_LIST_FAILURE]: (state, listId) => {
state.error = s__(
'Boards|An error occurred while fetching the board issues. Please reload the page.',
);
@@ -130,18 +153,18 @@ export default {
},
[mutationTypes.RESET_ISSUES]: (state) => {
- Object.keys(state.issuesByListId).forEach((listId) => {
- Vue.set(state.issuesByListId, listId, []);
+ Object.keys(state.boardItemsByListId).forEach((listId) => {
+ Vue.set(state.boardItemsByListId, listId, []);
});
},
[mutationTypes.UPDATE_ISSUE_BY_ID]: (state, { issueId, prop, value }) => {
- if (!state.issues[issueId]) {
+ if (!state.boardItems[issueId]) {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
throw new Error('No issue found.');
}
- Vue.set(state.issues[issueId], prop, value);
+ Vue.set(state.boardItems[issueId], prop, value);
},
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
@@ -167,16 +190,16 @@ export default {
const fromList = state.boardLists[fromListId];
const toList = state.boardLists[toListId];
- const issue = moveIssueListHelper(originalIssue, fromList, toList);
- Vue.set(state.issues, issue.id, issue);
+ const issue = moveItemListHelper(originalIssue, fromList, toList);
+ Vue.set(state.boardItems, issue.id, issue);
- removeIssueFromList({ state, listId: fromListId, issueId: issue.id });
- addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId });
+ removeItemFromList({ state, listId: fromListId, itemId: issue.id });
+ addItemToList({ state, listId: toListId, itemId: issue.id, moveBeforeId, moveAfterId });
},
[mutationTypes.MOVE_ISSUE_SUCCESS]: (state, { issue }) => {
const issueId = getIdFromGraphQLId(issue.id);
- Vue.set(state.issues, issueId, formatIssue({ ...issue, id: issueId }));
+ Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId }));
},
[mutationTypes.MOVE_ISSUE_FAILURE]: (
@@ -184,12 +207,12 @@ export default {
{ originalIssue, fromListId, toListId, originalIndex },
) => {
state.error = s__('Boards|An error occurred while moving the issue. Please try again.');
- Vue.set(state.issues, originalIssue.id, originalIssue);
- removeIssueFromList({ state, listId: toListId, issueId: originalIssue.id });
- addIssueToList({
+ Vue.set(state.boardItems, originalIssue.id, originalIssue);
+ removeItemFromList({ state, listId: toListId, itemId: originalIssue.id });
+ addItemToList({
state,
listId: fromListId,
- issueId: originalIssue.id,
+ itemId: originalIssue.id,
atIndex: originalIndex,
});
},
@@ -211,23 +234,23 @@ export default {
},
[mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
- addIssueToList({
+ addItemToList({
state,
listId: list.id,
- issueId: issue.id,
+ itemId: issue.id,
atIndex: position,
});
- Vue.set(state.issues, issue.id, issue);
+ Vue.set(state.boardItems, issue.id, issue);
},
[mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issueId }) => {
state.error = s__('Boards|An error occurred while creating the issue. Please try again.');
- removeIssueFromList({ state, listId: list.id, issueId });
+ removeItemFromList({ state, listId: list.id, itemId: issueId });
},
[mutationTypes.REMOVE_ISSUE_FROM_LIST]: (state, { list, issue }) => {
- removeIssueFromList({ state, listId: list.id, issueId: issue.id });
- Vue.delete(state.issues, issue.id);
+ removeItemFromList({ state, listId: list.id, itemId: issue.id });
+ Vue.delete(state.boardItems, issue.id);
},
[mutationTypes.SET_CURRENT_PAGE]: () => {
@@ -272,7 +295,7 @@ export default {
},
[mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => {
- state.addColumnFormVisible = visible;
+ Vue.set(state.addColumnForm, 'visible', visible);
},
[mutationTypes.ADD_LIST_TO_HIGHLIGHTED_LISTS]: (state, listId) => {
@@ -282,4 +305,8 @@ export default {
[mutationTypes.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS]: (state, listId) => {
state.highlightedLists = state.highlightedLists.filter((id) => id !== listId);
},
+
+ [mutationTypes.RESET_BOARD_ITEM_SELECTION]: (state) => {
+ state.selectedBoardItems = [];
+ },
};
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 91544d6c9c5..19ba2a5df83 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -1,19 +1,22 @@
-import { inactiveId } from '~/boards/constants';
+import { inactiveId, ListType } from '~/boards/constants';
export default () => ({
boardType: null,
+ issuableType: null,
+ fullPath: null,
disabled: false,
isShowingLabels: true,
activeId: inactiveId,
sidebarType: '',
boardLists: {},
listsFlags: {},
- issuesByListId: {},
+ boardItemsByListId: {},
isSettingAssignees: false,
pageInfoByListId: {},
- issues: {},
+ boardItems: {},
filterParams: {},
boardConfig: {},
+ labelsLoading: false,
labels: [],
highlightedLists: [],
selectedBoardItems: [],
@@ -25,7 +28,10 @@ export default () => ({
},
selectedProject: {},
error: undefined,
- addColumnFormVisible: false,
+ addColumnForm: {
+ visible: false,
+ columnType: ListType.label,
+ },
// TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes: false,
});