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.js3
-rw-r--r--app/assets/javascripts/boards/components/board_blocked_icon.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue131
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue24
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue182
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue23
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue12
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue31
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue35
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue24
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue83
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue2
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue102
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js6
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue2
-rw-r--r--app/assets/javascripts/boards/components/project_select_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue2
-rw-r--r--app/assets/javascripts/boards/constants.js5
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql1
-rw-r--r--app/assets/javascripts/boards/index.js26
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js47
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js14
-rw-r--r--app/assets/javascripts/boards/mount_filtered_search_issue_boards.js31
-rw-r--r--app/assets/javascripts/boards/stores/actions.js29
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js11
-rw-r--r--app/assets/javascripts/boards/stores/getters.js2
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js18
32 files changed, 650 insertions, 216 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index e14a770411e..46f97e09385 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -54,6 +54,7 @@ export function formatListIssues(listIssues) {
const listIssue = {
...i,
id,
+ fullId: i.id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
};
@@ -106,8 +107,8 @@ export function formatIssueInput(issueInput, boardConfig) {
const { labels, assigneeId, milestoneId } = boardConfig;
return {
- milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null,
...issueInput,
+ milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null,
labelIds: [...labelIds, ...(labels?.map((l) => fullLabelId(l)) || [])],
assigneeIds: [...assigneeIds, ...(assigneeId ? [fullUserId(assigneeId)] : [])],
};
diff --git a/app/assets/javascripts/boards/components/board_blocked_icon.vue b/app/assets/javascripts/boards/components/board_blocked_icon.vue
index 0f92e714752..b81edb4dfe6 100644
--- a/app/assets/javascripts/boards/components/board_blocked_icon.vue
+++ b/app/assets/javascripts/boards/components/board_blocked_icon.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
-import { IssueType } from '~/graphql_shared/constants';
+import { TYPE_ISSUE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { truncate } from '~/lib/utils/text_utility';
import { __, n__, s__, sprintf } from '~/locale';
@@ -13,7 +13,7 @@ export default {
},
},
graphQLIdType: {
- [issuableTypes.issue]: IssueType,
+ [issuableTypes.issue]: TYPE_ISSUE,
},
referenceFormatter: {
[issuableTypes.issue]: (r) => r.split('/')[1],
@@ -163,7 +163,7 @@ export default {
><span data-testid="popover-title">{{ blockedLabel }}</span></template
>
<template v-if="loading">
- <gl-loading-icon />
+ <gl-loading-icon size="sm" />
<p class="gl-mt-4 gl-mb-0 gl-font-small">{{ loadingMessage }}</p>
</template>
<template v-else>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 2f4e9044b9e..05b64ddc773 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -1,5 +1,12 @@
<script>
-import { GlLabel, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlLabel,
+ GlTooltip,
+ GlTooltipDirective,
+ GlIcon,
+ GlLoadingIcon,
+ GlSprintf,
+} from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
@@ -16,6 +23,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue';
export default {
components: {
+ GlTooltip,
GlLabel,
GlLoadingIcon,
GlIcon,
@@ -25,6 +33,7 @@ export default {
IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
BoardBlockedIcon,
+ GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -55,7 +64,7 @@ export default {
};
},
computed: {
- ...mapState(['isShowingLabels', 'issuableType']),
+ ...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
...mapGetters(['isEpicBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
@@ -99,6 +108,12 @@ export default {
}
return false;
},
+ shouldRenderEpicCountables() {
+ return this.isEpicBoard && this.item.hasIssues;
+ },
+ shouldRenderEpicProgress() {
+ return this.totalWeight > 0;
+ },
showLabelFooter() {
return this.isShowingLabels && this.item.labels.find(this.showLabel);
},
@@ -115,6 +130,20 @@ export default {
}
return __('Blocked issue');
},
+ totalEpicsCount() {
+ return this.item.descendantCounts.openedEpics + this.item.descendantCounts.closedEpics;
+ },
+ totalIssuesCount() {
+ return this.item.descendantCounts.openedIssues + this.item.descendantCounts.closedIssues;
+ },
+ totalWeight() {
+ return (
+ this.item.descendantWeightSum.openedIssues + this.item.descendantWeightSum.closedIssues
+ );
+ },
+ totalProgress() {
+ return Math.round((this.item.descendantWeightSum.closedIssues / this.totalWeight) * 100);
+ },
},
methods: {
...mapActions(['performSearch', 'setError']),
@@ -227,17 +256,93 @@ export default {
{{ itemId }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
- <issue-due-date
- v-if="item.dueDate"
- :date="item.dueDate"
- :closed="item.closed || Boolean(item.closedAt)"
- />
- <issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
- <issue-card-weight
- v-if="validIssueWeight(item)"
- :weight="item.weight"
- @click="filterByWeight(item.weight)"
- />
+ <span v-if="shouldRenderEpicCountables" data-testid="epic-countables">
+ <gl-tooltip :target="() => $refs.countBadge" data-testid="epic-countables-tooltip">
+ <p v-if="allowSubEpics" class="gl-font-weight-bold gl-m-0">
+ {{ __('Epics') }} &#8226;
+ <span class="gl-font-weight-normal">
+ <gl-sprintf :message="__('%{openedEpics} open, %{closedEpics} closed')">
+ <template #openedEpics>{{ item.descendantCounts.openedEpics }}</template>
+ <template #closedEpics>{{ item.descendantCounts.closedEpics }}</template>
+ </gl-sprintf>
+ </span>
+ </p>
+ <p class="gl-font-weight-bold gl-m-0">
+ {{ __('Issues') }} &#8226;
+ <span class="gl-font-weight-normal">
+ <gl-sprintf :message="__('%{openedIssues} open, %{closedIssues} closed')">
+ <template #openedIssues>{{ item.descendantCounts.openedIssues }}</template>
+ <template #closedIssues>{{ item.descendantCounts.closedIssues }}</template>
+ </gl-sprintf>
+ </span>
+ </p>
+ <p class="gl-font-weight-bold gl-m-0">
+ {{ __('Total weight') }} &#8226;
+ <span class="gl-font-weight-normal" data-testid="epic-countables-total-weight">
+ {{ totalWeight }}
+ </span>
+ </p>
+ </gl-tooltip>
+
+ <gl-tooltip
+ v-if="shouldRenderEpicProgress"
+ :target="() => $refs.progressBadge"
+ data-testid="epic-progress-tooltip"
+ >
+ <p class="gl-font-weight-bold gl-m-0">
+ {{ __('Progress') }} &#8226;
+ <span class="gl-font-weight-normal" data-testid="epic-progress-tooltip-content">
+ <gl-sprintf
+ :message="__('%{completedWeight} of %{totalWeight} weight completed')"
+ >
+ <template #completedWeight>{{
+ item.descendantWeightSum.closedIssues
+ }}</template>
+ <template #totalWeight>{{ totalWeight }}</template>
+ </gl-sprintf>
+ </span>
+ </p>
+ </gl-tooltip>
+
+ <span ref="countBadge" class="issue-count-badge board-card-info gl-mr-0 gl-pr-0">
+ <span v-if="allowSubEpics" class="gl-mr-3">
+ <gl-icon name="epic" />
+ {{ totalEpicsCount }}
+ </span>
+ <span class="gl-mr-3" data-testid="epic-countables-counts-issues">
+ <gl-icon name="issues" />
+ {{ totalIssuesCount }}
+ </span>
+ <span class="gl-mr-3" data-testid="epic-countables-weight-issues">
+ <gl-icon name="weight" />
+ {{ totalWeight }}
+ </span>
+ </span>
+
+ <span
+ v-if="shouldRenderEpicProgress"
+ ref="progressBadge"
+ class="issue-count-badge board-card-info gl-pl-0"
+ >
+ <span class="gl-mr-3" data-testid="epic-progress">
+ <gl-icon name="progress" />
+ {{ totalProgress }}%
+ </span>
+ </span>
+ </span>
+ <span v-if="!isEpicBoard">
+ <issue-due-date
+ v-if="item.dueDate"
+ :date="item.dueDate"
+ :closed="item.closed || Boolean(item.closedAt)"
+ />
+ <issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
+ <issue-card-weight
+ v-if="validIssueWeight(item)"
+ :weight="item.weight"
+ @click="filterByWeight(item.weight)"
+ />
+ </span>
</span>
</div>
<div class="board-card-assignee gl-display-flex">
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index cc7262f3a39..69abf886ad7 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -41,7 +41,7 @@ export default {
watch: {
filterParams: {
handler() {
- if (this.list.id) {
+ if (this.list.id && !this.list.collapsed) {
this.fetchItemsForList({ listId: this.list.id });
}
},
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index b770ac06e89..53b071aaed1 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -12,10 +12,8 @@ import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
components: {
BoardAddNewColumn,
- BoardColumn:
- gon.features?.graphqlBoardLists || gon.features?.epicBoards
- ? BoardColumn
- : BoardColumnDeprecated,
+ BoardColumn,
+ BoardColumnDeprecated,
BoardContentSidebar: () => import('~/boards/components/board_content_sidebar.vue'),
EpicBoardContentSidebar: () =>
import('ee_component/boards/components/epic_board_content_sidebar.vue'),
@@ -38,11 +36,14 @@ export default {
computed: {
...mapState(['boardLists', 'error', 'addColumnForm']),
...mapGetters(['isSwimlanesOn', 'isEpicBoard']),
+ useNewBoardColumnComponent() {
+ return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard;
+ },
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
boardListsToUse() {
- return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard
+ return this.useNewBoardColumnComponent
? sortBy([...Object.values(this.boardLists)], 'position')
: this.lists;
},
@@ -65,6 +66,9 @@ export default {
return this.canDragColumns ? options : {};
},
+ boardColumnComponent() {
+ return this.useNewBoardColumnComponent ? BoardColumn : BoardColumnDeprecated;
+ },
},
methods: {
...mapActions(['moveList', 'unsetError']),
@@ -102,7 +106,8 @@ export default {
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
@end="handleDragOnEnd"
>
- <board-column
+ <component
+ :is="boardColumnComponent"
v-for="(list, index) in boardListsToUse"
:key="index"
ref="board"
@@ -125,14 +130,9 @@ export default {
<board-content-sidebar
v-if="isSwimlanesOn || glFeatures.graphqlBoardLists"
- class="boards-sidebar"
data-testid="issue-boards-sidebar"
/>
- <epic-board-content-sidebar
- v-else-if="isEpicBoard"
- class="boards-sidebar"
- data-testid="epic-boards-sidebar"
- />
+ <epic-board-content-sidebar v-else-if="isEpicBoard" data-testid="epic-boards-sidebar" />
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 16a8a9d253f..e014b82d362 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -1,20 +1,20 @@
<script>
import { GlDrawer } from '@gitlab/ui';
+import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
-import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
+import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
- headerHeight: `${contentTop()}px`,
components: {
GlDrawer,
BoardSidebarTitle,
@@ -25,8 +25,10 @@ export default {
BoardSidebarLabelsSelect,
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
- BoardSidebarWeightInput: () =>
- import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
+ SidebarTodoWidget,
+ MountingPortal,
+ SidebarWeightWidget: () =>
+ import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
IterationSidebarDropdownWidget: () =>
import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'),
},
@@ -45,6 +47,7 @@ export default {
default: false,
},
},
+ inheritAttrs: false,
computed: {
...mapGetters([
'isSidebarOpen',
@@ -64,7 +67,12 @@ export default {
},
},
methods: {
- ...mapActions(['toggleBoardItem', 'setAssignees', 'setActiveItemConfidential']),
+ ...mapActions([
+ 'toggleBoardItem',
+ 'setAssignees',
+ 'setActiveItemConfidential',
+ 'setActiveItemWeight',
+ ]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
@@ -73,87 +81,105 @@ export default {
</script>
<template>
- <gl-drawer
- v-if="showSidebar"
- :open="isSidebarOpen"
- :header-height="$options.headerHeight"
- @close="handleClose"
- >
- <template #header>{{ __('Issue details') }}</template>
- <template #default>
- <board-sidebar-title />
- <sidebar-assignees-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :initial-assignees="activeBoardItem.assignees"
- :allow-multiple-assignees="multipleAssigneesFeatureAvailable"
- @assignees-updated="setAssignees"
- />
- <sidebar-dropdown-widget
- v-if="epicFeatureAvailable"
- :iid="activeBoardItem.iid"
- issuable-attribute="epic"
- :workspace-path="projectPathForActiveIssue"
- :attr-workspace-path="groupPathForActiveIssue"
- :issuable-type="issuableType"
- data-testid="sidebar-epic"
- />
- <div>
+ <mounting-portal mount-to="#js-right-sidebar-portal" name="board-content-sidebar" append>
+ <gl-drawer
+ v-if="showSidebar"
+ v-bind="$attrs"
+ :open="isSidebarOpen"
+ class="boards-sidebar gl-absolute"
+ @close="handleClose"
+ >
+ <template #title>
+ <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Issue details') }}</h2>
+ </template>
+ <template #header>
+ <sidebar-todo-widget
+ class="gl-mt-3"
+ :issuable-id="activeBoardItem.fullId"
+ :issuable-iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ />
+ </template>
+ <template #default>
+ <board-sidebar-title />
+ <sidebar-assignees-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :initial-assignees="activeBoardItem.assignees"
+ :allow-multiple-assignees="multipleAssigneesFeatureAvailable"
+ @assignees-updated="setAssignees"
+ />
<sidebar-dropdown-widget
+ v-if="epicFeatureAvailable"
:iid="activeBoardItem.iid"
- issuable-attribute="milestone"
+ issuable-attribute="epic"
:workspace-path="projectPathForActiveIssue"
- :attr-workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
:issuable-type="issuableType"
- data-testid="sidebar-milestones"
+ data-testid="sidebar-epic"
/>
- <template v-if="!glFeatures.iterationCadences">
+ <div>
<sidebar-dropdown-widget
- v-if="iterationFeatureAvailable"
:iid="activeBoardItem.iid"
- issuable-attribute="iteration"
+ issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
- :attr-workspace-path="groupPathForActiveIssue"
+ :attr-workspace-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- class="gl-mt-5"
- data-testid="iteration-edit"
- data-qa-selector="iteration_container"
+ data-testid="sidebar-milestones"
/>
- </template>
- <template v-else>
- <iteration-sidebar-dropdown-widget
- v-if="iterationFeatureAvailable"
- :iid="activeBoardItem.iid"
- :workspace-path="projectPathForActiveIssue"
- :attr-workspace-path="groupPathForActiveIssue"
- :issuable-type="issuableType"
- class="gl-mt-5"
- data-testid="iteration-edit"
- data-qa-selector="iteration_container"
- />
- </template>
- </div>
- <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
- <sidebar-date-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :issuable-type="issuableType"
- data-testid="sidebar-due-date"
- />
- <board-sidebar-labels-select class="labels" />
- <board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" />
- <sidebar-confidentiality-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :issuable-type="issuableType"
- @confidentialityUpdated="setActiveItemConfidential($event)"
- />
- <sidebar-subscriptions-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :issuable-type="issuableType"
- data-testid="sidebar-notifications"
- />
- </template>
- </gl-drawer>
+ <template v-if="!glFeatures.iterationCadences">
+ <sidebar-dropdown-widget
+ v-if="iterationFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ issuable-attribute="iteration"
+ :workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
+ :issuable-type="issuableType"
+ class="gl-mt-5"
+ data-testid="iteration-edit"
+ />
+ </template>
+ <template v-else>
+ <iteration-sidebar-dropdown-widget
+ v-if="iterationFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ :workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
+ :issuable-type="issuableType"
+ class="gl-mt-5"
+ data-testid="iteration-edit"
+ />
+ </template>
+ </div>
+ <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
+ <sidebar-date-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ data-testid="sidebar-due-date"
+ />
+ <board-sidebar-labels-select class="labels" />
+ <sidebar-weight-widget
+ v-if="weightFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ @weightUpdated="setActiveItemWeight($event)"
+ />
+ <sidebar-confidentiality-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ @confidentialityUpdated="setActiveItemConfidential($event)"
+ />
+ <sidebar-subscriptions-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ data-testid="sidebar-notifications"
+ />
+ </template>
+ </gl-drawer>
+ </mounting-portal>
</template>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 13388f02f1f..cfd6b21fa66 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -27,7 +27,7 @@ export default {
},
computed: {
urlParams() {
- const { authorUsername, labelName, search } = this.filterParams;
+ const { authorUsername, labelName, assigneeUsername, search } = this.filterParams;
let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
@@ -35,6 +35,7 @@ export default {
{
'not[label_name][]': this.filterParams.not.labelName,
'not[author_username]': this.filterParams.not.authorUsername,
+ 'not[assignee_username]': this.filterParams.not.assigneeUsername,
},
undefined,
);
@@ -44,6 +45,7 @@ export default {
...notParams,
author_username: authorUsername,
'label_name[]': labelName,
+ assignee_username: assigneeUsername,
search,
};
},
@@ -62,7 +64,7 @@ export default {
this.performSearch();
},
getFilteredSearchValue() {
- const { authorUsername, labelName, search } = this.filterParams;
+ const { authorUsername, labelName, assigneeUsername, search } = this.filterParams;
const filteredSearchValue = [];
if (authorUsername) {
@@ -72,6 +74,13 @@ export default {
});
}
+ if (assigneeUsername) {
+ filteredSearchValue.push({
+ type: 'assignee_username',
+ value: { data: assigneeUsername, operator: '=' },
+ });
+ }
+
if (labelName?.length) {
filteredSearchValue.push(
...labelName.map((label) => ({
@@ -88,6 +97,13 @@ export default {
});
}
+ if (this.filterParams['not[assigneeUsername]']) {
+ filteredSearchValue.push({
+ type: 'assignee_username',
+ value: { data: this.filterParams['not[assigneeUsername]'], operator: '!=' },
+ });
+ }
+
if (this.filterParams['not[labelName]']) {
filteredSearchValue.push(
...this.filterParams['not[labelName]'].map((label) => ({
@@ -121,6 +137,9 @@ export default {
case 'author_username':
filterParams.authorUsername = filter.value.data;
break;
+ case 'assignee_username':
+ filterParams.assigneeUsername = filter.value.data;
+ break;
case 'label_name':
labels.push(filter.value.data);
break;
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index aa75a0d68f5..386ed6bd0a1 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -2,9 +2,9 @@
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
+import { TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { getParameterByName } from '~/lib/utils/common_utils';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import { fullLabelId, fullBoardId } from '../boards_util';
import { formType } from '../constants';
@@ -188,21 +188,19 @@ export default {
};
},
issueBoardScopeMutationVariables() {
- /* eslint-disable @gitlab/require-i18n-strings */
return {
weight: this.board.weight,
assigneeId: this.board.assignee?.id
- ? convertToGraphQLId('User', this.board.assignee.id)
+ ? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null,
milestoneId:
this.board.milestone?.id || this.board.milestone?.id === 0
- ? convertToGraphQLId('Milestone', this.board.milestone.id)
+ ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
: null,
iterationId: this.board.iteration_id
- ? convertToGraphQLId('Iteration', this.board.iteration_id)
+ ? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
: null,
};
- /* eslint-enable @gitlab/require-i18n-strings */
},
boardScopeMutationVariables() {
return {
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 81740b5cd17..8dca6be853f 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,6 +6,7 @@ import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_opt
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
import Tracking from '~/tracking';
+import { toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
@@ -21,6 +22,7 @@ export default {
components: {
BoardCard,
BoardNewIssue,
+ BoardNewEpic: () => import('ee_component/boards/components/board_new_epic.vue'),
GlLoadingIcon,
GlIntersectionObserver,
},
@@ -49,6 +51,7 @@ export default {
scrollOffset: 250,
showCount: false,
showIssueForm: false,
+ showEpicForm: false,
};
},
computed: {
@@ -64,6 +67,9 @@ export default {
issuableType: this.isEpicBoard ? 'epics' : 'issues',
});
},
+ toggleFormEventPrefix() {
+ return this.isEpicBoard ? toggleFormEventPrefix.epic : toggleFormEventPrefix.issue;
+ },
boardItemsSizeExceedsMax() {
return this.list.maxIssueCount > 0 && this.listItemsCount > this.list.maxIssueCount;
},
@@ -76,6 +82,12 @@ export default {
loadingMore() {
return this.listsFlags[this.list.id]?.isLoadingMore;
},
+ epicCreateFormVisible() {
+ return this.isEpicBoard && this.list.listType !== 'closed' && this.showEpicForm;
+ },
+ issueCreateFormVisible() {
+ return !this.isEpicBoard && this.list.listType !== 'closed' && this.showIssueForm;
+ },
listRef() {
// When list is draggable, the reference to the list needs to be accessed differently
return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
@@ -116,9 +128,10 @@ export default {
'list.id': {
handler(id, oldVal) {
if (id) {
- eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
+ eventHub.$on(`${this.toggleFormEventPrefix}${this.list.id}`, this.toggleForm);
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- eventHub.$off(`toggle-issue-form-${oldVal}`, this.toggleForm);
+
+ eventHub.$off(`${this.toggleFormEventPrefix}${oldVal}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${oldVal}`, this.scrollToTop);
}
},
@@ -126,7 +139,7 @@ export default {
},
},
beforeDestroy() {
- eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
+ eventHub.$off(`${this.toggleFormEventPrefix}${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
},
methods: {
@@ -147,7 +160,11 @@ export default {
this.fetchItemsForList({ listId: this.list.id, fetchNext: true });
},
toggleForm() {
- this.showIssueForm = !this.showIssueForm;
+ if (this.isEpicBoard) {
+ this.showEpicForm = !this.showEpicForm;
+ } else {
+ this.showIssueForm = !this.showIssueForm;
+ }
},
onReachingListBottom() {
if (!this.loadingMore && this.hasNextPage) {
@@ -225,9 +242,10 @@ export default {
:aria-label="$options.i18n.loading"
data-testid="board_list_loading"
>
- <gl-loading-icon />
+ <gl-loading-icon size="sm" />
</div>
- <board-new-issue v-if="list.listType !== 'closed' && showIssueForm" :list="list" />
+ <board-new-issue v-if="issueCreateFormVisible" :list="list" />
+ <board-new-epic v-if="epicCreateFormVisible" :list="list" />
<component
:is="treeRootWrapper"
v-show="!loading"
@@ -255,6 +273,7 @@ export default {
<li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
<gl-loading-icon
v-if="loadingMore"
+ size="sm"
:label="$options.i18n.loadingMoreboardItems"
data-testid="count-loading-icon"
/>
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index 9b3e7e1547d..fabaf7a85f5 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -429,7 +429,7 @@ export default {
data-qa-selector="board_list_cards_area"
>
<div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
- <gl-loading-icon />
+ <gl-loading-icon size="sm" />
</div>
<board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
<ul
@@ -450,7 +450,7 @@ export default {
:disabled="disabled"
/>
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
- <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" />
+ <gl-loading-icon v-show="list.loadingMore" size="sm" label="Loading more issues" />
<span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index bf8396f52a6..8d5f0f7eb89 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -16,13 +16,14 @@ import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import AccessorUtilities from '../../lib/utils/accessor';
-import { inactiveId, LIST, ListType } from '../constants';
+import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
import ItemCount from './item_count.vue';
export default {
i18n: {
newIssue: __('New issue'),
+ newEpic: s__('Boards|New epic'),
listSettings: __('List settings'),
expand: s__('Boards|Expand'),
collapse: s__('Boards|Collapse'),
@@ -72,7 +73,7 @@ export default {
},
computed: {
...mapState(['activeId']),
- ...mapGetters(['isEpicBoard']),
+ ...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
isLoggedIn() {
return Boolean(this.currentUserId);
},
@@ -102,7 +103,7 @@ export default {
},
showListHeaderActions() {
if (this.isLoggedIn) {
- return this.isNewIssueShown || this.isSettingsShown;
+ return this.isNewIssueShown || this.isNewEpicShown || this.isSettingsShown;
}
return false;
},
@@ -124,6 +125,9 @@ export default {
isNewIssueShown() {
return (this.listType === ListType.backlog || this.showListHeaderButton) && !this.isEpicBoard;
},
+ isNewEpicShown() {
+ return this.isEpicBoard && this.listType !== ListType.closed;
+ },
isSettingsShown() {
return (
this.listType !== ListType.backlog && this.showListHeaderButton && !this.list.collapsed
@@ -165,7 +169,17 @@ export default {
},
showNewIssueForm() {
- eventHub.$emit(`toggle-issue-form-${this.list.id}`);
+ if (this.isSwimlanesOn) {
+ eventHub.$emit('open-unassigned-lane');
+ this.$nextTick(() => {
+ eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
+ });
+ } else {
+ eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
+ }
+ },
+ showNewEpicForm() {
+ eventHub.$emit(`${toggleFormEventPrefix.epic}${this.list.id}`);
},
toggleExpanded() {
const collapsed = !this.list.collapsed;
@@ -342,7 +356,7 @@ export default {
<!-- EE end -->
<div
- class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
+ class="issue-count-badge gl-display-inline-flex gl-pr-2 no-drag gl-text-gray-500"
data-testid="issue-count-badge"
:class="{
'gl-display-none!': list.collapsed && isSwimlanesHeader,
@@ -380,6 +394,17 @@ export default {
/>
<gl-button
+ v-if="isNewEpicShown"
+ v-show="!list.collapsed"
+ v-gl-tooltip.hover
+ :aria-label="$options.i18n.newEpic"
+ :title="$options.i18n.newEpic"
+ class="no-drag"
+ icon="plus"
+ @click="showNewEpicForm"
+ />
+
+ <gl-button
v-if="isSettingsShown"
ref="settingsBtn"
v-gl-tooltip.hover
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index a63b49f9508..caeecb25227 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -4,13 +4,13 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
import { __ } from '~/locale';
+import { toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
export default {
name: 'BoardNewIssue',
i18n: {
- submit: __('Create issue'),
cancel: __('Cancel'),
},
components: {
@@ -32,7 +32,15 @@ export default {
},
computed: {
...mapState(['selectedProject']),
- ...mapGetters(['isGroupBoard']),
+ ...mapGetters(['isGroupBoard', 'isEpicBoard']),
+ /**
+ * We've extended this component in EE where
+ * submitButtonTitle returns a different string
+ * hence this is kept as a computed prop.
+ */
+ submitButtonTitle() {
+ return __('Create issue');
+ },
disabled() {
if (this.isGroupBoard) {
return this.title === '' || !this.selectedProject.name;
@@ -50,9 +58,7 @@ export default {
},
methods: {
...mapActions(['addListNewIssue']),
- submit(e) {
- e.preventDefault();
-
+ submit() {
const { title } = this;
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
@@ -76,7 +82,7 @@ export default {
},
reset() {
this.title = '';
- eventHub.$emit(`toggle-issue-form-${this.list.id}`);
+ eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
},
},
};
@@ -85,7 +91,7 @@ export default {
<template>
<div class="board-new-issue-form">
<div class="board-card position-relative p-3 rounded">
- <form ref="submitForm" @submit="submit">
+ <form ref="submitForm" @submit.prevent="submit">
<label :for="inputFieldId" class="label-bold">{{ __('Title') }}</label>
<input
:id="inputFieldId"
@@ -96,7 +102,7 @@ export default {
name="issue_title"
autocomplete="off"
/>
- <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
+ <project-select v-if="isGroupBoard && !isEpicBoard" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3">
<gl-button
ref="submitButton"
@@ -106,7 +112,7 @@ export default {
category="primary"
type="submit"
>
- {{ $options.i18n.submit }}
+ {{ submitButtonTitle }}
</gl-button>
<gl-button
ref="cancelButton"
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 75975c77df5..c089a6a39af 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui';
+import { MountingPortal } from 'portal-vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
@@ -9,14 +10,13 @@ import eventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
- headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List settings'),
components: {
GlButton,
GlDrawer,
GlLabel,
+ MountingPortal,
BoardSettingsSidebarWipLimit: () =>
import('ee_component/boards/components/board_settings_wip_limit.vue'),
BoardSettingsListTypes: () =>
@@ -24,6 +24,7 @@ export default {
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
inject: ['canAdminList'],
+ inheritAttrs: false,
data() {
return {
ListType,
@@ -86,43 +87,45 @@ export default {
</script>
<template>
- <gl-drawer
- v-if="showSidebar"
- class="js-board-settings-sidebar"
- :open="isSidebarOpen"
- :header-height="$options.headerHeight"
- @close="unsetActiveId"
- >
- <template #header>{{ $options.listSettingsText }}</template>
- <template v-if="isSidebarOpen">
- <div v-if="boardListType === ListType.label">
- <label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
- <gl-label
- :title="activeListLabel.title"
- :background-color="activeListLabel.color"
- :scoped="showScopedLabels(activeListLabel)"
- />
- </div>
+ <mounting-portal mount-to="#js-right-sidebar-portal" name="board-settings-sidebar" append>
+ <gl-drawer
+ v-if="showSidebar"
+ v-bind="$attrs"
+ class="js-board-settings-sidebar gl-absolute"
+ :open="isSidebarOpen"
+ @close="unsetActiveId"
+ >
+ <template #title>{{ $options.listSettingsText }}</template>
+ <template v-if="isSidebarOpen">
+ <div v-if="boardListType === ListType.label">
+ <label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
+ <gl-label
+ :title="activeListLabel.title"
+ :background-color="activeListLabel.color"
+ :scoped="showScopedLabels(activeListLabel)"
+ />
+ </div>
- <board-settings-list-types
- v-else
- :active-list="activeList"
- :board-list-type="boardListType"
- />
- <board-settings-sidebar-wip-limit
- v-if="isWipLimitsOn"
- :max-issue-count="activeList.maxIssueCount"
- />
- <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-mt-4">
- <gl-button
- variant="danger"
- category="secondary"
- icon="remove"
- data-testid="remove-list"
- @click.stop="deleteBoard"
- >{{ __('Remove list') }}
- </gl-button>
- </div>
- </template>
- </gl-drawer>
+ <board-settings-list-types
+ v-else
+ :active-list="activeList"
+ :board-list-type="boardListType"
+ />
+ <board-settings-sidebar-wip-limit
+ v-if="isWipLimitsOn"
+ :max-issue-count="activeList.maxIssueCount"
+ />
+ <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-mt-4">
+ <gl-button
+ variant="danger"
+ category="secondary"
+ icon="remove"
+ data-testid="remove-list"
+ @click.stop="deleteBoard"
+ >{{ __('Remove list') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-drawer>
+ </mounting-portal>
</template>
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 55bc91cbcff..21a34182369 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -105,7 +105,7 @@ export default Vue.extend({
closeSidebar() {
this.detail.issue = {};
},
- setAssignees(assignees) {
+ setAssignees({ assignees }) {
boardsStore.detail.issue.setAssignees(assignees);
},
showScopedLabels(label) {
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 5124467136e..98027917221 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -327,7 +327,7 @@ export default {
:class="scrollFadeClass"
></div>
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<div v-if="canAdminBoard">
<gl-dropdown-divider />
diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
index 85c7b27336b..c1536dff2c6 100644
--- a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
+++ b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
@@ -316,7 +316,7 @@ export default {
:class="scrollFadeClass"
></div>
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<div v-if="canAdminBoard">
<gl-dropdown-divider />
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
new file mode 100644
index 00000000000..d8dac17d326
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -0,0 +1,102 @@
+<script>
+import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
+import issueBoardFilters from '~/boards/issue_board_filters';
+import { TYPE_USER } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { __ } from '~/locale';
+import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+
+export default {
+ i18n: {
+ search: __('Search'),
+ label: __('Label'),
+ author: __('Author'),
+ assignee: __('Assignee'),
+ is: __('is'),
+ isNot: __('is not'),
+ },
+ components: { BoardFilteredSearch },
+ props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ boardType: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ tokens() {
+ const { label, is, isNot, author, assignee } = this.$options.i18n;
+ const { fetchAuthors, fetchLabels } = issueBoardFilters(
+ this.$apollo,
+ this.fullPath,
+ this.boardType,
+ );
+
+ return [
+ {
+ icon: 'labels',
+ title: label,
+ type: 'label_name',
+ operators: [
+ { value: '=', description: is },
+ { value: '!=', description: isNot },
+ ],
+ token: LabelToken,
+ unique: false,
+ symbol: '~',
+ fetchLabels,
+ },
+ {
+ icon: 'pencil',
+ title: author,
+ type: 'author_username',
+ operators: [
+ { value: '=', description: is },
+ { value: '!=', description: isNot },
+ ],
+ symbol: '@',
+ token: AuthorToken,
+ unique: true,
+ fetchAuthors,
+ preloadedAuthors: this.preloadedAuthors(),
+ },
+ {
+ icon: 'user',
+ title: assignee,
+ type: 'assignee_username',
+ operators: [
+ { value: '=', description: is },
+ { value: '!=', description: isNot },
+ ],
+ token: AuthorToken,
+ unique: true,
+ fetchAuthors,
+ preloadedAuthors: this.preloadedAuthors(),
+ },
+ ];
+ },
+ },
+ methods: {
+ preloadedAuthors() {
+ return gon?.current_user_id
+ ? [
+ {
+ id: convertToGraphQLId(TYPE_USER, gon.current_user_id),
+ name: gon.current_user_fullname,
+ username: gon.current_username,
+ avatarUrl: gon.current_user_avatar_url,
+ },
+ ]
+ : [];
+ },
+ },
+};
+</script>
+
+<template>
+ <board-filtered-search data-testid="issue-board-filtered-search" :tokens="tokens" />
+</template>
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2fd16f06455..6eb1dbfb46a 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -53,7 +53,9 @@ export default function initNewListDropdown() {
data(term, callback) {
const reqFailed = () => {
$dropdownToggle.data('bs.dropdown').hide();
- flash(__('Error fetching labels.'));
+ createFlash({
+ message: __('Error fetching labels.'),
+ });
};
if (store.getters.shouldUseGraphQL) {
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 77b6af77652..1412411c275 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -126,7 +126,7 @@ export default {
v-show="groupProjectsFlags.isLoading"
data-testid="dropdown-text-loading-icon"
>
- <gl-loading-icon class="gl-mx-auto" />
+ <gl-loading-icon class="gl-mx-auto" size="sm" />
</gl-dropdown-text>
<gl-dropdown-text
v-if="isFetchResultEmpty && !groupProjectsFlags.isLoading"
diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue
index afe161d9c54..fc95ba0461d 100644
--- a/app/assets/javascripts/boards/components/project_select_deprecated.vue
+++ b/app/assets/javascripts/boards/components/project_select_deprecated.vue
@@ -136,7 +136,7 @@ export default {
{{ project.namespacedName }}
</gl-dropdown-item>
<gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
- <gl-loading-icon class="gl-mx-auto" />
+ <gl-loading-icon class="gl-mx-auto" size="sm" />
</gl-dropdown-text>
<gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message">
<span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
index 352a25ef6d9..84802650dad 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -93,7 +93,7 @@ export default {
<slot name="title">
<span data-testid="title">{{ title }}</span>
</slot>
- <gl-loading-icon v-if="loading" inline class="gl-ml-2" />
+ <gl-loading-icon v-if="loading" size="sm" inline class="gl-ml-2" />
</span>
<gl-button
v-if="canUpdate"
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 80a8fc99895..21ef70582a4 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -45,6 +45,11 @@ export const formType = {
edit: 'edit',
};
+export const toggleFormEventPrefix = {
+ epic: 'toggle-epic-form-',
+ issue: 'toggle-issue-form-',
+};
+
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
diff --git a/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
index 3c5f4b3e3bd..70eb1dfbf7e 100644
--- a/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
@@ -1,6 +1,7 @@
mutation issueSetLabels($input: UpdateIssueInput!) {
updateIssue(input: $input) {
issue {
+ id
labels {
nodes {
id
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index fb347ce852d..de7c8a3bd6b 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,4 +1,5 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import PortalVue from 'portal-vue';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions, mapGetters } from 'vuex';
@@ -24,6 +25,7 @@ import '~/boards/filters/due_date_filters';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
+import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import toggleFocusMode from '~/boards/toggle_focus';
@@ -41,6 +43,7 @@ import boardConfigToggle from './config_toggle';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo);
+Vue.use(PortalVue);
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
@@ -76,6 +79,10 @@ export default () => {
issueBoardsApp.$destroy(true);
}
+ if (gon?.features?.issueBoardsFilteredSearch) {
+ initBoardsFilteredSearch(apolloProvider);
+ }
+
if (!gon?.features?.graphqlBoardLists) {
boardsStore.create();
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
@@ -182,9 +189,14 @@ export default () => {
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
},
mounted() {
- this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
-
- this.filterManager.setup();
+ if (!gon?.features?.issueBoardsFilteredSearch) {
+ this.filterManager = new FilteredSearchBoards(
+ boardsStore.filter,
+ true,
+ boardsStore.cantEdit,
+ );
+ this.filterManager.setup();
+ }
this.performSearch();
@@ -304,9 +316,11 @@ export default () => {
// eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler
new Vue({
el: document.getElementById('js-add-list'),
- data: {
- filters: boardsStore.state.filters,
- ...getMilestoneTitle($boardApp),
+ data() {
+ return {
+ filters: boardsStore.state.filters,
+ ...getMilestoneTitle($boardApp),
+ };
},
mounted() {
initNewListDropdown();
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
new file mode 100644
index 00000000000..699d7e12de4
--- /dev/null
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -0,0 +1,47 @@
+import groupBoardMembers from '~/boards/graphql/group_board_members.query.graphql';
+import projectBoardMembers from '~/boards/graphql/project_board_members.query.graphql';
+import { BoardType } from './constants';
+import boardLabels from './graphql/board_labels.query.graphql';
+
+export default function issueBoardFilters(apollo, fullPath, boardType) {
+ const isGroupBoard = boardType === BoardType.group;
+ const isProjectBoard = boardType === BoardType.project;
+ const transformLabels = ({ data }) => {
+ return isGroupBoard ? data.group?.labels.nodes || [] : data.project?.labels.nodes || [];
+ };
+
+ const boardAssigneesQuery = () => {
+ return isGroupBoard ? groupBoardMembers : projectBoardMembers;
+ };
+
+ const fetchAuthors = (authorsSearchTerm) => {
+ return apollo
+ .query({
+ query: boardAssigneesQuery(),
+ variables: {
+ fullPath,
+ search: authorsSearchTerm,
+ },
+ })
+ .then(({ data }) => data.workspace?.assignees.nodes.map(({ user }) => user));
+ };
+
+ const fetchLabels = (labelSearchTerm) => {
+ return apollo
+ .query({
+ query: boardLabels,
+ variables: {
+ fullPath,
+ searchTerm: labelSearchTerm,
+ isGroup: isGroupBoard,
+ isProject: isProjectBoard,
+ },
+ })
+ .then(transformLabels);
+ };
+
+ return {
+ fetchLabels,
+ fetchAuthors,
+ };
+}
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index a95d749d71c..1bb0ee5b7e3 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,6 +1,6 @@
/* global DocumentTouch */
-import sortableConfig from 'ee_else_ce/sortable/sortable_config';
+import sortableConfig from '~/sortable/sortable_config';
export function sortableStart() {
document.body.classList.add('is-dragging');
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 6c6e2522d92..ab24532d87f 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,5 +1,5 @@
/* eslint-disable class-methods-use-this */
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import boardsStore from '../stores/boards_store';
import ListAssignee from './assignee';
@@ -127,7 +127,11 @@ class List {
moveBeforeId,
moveAfterId,
})
- .catch(() => flash(__('Something went wrong while moving issues.')));
+ .catch(() =>
+ createFlash({
+ message: __('Something went wrong while moving issues.'),
+ }),
+ );
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
@@ -145,7 +149,11 @@ class List {
moveBeforeId,
moveAfterId,
})
- .catch(() => flash(__('Something went wrong while moving issues.')));
+ .catch(() =>
+ createFlash({
+ message: __('Something went wrong while moving issues.'),
+ }),
+ );
}
findIssue(id) {
diff --git a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
new file mode 100644
index 00000000000..7732091ef34
--- /dev/null
+++ b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
+import store from '~/boards/stores';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { queryToObject } from '~/lib/utils/url_utility';
+
+export default (apolloProvider) => {
+ const el = document.getElementById('js-issue-board-filtered-search');
+ const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
+
+ const initialFilterParams = {
+ ...convertObjectPropsToCamelCase(rawFilterParams, {}),
+ };
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ provide: {
+ initialFilterParams,
+ },
+ store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
+ apolloProvider,
+ render: (createElement) =>
+ createElement(IssueBoardFilteredSearch, {
+ props: { fullPath: store.state?.fullPath || '', boardType: store.state?.boardType || '' },
+ }),
+ });
+};
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index d4893f9eca7..0f1b72146c9 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -18,7 +18,9 @@ import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
-import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+// eslint-disable-next-line import/no-deprecated
+import { urlParamsToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
formatBoardLists,
@@ -74,6 +76,7 @@ export default {
performSearch({ dispatch }) {
dispatch(
'setFilters',
+ // eslint-disable-next-line import/no-deprecated
convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)),
);
@@ -170,8 +173,9 @@ export default {
addList: ({ commit, dispatch, getters }, list) => {
commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list));
+
dispatch('fetchItemsForList', {
- listId: getters.getListByTitle(ListTypeTitles.backlog).id,
+ listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
});
},
@@ -237,7 +241,7 @@ export default {
},
updateList: (
- { commit, state: { issuableType } },
+ { commit, state: { issuableType, boardItemsByListId = {} }, dispatch },
{ listId, position, collapsed, backupList },
) => {
gqlClient
@@ -252,6 +256,12 @@ export default {
.then(({ data }) => {
if (data?.updateBoardList?.errors.length) {
commit(types.UPDATE_LIST_FAILURE, backupList);
+ return;
+ }
+
+ // Only fetch when board items havent been fetched on a collapsed list
+ if (!boardItemsByListId[listId]) {
+ dispatch('fetchItemsForList', { listId });
}
})
.catch(() => {
@@ -285,7 +295,7 @@ export default {
commit(types.REMOVE_LIST_FAILURE, listsBackup);
} else {
dispatch('fetchItemsForList', {
- listId: getters.getListByTitle(ListTypeTitles.backlog).id,
+ listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
});
}
},
@@ -296,6 +306,8 @@ export default {
},
fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
+ if (!listId) return null;
+
if (!fetchNext) {
commit(types.RESET_ITEMS_FOR_LIST, listId);
}
@@ -469,11 +481,11 @@ export default {
}
},
- setAssignees: ({ commit, getters }, assigneeUsernames) => {
+ setAssignees: ({ commit }, { id, assignees }) => {
commit('UPDATE_BOARD_ITEM_BY_ID', {
- itemId: getters.activeBoardItem.id,
+ itemId: id,
prop: 'assignees',
- value: assigneeUsernames,
+ value: assignees,
});
},
@@ -701,4 +713,7 @@ export default {
unsetError: ({ commit }) => {
commit(types.SET_ERROR, undefined);
},
+
+ // EE action needs CE empty equivalent
+ setActiveItemWeight: () => {},
};
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 092f81ad279..49c40c7776a 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -7,13 +7,9 @@ import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
-import {
- urlParamsToObject,
- getUrlParamsArray,
- parseBoolean,
- convertObjectPropsToCamelCase,
-} from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+// eslint-disable-next-line import/no-deprecated
+import { mergeUrlParams, urlParamsToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub';
import ListAssignee from '../models/assignee';
@@ -601,6 +597,7 @@ const boardsStore = {
getListIssues(list, emptyIssues = true) {
const data = {
+ // eslint-disable-next-line import/no-deprecated
...urlParamsToObject(this.filter.path),
page: list.page,
};
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index b61ecc5ccb6..140c9ef7ac4 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -16,7 +16,7 @@ export default {
},
activeBoardItem: (state) => {
- return state.boardItems[state.activeId] || {};
+ return state.boardItems[state.activeId] || { iid: '', id: '', fullId: '' };
},
groupPathForActiveIssue: (_, getters) => {
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 6cd0a62657e..a32a100fa11 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -35,13 +35,23 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
- const { boardType, disabled, boardId, fullBoardId, fullPath, boardConfig, issuableType } = data;
+ const {
+ allowSubEpics,
+ boardConfig,
+ boardId,
+ boardType,
+ disabled,
+ fullBoardId,
+ fullPath,
+ issuableType,
+ } = data;
+ state.allowSubEpics = allowSubEpics;
+ state.boardConfig = boardConfig;
state.boardId = boardId;
- state.fullBoardId = fullBoardId;
- state.fullPath = fullPath;
state.boardType = boardType;
state.disabled = disabled;
- state.boardConfig = boardConfig;
+ state.fullBoardId = fullBoardId;
+ state.fullPath = fullPath;
state.issuableType = issuableType;
},