Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/boards/components')
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue5
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue11
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue44
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue154
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue8
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue46
8 files changed, 241 insertions, 30 deletions
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index aacea0b970c..2821b799cef 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import BoardCardInner from './board_card_inner.vue';
export default {
@@ -31,7 +31,6 @@ export default {
},
computed: {
...mapState(['selectedBoardItems', 'activeId']),
- ...mapGetters(['isSwimlanesOn']),
isActive() {
return this.item.id === this.activeId;
},
@@ -46,7 +45,7 @@ export default {
...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 (e.target.closest('.js-no-trigger')) return;
const isMultiSelect = e.ctrlKey || e.metaKey;
if (isMultiSelect) {
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 9ff2cdd76d0..0cb2e64042e 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -190,6 +190,7 @@ export default {
<template v-for="label in orderedLabels">
<gl-label
:key="label.id"
+ class="js-no-trigger"
:background-color="label.color"
:title="label.title"
:description="label.description"
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index a4b1e6adacf..b8a38d833ad 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -4,7 +4,6 @@ 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';
import BoardColumn from './board_column.vue';
@@ -48,7 +47,7 @@ export default {
: this.lists;
},
canDragColumns() {
- return !this.isEpicBoard && this.glFeatures.graphqlBoardLists && this.canAdminList;
+ return (this.isEpicBoard || this.glFeatures.graphqlBoardLists) && this.canAdminList;
},
boardColumnWrapper() {
return this.canDragColumns ? Draggable : 'div';
@@ -73,14 +72,7 @@ export default {
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;
const listId = item.dataset.id;
@@ -108,7 +100,6 @@ export default {
ref="list"
v-bind="draggableOptions"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
- @start="handleDragOnStart"
@end="handleDragOnEnd"
>
<board-column
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 46359cc2bca..e1f8457c0e2 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -4,13 +4,13 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
-import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.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 glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
+import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
export default {
headerHeight: `${contentTop()}px`,
@@ -18,10 +18,11 @@ export default {
GlDrawer,
BoardSidebarTitle,
SidebarAssigneesWidget,
+ SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
BoardSidebarDueDate,
- BoardSidebarSubscription,
+ SidebarSubscriptionsWidget,
BoardSidebarMilestoneSelect,
BoardSidebarEpicSelect: () =>
import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
@@ -30,7 +31,20 @@ export default {
SidebarIterationWidget: () =>
import('ee_component/sidebar/components/sidebar_iteration_widget.vue'),
},
- mixins: [glFeatureFlagsMixin()],
+ inject: {
+ multipleAssigneesFeatureAvailable: {
+ default: false,
+ },
+ epicFeatureAvailable: {
+ default: false,
+ },
+ iterationFeatureAvailable: {
+ default: false,
+ },
+ weightFeatureAvailable: {
+ default: false,
+ },
+ },
computed: {
...mapGetters([
'isSidebarOpen',
@@ -50,7 +64,7 @@ export default {
},
},
methods: {
- ...mapActions(['toggleBoardItem', 'setAssignees']),
+ ...mapActions(['toggleBoardItem', 'setAssignees', 'setActiveItemConfidential']),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
@@ -72,13 +86,14 @@ export default {
:iid="activeBoardItem.iid"
:full-path="fullPath"
:initial-assignees="activeBoardItem.assignees"
- class="assignee"
+ :allow-multiple-assignees="multipleAssigneesFeatureAvailable"
@assignees-updated="setAssignees"
/>
- <board-sidebar-epic-select class="epic" />
+ <board-sidebar-epic-select v-if="epicFeatureAvailable" class="epic" />
<div>
<board-sidebar-milestone-select />
<sidebar-iteration-widget
+ v-if="iterationFeatureAvailable"
:iid="activeBoardItem.iid"
:workspace-path="projectPathForActiveIssue"
:iterations-workspace-path="groupPathForActiveIssue"
@@ -89,8 +104,19 @@ export default {
<board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
<board-sidebar-due-date />
<board-sidebar-labels-select class="labels" />
- <board-sidebar-weight-input v-if="glFeatures.issueWeights" class="weight" />
- <board-sidebar-subscription class="subscriptions" />
+ <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>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
new file mode 100644
index 00000000000..e564af0c353
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -0,0 +1,154 @@
+<script>
+import { pickBy } from 'lodash';
+import { mapActions } from 'vuex';
+import { updateHistory, 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'),
+ label: __('Label'),
+ author: __('Author'),
+ },
+ components: { FilteredSearch },
+ inject: ['initialFilterParams'],
+ props: {
+ tokens: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ filterParams: this.initialFilterParams,
+ };
+ },
+ computed: {
+ urlParams() {
+ const { authorUsername, labelName, search } = this.filterParams;
+ let notParams = {};
+
+ if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
+ notParams = pickBy(
+ {
+ 'not[label_name][]': this.filterParams.not.labelName,
+ 'not[author_username]': this.filterParams.not.authorUsername,
+ },
+ undefined,
+ );
+ }
+
+ return {
+ ...notParams,
+ author_username: authorUsername,
+ 'label_name[]': labelName,
+ search,
+ };
+ },
+ },
+ methods: {
+ ...mapActions(['performSearch']),
+ handleFilter(filters) {
+ this.filterParams = this.getFilterParams(filters);
+
+ updateHistory({
+ url: setUrlParams(this.urlParams, window.location.href, true, false, true),
+ title: document.title,
+ replace: true,
+ });
+
+ this.performSearch();
+ },
+ getFilteredSearchValue() {
+ const { authorUsername, labelName, search } = this.filterParams;
+ const filteredSearchValue = [];
+
+ if (authorUsername) {
+ filteredSearchValue.push({
+ type: 'author_username',
+ value: { data: authorUsername, operator: '=' },
+ });
+ }
+
+ if (labelName?.length) {
+ filteredSearchValue.push(
+ ...labelName.map((label) => ({
+ type: 'label_name',
+ value: { data: label, operator: '=' },
+ })),
+ );
+ }
+
+ if (this.filterParams['not[authorUsername]']) {
+ filteredSearchValue.push({
+ type: 'author_username',
+ value: { data: this.filterParams['not[authorUsername]'], operator: '!=' },
+ });
+ }
+
+ if (this.filterParams['not[labelName]']) {
+ filteredSearchValue.push(
+ ...this.filterParams['not[labelName]'].map((label) => ({
+ type: 'label_name',
+ value: { data: label, operator: '!=' },
+ })),
+ );
+ }
+
+ if (search) {
+ filteredSearchValue.push(search);
+ }
+
+ return filteredSearchValue;
+ },
+ getFilterParams(filters = []) {
+ const notFilters = filters.filter((item) => item.value.operator === '!=');
+ const equalsFilters = filters.filter((item) => item.value.operator === '=');
+
+ return { ...this.generateParams(equalsFilters), not: { ...this.generateParams(notFilters) } };
+ },
+ generateParams(filters = []) {
+ const filterParams = {};
+ const labels = [];
+ const plainText = [];
+
+ filters.forEach((filter) => {
+ switch (filter.type) {
+ case 'author_username':
+ filterParams.authorUsername = filter.value.data;
+ break;
+ case 'label_name':
+ labels.push(filter.value.data);
+ break;
+ case 'filtered-search-term':
+ if (filter.value.data) plainText.push(filter.value.data);
+ break;
+ default:
+ break;
+ }
+ });
+
+ if (labels.length) {
+ filterParams.labelName = labels;
+ }
+
+ if (plainText.length) {
+ filterParams.search = plainText.join(' ');
+ }
+ return filterParams;
+ },
+ },
+};
+</script>
+
+<template>
+ <filtered-search
+ class="gl-w-full"
+ namespace=""
+ :tokens="tokens"
+ :search-input-placeholder="$options.i18n.search"
+ :initial-filter-value="getFilteredSearchValue()"
+ @onFilter="handleFilter"
+ />
+</template>
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index ca66ad6934a..f94697172ac 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -161,7 +161,7 @@ export default {
const collapsed = !this.list.collapsed;
this.toggleListCollapsed({ listId: this.list.id, collapsed });
- if (!this.isLoggedIn || this.isEpicBoard) {
+ if (!this.isLoggedIn) {
this.addToLocalStorage();
} else {
this.updateListFunction();
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 997655c346a..3d7f1f38a34 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -29,17 +29,17 @@ export default {
};
},
computed: {
- ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
+ ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL', 'isEpicBoard']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
- return this.glFeatures.wipLimits;
+ return this.glFeatures.wipLimits && !this.isEpicBoard;
},
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects
*/
- if (this.shouldUseGraphQL) {
+ if (this.shouldUseGraphQL || this.isEpicBoard) {
return this.boardLists[this.activeId];
}
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
@@ -71,7 +71,7 @@ export default {
deleteBoard() {
// eslint-disable-next-line no-alert
if (window.confirm(__('Are you sure you want to remove this list?'))) {
- if (this.shouldUseGraphQL) {
+ if (this.shouldUseGraphQL || this.isEpicBoard) {
this.removeList(this.activeId);
} else {
this.activeList.destroy();
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index f78be83cd82..919ef0d3783 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -1,10 +1,12 @@
<script>
import { GlLabel } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
+import Api from '~/api';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
@@ -14,7 +16,13 @@ export default {
LabelsSelect,
GlLabel,
},
- inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
+ inject: {
+ labelsFetchPath: {
+ default: null,
+ },
+ labelsManagePath: {},
+ labelsFilterBasePath: {},
+ },
data() {
return {
loading: false,
@@ -38,6 +46,32 @@ export default {
scoped: isScopedLabel(label),
}));
},
+ fetchPath() {
+ /*
+ Labels fetched in epic boards are always group-level labels
+ and the correct path are passed from the backend (injected through labelsFetchPath)
+
+ For issue boards, we should always include project-level labels and use a different endpoint.
+ (it requires knowing the project path of a selected issue.)
+
+ Note 1. that we will be using GraphQL to fetch labels when we create a labels select widget.
+ And this component will be removed _wholesale_ https://gitlab.com/gitlab-org/gitlab/-/issues/300653.
+
+ Note 2. Moreover, 'fetchPath' needs to be used as a key for 'labels-select' component to force updates.
+ 'labels-select' has its own vuex store and initializes the passed props as states
+ and these states aren't reactively bound to the passed props.
+ */
+
+ const projectLabelsFetchPath = mergeUrlParams(
+ { include_ancestor_groups: true },
+ Api.buildUrl(Api.projectLabelsPath).replace(
+ ':namespace_path/:project_path',
+ this.projectPathForActiveIssue,
+ ),
+ );
+
+ return this.labelsFetchPath || projectLabelsFetchPath;
+ },
},
methods: {
...mapActions(['setActiveBoardItemLabels']),
@@ -77,7 +111,12 @@ export default {
</script>
<template>
- <board-editable-item ref="sidebarItem" :title="__('Labels')" :loading="loading">
+ <board-editable-item
+ ref="sidebarItem"
+ :title="__('Labels')"
+ :loading="loading"
+ data-testid="sidebar-labels"
+ >
<template #collapsed>
<gl-label
v-for="label in issueLabels"
@@ -95,12 +134,13 @@ export default {
<template #default="{ edit }">
<labels-select
ref="labelsSelect"
+ :key="fetchPath"
:allow-label-edit="false"
:allow-label-create="false"
:allow-multiselect="true"
:allow-scoped-labels="true"
:selected-labels="selectedLabels"
- :labels-fetch-path="labelsFetchPath"
+ :labels-fetch-path="fetchPath"
:labels-manage-path="labelsManagePath"
:labels-filter-base-path="labelsFilterBasePath"
:labels-list-title="__('Select label')"