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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-16 12:09:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-16 12:09:20 +0300
commit8ae36d93f1a63874b584f0488fde88c1fee999c4 (patch)
treef1788ba1a7fb00248ff008f817f6feea89304339 /app/assets/javascripts/search
parentb394e58cc2e52e17e7991e3d7b705aa383c362ee (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/search')
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue14
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/index.vue291
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue43
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/tracking.js21
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/store/getters.js7
6 files changed, 373 insertions, 5 deletions
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 2ab5dfb8dea..8928f80d83a 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -2,6 +2,7 @@
import { GlButton, GlLink } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import Tracking from '~/tracking';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
HR_DEFAULT_CLASSES,
TRACKING_ACTION_CLICK,
@@ -12,6 +13,8 @@ import {
import { confidentialFilterData } from '../constants/confidential_filter_data';
import { stateFilterData } from '../constants/state_filter_data';
import ConfidentialityFilter from './confidentiality_filter.vue';
+import { labelFilterData } from './label_filter/data';
+import LabelFilter from './label_filter/index.vue';
import StatusFilter from './status_filter.vue';
export default {
@@ -21,7 +24,9 @@ export default {
GlLink,
StatusFilter,
ConfidentialityFilter,
+ LabelFilter,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
...mapState(['urlQuery', 'sidebarDirty', 'useNewNavigation']),
...mapGetters(['currentScope']),
@@ -34,6 +39,12 @@ export default {
showStatusFilter() {
return Object.values(stateFilterData.scopes).includes(this.currentScope);
},
+ showLabelFilter() {
+ return (
+ Object.values(labelFilterData.scopes).includes(this.currentScope) &&
+ this.glFeatures.searchIssueLabelAggregation
+ );
+ },
hrClasses() {
return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
},
@@ -61,7 +72,8 @@ export default {
<hr v-if="!useNewNavigation" :class="hrClasses" />
<status-filter v-if="showStatusFilter" class="gl-mb-5" />
<confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" />
- <div class="gl-display-flex gl-align-items-center gl-mt-5">
+ <label-filter v-if="showLabelFilter" />
+ <div class="gl-display-flex gl-align-items-center gl-mt-4">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
</gl-button>
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
new file mode 100644
index 00000000000..74855482b5d
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
@@ -0,0 +1,291 @@
+<script>
+import {
+ GlSearchBoxByType,
+ GlLabel,
+ GlLoadingIcon,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlFormCheckboxGroup,
+ GlDropdownForm,
+ GlAlert,
+ GlOutsideDirective as Outside,
+} from '@gitlab/ui';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { uniq } from 'lodash';
+import { rgbFromHex } from '@gitlab/ui/dist/utils/utils';
+import { slugify } from '~/lib/utils/text_utility';
+import { s__, sprintf } from '~/locale';
+
+import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
+
+import {
+ SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN,
+ SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN,
+ SEARCH_DESCRIBED_BY_DEFAULT,
+ SEARCH_DESCRIBED_BY_UPDATED,
+ SEARCH_RESULTS_LOADING,
+} from '~/vue_shared/global_search/constants';
+
+import { HR_DEFAULT_CLASSES, ONLY_SHOW_MD } from '../../constants';
+import LabelDropdownItems from './label_dropdown_items.vue';
+
+import {
+ FIRST_DROPDOWN_INDEX,
+ SEARCH_BOX_INDEX,
+ SEARCH_RESULTS_DESCRIPTION,
+ SEARCH_INPUT_DESCRIPTION,
+ labelFilterData,
+} from './data';
+
+import { trackSelectCheckbox, trackOpenDropdown } from './tracking';
+
+export default {
+ name: 'LabelFilter',
+ directives: { Outside },
+ components: {
+ DropdownKeyboardNavigation,
+ GlSearchBoxByType,
+ LabelDropdownItems,
+ GlLabel,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlFormCheckboxGroup,
+ GlDropdownForm,
+ GlLoadingIcon,
+ GlAlert,
+ },
+ data() {
+ return {
+ currentFocusIndex: SEARCH_BOX_INDEX,
+ isFocused: false,
+ };
+ },
+ i18n: {
+ SEARCH_LABELS: s__('GlobalSearch|Search labels'),
+ DROPDOWN_HEADER: s__('GlobalSearch|Label(s)'),
+ AGGREGATIONS_ERROR_MESSAGE: s__('GlobalSearch|Fetching aggregations error.'),
+ SEARCH_DESCRIBED_BY_DEFAULT,
+ SEARCH_RESULTS_LOADING,
+ SEARCH_DESCRIBED_BY_UPDATED,
+ SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN,
+ SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN,
+ },
+ computed: {
+ ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'aggregations']),
+ ...mapGetters([
+ 'filteredLabels',
+ 'filteredUnselectedLabels',
+ 'filteredAppliedSelectedLabels',
+ 'appliedSelectedLabels',
+ 'filteredUnappliedSelectedLabels',
+ ]),
+ searchInputDescribeBy() {
+ if (this.isLoggedIn) {
+ return this.$options.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN;
+ }
+ return this.$options.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN;
+ },
+ dropdownResultsDescription() {
+ if (!this.showSearchDropdown) {
+ return ''; // This allows aria-live to see register an update when the dropdown is shown
+ }
+
+ if (this.showDefaultItems) {
+ return sprintf(this.$options.i18n.SEARCH_DESCRIBED_BY_DEFAULT, {
+ count: this.filteredLabels.length,
+ });
+ }
+
+ return this.loading
+ ? this.$options.i18n.SEARCH_RESULTS_LOADING
+ : sprintf(this.$options.i18n.SEARCH_DESCRIBED_BY_UPDATED, {
+ count: this.filteredLabels.length,
+ });
+ },
+ currentFocusedOption() {
+ return this.filteredLabels[this.currentFocusIndex] || null;
+ },
+ currentFocusedId() {
+ return `${slugify(this.currentFocusedOption?.parent_full_name || 'undefined-name')}_${slugify(
+ this.currentFocusedOption?.title || 'undefined-title',
+ )}`;
+ },
+ defaultIndex() {
+ if (this.showDefaultItems) {
+ return SEARCH_BOX_INDEX;
+ }
+ return FIRST_DROPDOWN_INDEX;
+ },
+ hasSelectedLabels() {
+ return this.filteredAppliedSelectedLabels.length > 0;
+ },
+ hasUnselectedLabels() {
+ return this.filteredUnselectedLabels.length > 0;
+ },
+ dividerClasses() {
+ return [...HR_DEFAULT_CLASSES, ...ONLY_SHOW_MD];
+ },
+ labelSearchBox() {
+ return this.$refs.searchLabelInputBox?.$el.querySelector('[role=searchbox]');
+ },
+ combinedSelectedFilters() {
+ const appliedSelectedLabelKeys = this.appliedSelectedLabels.map((label) => label.key);
+ const { labels = [] } = this.query;
+
+ return uniq([...appliedSelectedLabelKeys, ...labels]);
+ },
+ searchLabels: {
+ get() {
+ return this.searchLabelString;
+ },
+ set(value) {
+ this.setLabelFilterSearch({ value });
+ },
+ },
+ selectedFilters: {
+ get() {
+ return this.combinedSelectedFilters;
+ },
+ set(value) {
+ this.setQuery({ key: this.$options.labelFilterData?.filterParam, value });
+
+ trackSelectCheckbox(value);
+ },
+ },
+ },
+ async created() {
+ await this.fetchAllAggregation();
+ },
+ methods: {
+ ...mapActions(['fetchAllAggregation', 'setQuery', 'closeLabel', 'setLabelFilterSearch']),
+ openDropdown() {
+ this.isFocused = true;
+
+ trackOpenDropdown();
+ },
+ closeDropdown(event) {
+ const { target } = event;
+
+ if (this.labelSearchBox !== target) {
+ this.isFocused = false;
+ }
+ },
+ onLabelClose(event) {
+ if (!event?.target?.closest('.gl-label')?.dataset) {
+ return;
+ }
+
+ const { key } = event.target.closest('.gl-label').dataset;
+ this.closeLabel({ key });
+ },
+ reactiveLabelColor(label) {
+ const { color, key } = label;
+
+ return this.query?.labels?.some((labelKey) => labelKey === key)
+ ? color
+ : `rgba(${rgbFromHex(color)}, 0.3)`;
+ },
+ isLabelClosable(label) {
+ const { key } = label;
+ return this.query?.labels?.some((labelKey) => labelKey === key);
+ },
+ },
+ FIRST_DROPDOWN_INDEX,
+ SEARCH_RESULTS_DESCRIPTION,
+ SEARCH_INPUT_DESCRIPTION,
+ labelFilterData,
+};
+</script>
+
+<template>
+ <div class="gl-pb-0 gl-md-pt-0 label-filter gl-relative">
+ <h5
+ class="gl-my-0"
+ data-testid="label-filter-title"
+ :class="{ 'gl-font-sm': useSidebarNavigation }"
+ >
+ {{ $options.labelFilterData.header }}
+ </h5>
+ <div class="gl-my-5">
+ <gl-label
+ v-for="label in appliedSelectedLabels"
+ :key="label.key"
+ class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
+ :data-key="label.key"
+ :background-color="reactiveLabelColor(label)"
+ :title="label.title"
+ :show-close-button="isLabelClosable(label)"
+ @close="onLabelClose"
+ />
+ </div>
+ <gl-search-box-by-type
+ ref="searchLabelInputBox"
+ v-model="searchLabels"
+ role="searchbox"
+ autocomplete="off"
+ :placeholder="$options.i18n.SEARCH_LABELS"
+ :aria-activedescendant="currentFocusedId"
+ :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
+ @focusin="openDropdown"
+ @keydown.esc="closeDropdown"
+ />
+ <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{
+ searchInputDescribeBy
+ }}</span>
+ <span
+ role="region"
+ :data-testid="$options.SEARCH_RESULTS_DESCRIPTION"
+ class="gl-sr-only"
+ aria-live="polite"
+ aria-atomic="true"
+ >
+ {{ dropdownResultsDescription }}
+ </span>
+ <div
+ v-if="isFocused"
+ v-outside="closeDropdown"
+ data-testid="header-search-dropdown-menu"
+ class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-1"
+ :class="{
+ 'gl-max-w-none!': useSidebarNavigation,
+ 'gl-min-w-full!': useSidebarNavigation,
+ 'gl-w-full!': useSidebarNavigation,
+ }"
+ >
+ <div class="header-search-dropdown-content gl-py-2">
+ <dropdown-keyboard-navigation
+ v-model="currentFocusIndex"
+ :max="filteredLabels.length - 1"
+ :min="$options.FIRST_DROPDOWN_INDEX"
+ :default-index="defaultIndex"
+ :enable-cycle="true"
+ />
+ <div v-if="!aggregations.error">
+ <gl-dropdown-section-header v-if="hasSelectedLabels || hasUnselectedLabels">{{
+ $options.i18n.DROPDOWN_HEADER
+ }}</gl-dropdown-section-header>
+ <gl-dropdown-form>
+ <gl-form-checkbox-group v-model="selectedFilters">
+ <label-dropdown-items
+ v-if="hasSelectedLabels"
+ data-testid="selected-lavel-items"
+ :labels="filteredAppliedSelectedLabels"
+ />
+ <gl-dropdown-divider v-if="hasSelectedLabels && hasUnselectedLabels" />
+ <label-dropdown-items
+ v-if="hasUnselectedLabels"
+ data-testid="unselected-lavel-items"
+ :labels="filteredUnselectedLabels"
+ />
+ </gl-form-checkbox-group>
+ </gl-dropdown-form>
+ </div>
+ <gl-alert v-else :dismissible="false" variant="danger">
+ {{ $options.i18n.AGGREGATIONS_ERROR_MESSAGE }}
+ </gl-alert>
+ <gl-loading-icon v-if="aggregations.fetching" size="lg" class="my-4" />
+ </div>
+ </div>
+ <hr v-if="!useSidebarNavigation" :class="dividerClasses" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue b/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue
new file mode 100644
index 00000000000..7a9e6a2e4fc
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue
@@ -0,0 +1,43 @@
+<script>
+import { GlFormCheckbox } from '@gitlab/ui';
+
+export default {
+ name: 'LabelDropdownItems',
+ components: {
+ GlFormCheckbox,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <ul class="gl-list-style-none gl-px-0">
+ <li
+ v-for="label in labels"
+ :id="label.key"
+ :ref="label.key"
+ :key="label.key"
+ :aria-label="label.title"
+ tabindex="-1"
+ class="gl-px-5 gl-py-3 label-filter-menu-item"
+ >
+ <gl-form-checkbox
+ class="label-with-color-checkbox gl-display-inline-flex gl-h-5 gl-min-h-5"
+ :value="label.key"
+ >
+ <span
+ data-testid="label-color-indicator"
+ class="gl-rounded-base gl-w-5 gl-h-5 gl-display-inline-block gl-vertical-align-bottom gl-mr-3"
+ :style="{ 'background-color': label.color }"
+ ></span>
+ <span class="gl-reset-text-align gl-m-0 gl-p-0 label-title">{{
+ label.title
+ }}</span></gl-form-checkbox
+ >
+ </li>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/tracking.js b/app/assets/javascripts/search/sidebar/components/label_filter/tracking.js
new file mode 100644
index 00000000000..c38922a559c
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/tracking.js
@@ -0,0 +1,21 @@
+import Tracking from '~/tracking';
+
+export const TRACKING_CATEGORY = 'Language filters';
+export const TRACKING_LABEL_FILTER = 'Label Key';
+
+export const TRACKING_LABEL_DROPDOWN = 'Dropdown';
+export const TRACKING_LABEL_CHECKBOX = 'Label Checkbox';
+
+export const TRACKING_ACTION_SELECT = 'search:agreggations:label:select';
+export const TRACKING_ACTION_SHOW = 'search:agreggations:label:show';
+
+export const trackSelectCheckbox = (value) =>
+ Tracking.event(TRACKING_ACTION_SELECT, TRACKING_LABEL_CHECKBOX, {
+ label: TRACKING_LABEL_FILTER,
+ property: value,
+ });
+
+export const trackOpenDropdown = () =>
+ Tracking.event(TRACKING_ACTION_SHOW, TRACKING_LABEL_DROPDOWN, {
+ label: TRACKING_LABEL_DROPDOWN,
+ });
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
index e531abf523b..c10b14bd116 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
@@ -2,7 +2,6 @@
import { GlButton, GlAlert, GlForm } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { __, s__, sprintf } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HR_DEFAULT_CLASSES, ONLY_SHOW_MD } from '../../constants';
import { convertFiltersData } from '../../utils';
import CheckboxFilter from './checkbox_filter.vue';
@@ -24,7 +23,6 @@ export default {
GlAlert,
GlForm,
},
- mixins: [glFeatureFlagsMixin()],
data() {
return {
showAll: false,
diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js
index d31d2b5ae11..c7cb595f42f 100644
--- a/app/assets/javascripts/search/store/getters.js
+++ b/app/assets/javascripts/search/store/getters.js
@@ -41,8 +41,11 @@ export const filteredLabels = (state) => {
export const filteredAppliedSelectedLabels = (state) =>
filteredLabels(state)?.filter((label) => state?.urlQuery?.labels?.includes(label.key));
-export const appliedSelectedLabels = (state) =>
- labelAggregationBuckets(state)?.filter((label) => state?.urlQuery?.labels?.includes(label.key));
+export const appliedSelectedLabels = (state) => {
+ return labelAggregationBuckets(state)?.filter((label) =>
+ state?.urlQuery?.labels?.includes(label.key),
+ );
+};
export const filteredUnappliedSelectedLabels = (state) =>
filteredLabels(state)?.filter((label) => state?.query?.labels?.includes(label.key));