-
+
-import {
- GlSearchBoxByType,
- GlLabel,
- GlLoadingIcon,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlFormCheckboxGroup,
- GlDropdownForm,
- GlAlert,
- GlOutsideDirective as Outside,
-} from '@gitlab/ui';
+import { GlLabel, GlCollapsibleListbox, GlOutsideDirective as Outside } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';
import { difference, uniq } from 'lodash';
import { rgbFromHex } from '@gitlab/ui/dist/utils/utils';
-import { slugify } from '~/lib/utils/text_utility';
-
-import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
-
import { I18N } from '~/vue_shared/global_search/constants';
import {
FIRST_DROPDOWN_INDEX,
@@ -27,92 +13,46 @@ import {
LABEL_FILTER_HEADER,
LABEL_FILTER_PARAM,
} from '../../constants';
-import LabelDropdownItems from './label_dropdown_items.vue';
-
import { trackSelectCheckbox, trackOpenDropdown } from './tracking';
export default {
name: 'LabelFilter',
directives: { Outside },
components: {
- DropdownKeyboardNavigation,
- GlSearchBoxByType,
- LabelDropdownItems,
GlLabel,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlFormCheckboxGroup,
- GlDropdownForm,
- GlLoadingIcon,
- GlAlert,
+ GlCollapsibleListbox,
},
data() {
return {
currentFocusIndex: SEARCH_BOX_INDEX,
- isFocused: false,
combinedSelectedLabels: [],
};
},
i18n: I18N,
computed: {
- ...mapState(['searchLabelString', 'query', 'urlQuery', 'aggregations']),
+ ...mapState(['query', 'urlQuery', 'aggregations']),
...mapGetters([
'filteredLabels',
- 'labelAggregationBuckets',
- 'filteredUnselectedLabels',
'filteredAppliedSelectedLabels',
'appliedSelectedLabels',
'unselectedLabels',
'unappliedNewLabels',
]),
- currentFocusedOption() {
- return this.filteredLabels[this.currentFocusIndex] || null;
- },
- currentFocusedId() {
- return `${slugify(this.currentFocusedOption?.parent_full_name || 'undefined-name')}_${slugify(
- this.currentFocusedOption?.title || 'undefined-title',
- )}`;
- },
- hasSelectedLabels() {
- return this.filteredAppliedSelectedLabels?.length > 0;
- },
- hasUnselectedLabels() {
- return this.filteredUnselectedLabels?.length > 0;
- },
- labelSearchBox() {
- return this.$refs.searchLabelInputBox?.$el.querySelector('[role=searchbox]');
- },
combinedSelectedFilters() {
const appliedSelectedLabelKeys = this.appliedSelectedLabels.map((label) => label.title);
const { labels = [] } = this.query;
-
- const uniqueResults = uniq([...appliedSelectedLabelKeys, ...labels]);
-
- return uniqueResults;
+ return uniq([...appliedSelectedLabelKeys, ...labels]);
},
- searchLabels: {
- get() {
- return this.searchLabelString;
- },
- set(value) {
- this.setLabelFilterSearch({ value });
- },
- },
- selectedLabels: {
- get() {
- return this.convertLabelNamesToIds(this.combinedSelectedLabels);
- },
- set(value) {
- const labelName = this.getLabelNameById(value);
- this.setQuery({ key: this.$options.LABEL_FILTER_PARAM, value: labelName });
- trackSelectCheckbox(value);
- },
+ items() {
+ // Map items to include the required "value" property needed for GlCollapsibleListbox
+
+ return this.filteredLabels.map((item) => ({ ...item, value: item.title }));
},
},
watch: {
combinedSelectedFilters(newLabels, oldLabels) {
- const hasDifference = difference(newLabels, oldLabels).length > 0;
- if (hasDifference) {
+ // Lodash `difference` checks one-way, compare length to check when labels are removed
+ if (newLabels.length !== oldLabels.length || difference(newLabels, oldLabels).length > 0) {
this.combinedSelectedLabels = newLabels;
}
},
@@ -123,65 +63,41 @@ export default {
},
},
async created() {
+ // Only preload labels if there is a label filter param in urlQuery
if (this.urlQuery?.[LABEL_FILTER_PARAM]?.length > 0) {
await this.fetchAllAggregation();
}
},
methods: {
...mapActions(['fetchAllAggregation', 'setQuery', 'closeLabel', 'setLabelFilterSearch']),
- async openDropdown() {
- this.isFocused = true;
-
- if (!this.aggregations.error && this.filteredLabels?.length === 0) {
+ async onShown() {
+ // only fetch labels when label dropdown is opened
+ if (this.items.length === 0) {
await this.fetchAllAggregation();
}
-
trackOpenDropdown();
},
- closeDropdown(event) {
- if (!this.isFocused) {
- return;
- }
-
- const { target } = event;
-
- if (this.labelSearchBox !== target) {
- this.isFocused = false;
- }
- },
onLabelClose(event) {
if (!event?.target?.closest('.gl-label')?.dataset) {
return;
}
-
const { title } = event.target.closest('.gl-label').dataset;
this.closeLabel({ title });
},
inactiveLabelColor(label) {
return `rgba(${rgbFromHex(label.color)}, 0.3)`;
},
- getLabelNameById(labelIds) {
- const labelNames = labelIds.map((id) => {
- const label = this.labelAggregationBuckets.find((filteredLabel) => {
- return filteredLabel.key === String(id);
- });
- return label?.title;
- });
- return labelNames;
+ onSearch(query) {
+ this.setLabelFilterSearch({ value: query });
},
- convertLabelNamesToIds(labelNames) {
- const labels = labelNames.map((labelName) =>
- this.labelAggregationBuckets.find((label) => {
- return label.title === labelName;
- }),
- );
- return labels.map((label) => label.key);
+ onSelect(value) {
+ this.setQuery({ key: LABEL_FILTER_PARAM, value });
+ trackSelectCheckbox(value);
},
},
FIRST_DROPDOWN_INDEX,
SEARCH_RESULTS_DESCRIPTION,
SEARCH_INPUT_DESCRIPTION,
- LABEL_FILTER_PARAM,
LABEL_FILTER_HEADER,
};
@@ -224,70 +140,40 @@ export default {
@close="onLabelClose"
/>
- {{
- $options.i18n.DESCRIBE_LABEL_FILTER_INPUT
- }}
-
-
- {{ $options.i18n.DESCRIBE_LABEL_FILTER }}
+
+
+ {{ $options.i18n.DESCRIBE_LABEL_FILTER_INPUT }}
-
-
-
+ data-testid="label-color-indicator"
+ class="dropdown-label-box gl-top-0 gl-mr-3 gl-shrink-0"
+ :style="{ 'background-color': item.color }"
+ >
+
+ {{ item.title }}
+
+
+