@trackunit/filters-filter-bar 1.3.151 → 1.3.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +317 -278
- package/index.esm.js +315 -281
- package/package.json +12 -11
- package/src/lib/components/DefaultFilterTypes.d.ts +4 -0
- package/src/lib/components/DynamicFilterList.d.ts +5 -1
- package/src/lib/components/FilterComponent.d.ts +47 -0
- package/src/lib/components/FilterHeader.d.ts +11 -5
- package/src/lib/components/FilterOptionsResults.d.ts +15 -0
- package/src/lib/components/FilterTableComponent.d.ts +23 -0
- package/src/lib/components/{StarredFilters.d.ts → FiltersMenu.d.ts} +5 -4
- package/src/lib/components/FiltersRenderer.d.ts +22 -0
- package/src/lib/components/GroupedFiltersList.d.ts +18 -0
- package/src/lib/components/ResetFiltersButton.d.ts +1 -6
- package/src/lib/components/index.d.ts +6 -1
- package/src/lib/hooks/useGroupFilters.d.ts +2 -2
- package/src/lib/types/FilterTypes.d.ts +1 -0
- package/src/lib/utils/FilterEvents.d.ts +3 -0
- package/src/translation.d.ts +2 -2
- package/src/lib/components/Filter.d.ts +0 -23
- package/src/lib/components/StarredFiltersMenu.d.ts +0 -29
package/index.esm.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-library-translation';
|
|
3
|
-
import { VirtualizedList, Text, Button, Popover, PopoverTrigger,
|
|
3
|
+
import { VirtualizedList, Text, Button, useViewportBreakpoints, Popover, PopoverTrigger, Tooltip, Icon, PopoverContent, Card, CardBody, MoreMenu, IconButton, MenuList } from '@trackunit/react-components';
|
|
4
4
|
import { useAnalytics, useTextSearch, useCurrentUser } from '@trackunit/react-core-hooks';
|
|
5
|
-
import { FilterBody, RadioFilterItem, CheckBoxFilterItem, FilterHeader as FilterHeader$1, FilterFooter, Filter
|
|
5
|
+
import { FilterBody, RadioFilterItem, CheckBoxFilterItem, FilterHeader as FilterHeader$1, FilterFooter, Filter } from '@trackunit/react-filter-components';
|
|
6
6
|
import { useRef, useMemo, useState, useEffect, useCallback, Fragment as Fragment$1 } from 'react';
|
|
7
7
|
import { capitalize } from 'string-ts';
|
|
8
8
|
import { createEvent } from '@trackunit/react-core-contexts-api';
|
|
9
|
-
import { Search, NumberField, RadioGroup
|
|
9
|
+
import { Search, NumberField, RadioGroup } from '@trackunit/react-form-components';
|
|
10
10
|
import { DayRangePicker } from '@trackunit/react-date-and-time-components';
|
|
11
11
|
import { nonNullable, capitalize as capitalize$1, objectValues, truthy, objectKeys } from '@trackunit/shared-utils';
|
|
12
|
-
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
13
12
|
import { twMerge } from 'tailwind-merge';
|
|
13
|
+
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
14
14
|
import { dequal } from 'dequal';
|
|
15
15
|
import isEqual from 'lodash/isEqual';
|
|
16
16
|
import { geoJsonPolygonSchema, geoJsonMultiPolygonSchema } from '@trackunit/geo-json-utils';
|
|
@@ -126,7 +126,7 @@ var defaultTranslations = {
|
|
|
126
126
|
"filtersBar.defaultMinMaxFilters.min": "Min",
|
|
127
127
|
"filtersBar.deselectAll": "Deselect All",
|
|
128
128
|
"filtersBar.emptyResults": "No results were found for your search term.",
|
|
129
|
-
"filtersBar.filtersHeading": "
|
|
129
|
+
"filtersBar.filtersHeading": "Filter",
|
|
130
130
|
"filtersBar.groups.ASSET": "Asset",
|
|
131
131
|
"filtersBar.groups.CUSTOM_FIELDS": "Custom Fields",
|
|
132
132
|
"filtersBar.groups.CUSTOMERS": "Customers",
|
|
@@ -145,10 +145,15 @@ var defaultTranslations = {
|
|
|
145
145
|
"filtersBar.loading": "Loading...",
|
|
146
146
|
"filtersBar.myNetworkGroup": "My Network",
|
|
147
147
|
"filtersBar.resetFilter": "Reset",
|
|
148
|
-
"filtersBar.resetFilters": "Reset
|
|
148
|
+
"filtersBar.resetFilters": "Reset all",
|
|
149
|
+
"filtersBar.searchFiltersPlaceholder": "Search filters...",
|
|
150
|
+
"filtersBar.searchPlaceholder": "Search...",
|
|
149
151
|
"filtersBar.selectAll": "Select All",
|
|
152
|
+
"filtersBar.showAll": "Show all",
|
|
150
153
|
"filtersBar.starredFilters.title": "Filters",
|
|
151
154
|
"filtersBar.starredFiltersTitle": "Starred Filters",
|
|
155
|
+
"filtersMenu.appliedFiltersLabel.plural": "{{count}} filters applied",
|
|
156
|
+
"filtersMenu.appliedFiltersLabel.singular": "1 filter applied",
|
|
152
157
|
"fleetlist.column.activity": "Activity",
|
|
153
158
|
"fleetlist.column.assetIds": "Asset Ids",
|
|
154
159
|
"fleetlist.column.assetType": "Asset Type",
|
|
@@ -327,6 +332,7 @@ const FilterEvents = {
|
|
|
327
332
|
"Starring Filter - Toggled": createEvent({
|
|
328
333
|
description: "A filter was starred or unstarred",
|
|
329
334
|
}),
|
|
335
|
+
"Showing Filters - All or Favorites": createEvent(),
|
|
330
336
|
};
|
|
331
337
|
|
|
332
338
|
/**
|
|
@@ -350,11 +356,11 @@ const toggleFilterValue = (value) => {
|
|
|
350
356
|
*
|
|
351
357
|
* @returns {ReactElement} - Returns the DynamicFilterList component.
|
|
352
358
|
*/
|
|
353
|
-
const DynamicFilterList = ({ rowCount, keyMapper, labelMapper, onChange, checked, count, showRequestMoreUseSearch = false, type, }) => {
|
|
359
|
+
const DynamicFilterList = ({ rowCount, keyMapper, labelMapper, onChange, checked, count, showRequestMoreUseSearch = false, type, className, }) => {
|
|
354
360
|
const [t] = useTranslation();
|
|
355
361
|
const parentRef = useRef(null);
|
|
356
362
|
const updatedRowCount = useMemo(() => (showRequestMoreUseSearch ? rowCount + 1 : rowCount), [rowCount, showRequestMoreUseSearch]);
|
|
357
|
-
return (jsx(FilterBody, { limitSize: true, ref: parentRef, children: jsx(VirtualizedList, { count: updatedRowCount, rowHeight: 40, separator: "space", children: index => {
|
|
363
|
+
return (jsx(FilterBody, { limitSize: true, ref: parentRef, children: jsx(VirtualizedList, { className: className, count: updatedRowCount, rowHeight: 40, separator: "space", children: index => {
|
|
358
364
|
return (jsx("div", { children: showRequestMoreUseSearch && index === rowCount ? (jsxs("div", { className: "p-3 pt-2", children: [jsx("span", { className: "text-sm text-gray-600", children: t("filter.more.options.if.you.search.title", { count: rowCount }) }), jsx("br", {}), jsx("span", { className: "text-sm italic text-gray-400", children: t("filter.more.options.if.you.search.description") })] })) : type === "Radio" ? (jsx(RadioFilterItem, { dataTestId: "dynamic-filter-radio-" + keyMapper(index), itemCount: count(index), label: labelMapper(index), selected: checked(index), value: keyMapper(index) }, keyMapper(index))) : (jsx(CheckBoxFilterItem, { checked: checked(index), dataTestId: "dynamic-filter-check-box-" + keyMapper(index), itemCount: count(index), label: labelMapper(index), name: keyMapper(index), onChange: () => {
|
|
359
365
|
onChange?.(index);
|
|
360
366
|
} }, keyMapper(index))) }));
|
|
@@ -366,17 +372,26 @@ const DynamicFilterList = ({ rowCount, keyMapper, labelMapper, onChange, checked
|
|
|
366
372
|
*
|
|
367
373
|
* @returns {ReactElement} - Returns the FilterHeader component.
|
|
368
374
|
*/
|
|
369
|
-
const FilterHeader = ({ filterKey, title, searchEnabled, searchProps, filterHasChanges, resetIndividualFilterToInitialState, onResetFilter, loading = false,
|
|
375
|
+
const FilterHeader = ({ filterKey, title, searchEnabled, searchProps, filterHasChanges, resetIndividualFilterToInitialState, onResetFilter, loading = false, className, dataTestId, hideHeader = false, children, }) => {
|
|
370
376
|
const [t] = useTranslation();
|
|
371
377
|
const handleResetFilter = () => {
|
|
372
378
|
resetIndividualFilterToInitialState(filterKey);
|
|
373
379
|
onResetFilter?.();
|
|
374
380
|
};
|
|
375
|
-
return (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
return (jsx(FilterHeader$1, { className: className, dataTestId: dataTestId ?? `${filterKey}-filter-header`, loading: loading, onReset: handleResetFilter, resetLabel: t("filtersBar.resetFilter"), searchComponent: searchEnabled ? jsx(FilterHeaderSearchComponent, { filterKey: filterKey, searchProps: searchProps }) : undefined, showReset: filterHasChanges, title: hideHeader ? undefined : title, children: children }));
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* FilterHeaderSearchComponent renders a search input for the FilterHeader component.
|
|
385
|
+
*/
|
|
386
|
+
const FilterHeaderSearchComponent = ({ searchProps, filterKey, }) => {
|
|
387
|
+
const [t] = useTranslation();
|
|
388
|
+
return (jsx(Search, { autoFocus: true, className: "w-full", fieldSize: "small", id: `${filterKey}-search`, onChange: e => searchProps.onChange(e.currentTarget.value), onClear: () => {
|
|
389
|
+
searchProps.onClear?.();
|
|
390
|
+
}, onKeyDown: e => {
|
|
391
|
+
if (e.key === "Enter" && searchProps.onEnter) {
|
|
392
|
+
searchProps.onEnter(searchProps.value);
|
|
393
|
+
}
|
|
394
|
+
}, placeholder: searchProps.placeholder ?? t("filtersBar.searchPlaceholder"), suffix: jsx(Text, { size: "small", subtle: true, children: searchProps.count }), value: searchProps.value }));
|
|
380
395
|
};
|
|
381
396
|
|
|
382
397
|
/**
|
|
@@ -396,7 +411,7 @@ const FilterResults = ({ results, ignoreUndefined, loading, children, }) => {
|
|
|
396
411
|
};
|
|
397
412
|
const EmptyResults = ({ loading }) => {
|
|
398
413
|
const [t] = useTranslation();
|
|
399
|
-
return (jsx("div", { className: "grid h-20 w-
|
|
414
|
+
return (jsx("div", { className: "grid h-20 w-[280px] place-content-center pl-2 pr-2", "data-testid": "empty-filter-results", children: jsx(Text, { align: "center", className: "w-full break-words", italicize: true, subtle: true, children: loading ? t("filtersBar.loading") : t("filtersBar.emptyResults") }) }));
|
|
400
415
|
};
|
|
401
416
|
|
|
402
417
|
/**
|
|
@@ -409,7 +424,6 @@ const DefaultCheckboxFilter = ({ filterDefinition, filterBarActions, options, lo
|
|
|
409
424
|
const { logEvent } = useAnalytics(FilterEvents);
|
|
410
425
|
const [hasSelectedAll, setHasSelectedAll] = useState(false);
|
|
411
426
|
const { t } = useTranslation();
|
|
412
|
-
const [selectedCount, setSelectedCount] = useState(0);
|
|
413
427
|
const handleSetValue = (value) => {
|
|
414
428
|
logEvent("Filters Applied - V2", {
|
|
415
429
|
type: filterName ?? `${capitalize(filterDefinition.filterKey)}Filter`,
|
|
@@ -474,19 +488,18 @@ const DefaultCheckboxFilter = ({ filterDefinition, filterBarActions, options, lo
|
|
|
474
488
|
useEffect(() => {
|
|
475
489
|
const values = filterBarActions.getValuesByKey(filterDefinition.filterKey);
|
|
476
490
|
if (Array.isArray(values)) {
|
|
477
|
-
|
|
478
|
-
if (values.length ===
|
|
479
|
-
setHasSelectedAll(
|
|
491
|
+
const optionsLength = undefinedCount ? options.length - 1 : options.length;
|
|
492
|
+
if (values.length === optionsLength) {
|
|
493
|
+
setHasSelectedAll(true);
|
|
480
494
|
}
|
|
481
495
|
else {
|
|
482
|
-
setHasSelectedAll(
|
|
496
|
+
setHasSelectedAll(false);
|
|
483
497
|
}
|
|
484
498
|
}
|
|
485
499
|
else {
|
|
486
|
-
setSelectedCount(0);
|
|
487
500
|
setHasSelectedAll(true);
|
|
488
501
|
}
|
|
489
|
-
}, [filterBarActions, filterDefinition.filterKey, undefinedCount]);
|
|
502
|
+
}, [filterBarActions, filterDefinition.filterKey, options.length, undefinedCount]);
|
|
490
503
|
const handleSelectAll = () => {
|
|
491
504
|
if (hasSelectedAll) {
|
|
492
505
|
setMultipleValues([]);
|
|
@@ -504,19 +517,20 @@ const DefaultCheckboxFilter = ({ filterDefinition, filterBarActions, options, lo
|
|
|
504
517
|
return (jsxs(Fragment, { children: [jsx(FilterHeader, { ...filterDefinition, ...filterBarActions, filterHasChanges: filterBarActions.appliedFilterKeys().includes(filterDefinition.filterKey), loading: loading, searchEnabled: true, searchProps: {
|
|
505
518
|
value: customSearch?.value ?? searchText,
|
|
506
519
|
onChange: customSearch?.onChange ?? setSearchText,
|
|
520
|
+
onClear: customSearch?.onClear ?? (() => setSearchText("")),
|
|
507
521
|
count: undefinedCount ? filteredOptions.length - 1 : filteredOptions.length,
|
|
508
|
-
}
|
|
522
|
+
}, children: options.length >= 2 ? (jsx(Button, { className: "place-self-start", dataTestId: "selectAllButton", disabled: hasSelectedAll, onClick: handleSelectAll, size: "small", variant: "ghost", children: t("filtersBar.selectAll") })) : null }), jsx(FilterResults, { ignoreUndefined: undefinedCount !== null, loading: loading, results: results, children: res => (jsx(DynamicFilterList, { checked: index => {
|
|
509
523
|
return filterDefinition.type === "valueNameArray"
|
|
510
524
|
? filterBarActions.objectArrayIncludesValue(filterDefinition.filterKey, res[index]?.key || "")
|
|
511
525
|
: filterBarActions.arrayIncludesValue(filterDefinition.filterKey, res[index]?.key || "");
|
|
512
|
-
}, count: index => res[index]?.count, keyMapper: index => res[index]?.key || "", labelMapper: index => res[index]?.label || "", onChange: index => {
|
|
526
|
+
}, className: "mb-1", count: index => res[index]?.count, keyMapper: index => res[index]?.key || "", labelMapper: index => res[index]?.label || "", onChange: index => {
|
|
513
527
|
const result = res[index];
|
|
514
528
|
if (result) {
|
|
515
529
|
handleSetValue(result);
|
|
516
530
|
}
|
|
517
531
|
}, rowCount: undefinedCount ? res.length - 1 : res.length, showRequestMoreUseSearch: showRequestMoreUseSearch, type: "CheckBox" })) }), showUndefinedOptionWithCountAtBottom && undefinedCount ? (jsx("div", { className: "m-1", children: jsx(CheckBoxFilterItem, { checked: filterDefinition.type === "valueNameArray"
|
|
518
532
|
? filterBarActions.objectArrayIncludesValue(filterDefinition.filterKey, results[undefinedCount.index]?.key || "")
|
|
519
|
-
: filterBarActions.arrayIncludesValue(filterDefinition.filterKey, results[undefinedCount.index]?.key || ""),
|
|
533
|
+
: filterBarActions.arrayIncludesValue(filterDefinition.filterKey, results[undefinedCount.index]?.key || ""), dataTestId: "dynamic-filter-check-box-undefined", itemCount: undefinedCount.count, label: results[undefinedCount.index]?.label, name: "dynamic-filter-check-box-undefined", onChange: () => {
|
|
520
534
|
const result = results[undefinedCount.index];
|
|
521
535
|
if (result) {
|
|
522
536
|
handleSetValue(result);
|
|
@@ -621,10 +635,282 @@ const DefaultRadioFilter = ({ filterDefinition, filterBarActions, options, loadi
|
|
|
621
635
|
return (jsxs(Fragment, { children: [jsx(FilterHeader, { ...filterBarActions, ...filterDefinition, filterHasChanges: filterBarActions.appliedFilterKeys().includes(filterDefinition.filterKey), loading: loading, searchEnabled: true, searchProps: {
|
|
622
636
|
value: customSearch?.value ?? searchText,
|
|
623
637
|
onChange: customSearch?.onChange ?? setSearchText,
|
|
638
|
+
onClear: customSearch?.onClear ?? (() => setSearchText("")),
|
|
624
639
|
count: filteredOptions.length,
|
|
625
640
|
} }), jsx(FilterResults, { loading: loading, results: customSearch ? options : filteredOptions, children: res => (jsx(RadioGroup, { id: "DefaultRadioFilter", onChange: e => {
|
|
626
641
|
handleClick(e.currentTarget.value);
|
|
627
|
-
}, value: selectedRadioId?.key || "", children: jsx(DynamicFilterList, { checked: index => filterBarActions.objectIncludesValue(filterDefinition.filterKey, res[index]?.key || ""), count: index => res[index]?.count, keyMapper: index => res[index]?.key || "", labelMapper: index => res[index]?.label || "", rowCount: res.length, showRequestMoreUseSearch: showRequestMoreUseSearch, type: "Radio" }) })) })] }));
|
|
642
|
+
}, value: selectedRadioId?.key || "", children: jsx(DynamicFilterList, { checked: index => filterBarActions.objectIncludesValue(filterDefinition.filterKey, res[index]?.key || ""), className: "m-1 mt-0", count: index => res[index]?.count, keyMapper: index => res[index]?.key || "", labelMapper: index => res[index]?.label || "", rowCount: res.length, showRequestMoreUseSearch: showRequestMoreUseSearch, type: "Radio" }) })) })] }));
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Returns the two first values, appends counter if more.
|
|
647
|
+
*
|
|
648
|
+
* @param {string[]} [input] Array of values to reduce
|
|
649
|
+
* @returns {*} {string}
|
|
650
|
+
*/
|
|
651
|
+
const reduceFilterText = (input) => {
|
|
652
|
+
if (!Array.isArray(input)) {
|
|
653
|
+
return input;
|
|
654
|
+
}
|
|
655
|
+
// Only first two elements
|
|
656
|
+
const firstTwo = input.slice(0, 2);
|
|
657
|
+
const remainder = input.slice(2, input.length).length;
|
|
658
|
+
const firstTwoJoined = firstTwo.join(", ");
|
|
659
|
+
const remainderStr = remainder > 0 ? `, +${remainder}` : "";
|
|
660
|
+
return firstTwoJoined + remainderStr;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Filter is a React component that renders a filter element based on the provided filter definition and state.
|
|
665
|
+
*
|
|
666
|
+
* @returns {ReactElement} - Returns the Filter component.
|
|
667
|
+
*/
|
|
668
|
+
const FilterComponent = ({ filter, filterBarActions, filterState, readOnly = false, visualStyle = "button", className, asIcon, size, }) => {
|
|
669
|
+
const values = filterBarActions.getValuesByKey(filter.filterKey);
|
|
670
|
+
const valuesLength = values && Array.isArray(values) ? values.length : undefined;
|
|
671
|
+
const getFilterText = () => {
|
|
672
|
+
if (!values) {
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
if (filter.valueAsText) {
|
|
676
|
+
return reduceFilterText(filter.valueAsText(values));
|
|
677
|
+
}
|
|
678
|
+
else if (filter.type === "valueNameArray") {
|
|
679
|
+
return reduceFilterText(values.map(value => value.name));
|
|
680
|
+
}
|
|
681
|
+
else if (filter.type === "valueName") {
|
|
682
|
+
return values.name;
|
|
683
|
+
}
|
|
684
|
+
return values.toString();
|
|
685
|
+
};
|
|
686
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
687
|
+
const setValue = (callback) => {
|
|
688
|
+
const newValue = callback(filterBarActions.getValuesByKey(filter.filterKey));
|
|
689
|
+
if (filter.type === "string") {
|
|
690
|
+
filterBarActions.setStringValue(filter.filterKey, newValue);
|
|
691
|
+
}
|
|
692
|
+
else if (filter.type === "stringArray") {
|
|
693
|
+
filterBarActions.setArrayValue(filter.filterKey, newValue);
|
|
694
|
+
}
|
|
695
|
+
else if (filter.type === "dateRange") {
|
|
696
|
+
filterBarActions.setDateRange(filter.filterKey, newValue);
|
|
697
|
+
}
|
|
698
|
+
else if (filter.type === "area") {
|
|
699
|
+
filterBarActions.setArea(newValue);
|
|
700
|
+
}
|
|
701
|
+
else if (filter.type === "valueNameArray") {
|
|
702
|
+
filterBarActions.setArrayObjectValue(filter.filterKey, newValue);
|
|
703
|
+
}
|
|
704
|
+
else if (filter.type === "valueName") {
|
|
705
|
+
filterBarActions.setObjectValue(filter.filterKey, newValue);
|
|
706
|
+
}
|
|
707
|
+
else if (filter.type === "minMax") {
|
|
708
|
+
filterBarActions.setMinMaxValue(filter.filterKey, newValue);
|
|
709
|
+
}
|
|
710
|
+
else if (filter.type === "boolean") {
|
|
711
|
+
filterBarActions.setBooleanValue(filter.filterKey, newValue);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
filterBarActions.setNumberValue(filter.filterKey, newValue);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const text = getFilterText();
|
|
718
|
+
const activeFilterText = Array.isArray(text) ? text.join(", ") : text;
|
|
719
|
+
const showDirectly = filter.showDirectly || false;
|
|
720
|
+
return showDirectly ? (jsx(Fragment, { children: filter.component({
|
|
721
|
+
filterDefinition: filter,
|
|
722
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
723
|
+
value: values,
|
|
724
|
+
setValue,
|
|
725
|
+
filterBarActions,
|
|
726
|
+
filterState,
|
|
727
|
+
}) })) : (jsx(Filter, { activeLabel: activeFilterText, activeOptionsCount: valuesLength, asIcon: asIcon, className: className, dataTestId: `${filter.filterKey}-filter-button`, isActive: filterBarActions.appliedFilterKeys().includes(filter.filterKey), popoverProps: { placement: visualStyle === "list-item" ? "right-start" : "bottom-start" }, readOnly: readOnly, size: size, title: filter.title, visualStyle: visualStyle, withStickyHeader: true, children: filter.component({
|
|
728
|
+
filterDefinition: filter,
|
|
729
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
730
|
+
value: values,
|
|
731
|
+
setValue,
|
|
732
|
+
filterBarActions,
|
|
733
|
+
filterState,
|
|
734
|
+
}) }));
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Custom React hook for grouping and filtering a list of filters.
|
|
739
|
+
*
|
|
740
|
+
* @param {FilterDefinition[]} filterDefinitions - The list of filter definitions to group and filter.
|
|
741
|
+
* @param {string[]} hiddenFilters - An array of filter keys representing filters to be hidden.
|
|
742
|
+
* @returns {UseGroupFiltersReturnType} An object containing the grouped filters.
|
|
743
|
+
*/
|
|
744
|
+
const useGroupFilters = (filterDefinitions, hiddenFilters) => {
|
|
745
|
+
const [t] = useTranslation();
|
|
746
|
+
const filtersGrouped = useMemo(() => {
|
|
747
|
+
const keys = uniqueKeysFromGroups(filterDefinitions);
|
|
748
|
+
return keys
|
|
749
|
+
.map(key => ({
|
|
750
|
+
key,
|
|
751
|
+
title: t(`filtersBar.groups.${key}`),
|
|
752
|
+
filters: filterDefinitions.filter(rawFilter => rawFilter.group === key && hiddenFilters.includes(rawFilter.filterKey) === false),
|
|
753
|
+
}))
|
|
754
|
+
.filter(filter => filter.filters.length > 0);
|
|
755
|
+
}, [filterDefinitions, hiddenFilters, t]);
|
|
756
|
+
return useMemo(() => ({ filtersGrouped }), [filtersGrouped]);
|
|
757
|
+
};
|
|
758
|
+
const uniqueKeysFromGroups = (filters) => [...new Set(filters.map(filter => filter.group))];
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* FiltersRenderer renders an array of Filter components from filter definitions
|
|
762
|
+
* It ignores hidden filters.
|
|
763
|
+
*
|
|
764
|
+
* Note: This component does not provide any styling or layout - use a wrapper for proper list styling.
|
|
765
|
+
*
|
|
766
|
+
* @param {object} props - The component props
|
|
767
|
+
* @param {FilterDefinition[]} props.filters - Array of filter definitions to render
|
|
768
|
+
* @param {FilterBarConfig & FilterMapActions & FilterMapGetter} props.filterBarConfig - The filter bar configuration
|
|
769
|
+
* @param {ComponentProps<typeof FilterComponent>["visualStyle"]} [props.visualStyle] - The visual style of the filters
|
|
770
|
+
* @returns {ReactElement[] | null} - Returns an array of Filter components or null
|
|
771
|
+
*/
|
|
772
|
+
const FiltersRenderer = ({ filters, filterBarConfig, visualStyle, }) => {
|
|
773
|
+
return filters.length === 0
|
|
774
|
+
? null
|
|
775
|
+
: filters
|
|
776
|
+
.filter(filter => !filter.isHidden)
|
|
777
|
+
.map(filter => (jsx(FilterComponent, { filter: filter, filterBarActions: filterBarConfig, filterState: { values: filterBarConfig.values, setters: filterBarConfig.setters }, visualStyle: visualStyle }, `filter-${filter.filterKey}`)));
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* FiltersList is a React component that displays a list of filters within a filter bar.
|
|
782
|
+
*
|
|
783
|
+
* @returns {ReactElement} - Returns the FiltersList component.
|
|
784
|
+
*/
|
|
785
|
+
const GroupedFiltersList = ({ filterBarConfig, filtersGrouped, className, dataTestId = "grouped-filters-list", }) => {
|
|
786
|
+
return (jsx("div", { className: className, "data-testid": dataTestId, role: "menu", children: filtersGrouped.map((group, idx) => {
|
|
787
|
+
const isLastGroup = idx === filtersGrouped.length - 1;
|
|
788
|
+
return (jsxs("div", { className: "flex flex-col gap-1", children: [jsxs("div", { children: [jsx(Text, { className: "text-secondary-400 h-7 px-3 py-2", dataTestId: `${group.key}-group-title`, size: "small", uppercase: true, weight: "bold", children: group.title }), jsx("ul", { "aria-labelledby": `${group.key}-group-title`, className: "grid", "data-testid": `${group.key}-group-list`, children: jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: group.filters, visualStyle: "list-item" }) })] }), isLastGroup ? null : jsx("div", { className: "bg-secondary-200 h-[1px] w-full", role: "separator" })] }, group.key));
|
|
789
|
+
}) }));
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* ResetFiltersButton is a React component that provides a button for resetting filters.
|
|
794
|
+
*
|
|
795
|
+
* @returns {ReactElement | null} The rendered ResetFiltersButton component, or null if no filters have been applied.
|
|
796
|
+
*/
|
|
797
|
+
const ResetFiltersButton = ({ resetFiltersToInitialState, dataTestId, className, }) => {
|
|
798
|
+
const [t] = useTranslation();
|
|
799
|
+
return (jsxs(Button, { className: className, dataTestId: dataTestId ?? "reset-filters-button", onClick: () => {
|
|
800
|
+
resetFiltersToInitialState();
|
|
801
|
+
}, size: "small", variant: "ghost", children: [t("filtersBar.resetFilters"), jsx("span", { className: "sr-only", children: "Resets all applied filters" })] }));
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* FilterMenu is a React component that displays a list of filters in a popover menu based on the provided filter bar configuration.
|
|
806
|
+
*
|
|
807
|
+
* @template TFilterBarDefinition - The type representing the filter bar definition.
|
|
808
|
+
* @returns {ReactElement} - Returns the FilterMenu component.
|
|
809
|
+
*/
|
|
810
|
+
const FiltersMenu = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], compact, title, dataTestId = "filters-menu", className, }) => {
|
|
811
|
+
const [t] = useTranslation();
|
|
812
|
+
const { isSm } = useViewportBreakpoints();
|
|
813
|
+
const [showCustomFilters, setShowCustomFilters] = useState(false);
|
|
814
|
+
// TODO: Add analytics if event requirements are defined
|
|
815
|
+
// const { logEvent } = useAnalytics(FilterEvents);
|
|
816
|
+
const hideInMenu = useMemo(() => {
|
|
817
|
+
return objectValues(filterBarDefinition)
|
|
818
|
+
.map(filter => {
|
|
819
|
+
const showInFilterBar = filter.showInFilterBar ? filter.showInFilterBar() : true;
|
|
820
|
+
const showInStarredMenu = filter.showInStarredMenu ? filter.showInStarredMenu() : true; // TODO: Starred menu concept should be completely removed everywhere
|
|
821
|
+
const showMenuAnywayBecauseFilterHasChanged = filterBarConfig.appliedFilterKeys().includes(filter.filterKey);
|
|
822
|
+
return (!showInFilterBar || !showInStarredMenu) && !showMenuAnywayBecauseFilterHasChanged
|
|
823
|
+
? filter.filterKey
|
|
824
|
+
: null;
|
|
825
|
+
})
|
|
826
|
+
.filter(truthy);
|
|
827
|
+
}, [filterBarConfig, filterBarDefinition]);
|
|
828
|
+
const { filtersGrouped } = useGroupFilters(objectValues(filterBarDefinition), [...hideInMenu, ...hiddenFilters]);
|
|
829
|
+
const { appliedFilters, filtersToShow, showDirectlyFilters, hasCustomFields } = useMemo(() => {
|
|
830
|
+
const allFilters = filtersGrouped.map(group => group.filters).flat();
|
|
831
|
+
return {
|
|
832
|
+
appliedFilters: allFilters.filter(filter => filterBarConfig.appliedFilterKeys().includes(filter.filterKey)),
|
|
833
|
+
filtersToShow: allFilters.filter(filter => !filter.showDirectly),
|
|
834
|
+
showDirectlyFilters: allFilters.filter(filter => filter.showDirectly),
|
|
835
|
+
hasCustomFields: allFilters.some(filter => filter.group === "CUSTOM_FIELDS"),
|
|
836
|
+
};
|
|
837
|
+
}, [filterBarConfig, filtersGrouped]);
|
|
838
|
+
const [searchResults, searchText, setSearchText] = useTextSearch(filtersToShow, item => [item.title]);
|
|
839
|
+
const { filtersGrouped: searchResultsGrouped } = useGroupFilters(searchResults, []);
|
|
840
|
+
const { filtersGrouped: filtersToShowGrouped } = useGroupFilters(filtersToShow, []);
|
|
841
|
+
const appliedCustomFields = useMemo(() => appliedFilters.filter(filter => filter.group === "CUSTOM_FIELDS"), [appliedFilters]);
|
|
842
|
+
return (jsxs("div", { className: twMerge("flex items-center gap-2", className), "data-testid": dataTestId, children: [jsx(Popover, { onOpenStateChange: open => {
|
|
843
|
+
if (!open) {
|
|
844
|
+
setShowCustomFilters(false);
|
|
845
|
+
setSearchText("");
|
|
846
|
+
}
|
|
847
|
+
}, placement: "bottom-start", children: modalState => {
|
|
848
|
+
return (jsxs(Fragment, { children: [jsx(PopoverTrigger, { children: jsx("div", { "data-testid": "starred-filters-menu-trigger", id: "starred-filters-menu-trigger", children: jsx(Tooltip, { disabled: !compact || modalState.isOpen, label: jsx(FilterButtonTooltipLabel, { filterBarConfig: filterBarConfig }), children: jsx(Button, { prefix: jsx(Icon, { ariaHidden: true, color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Filter", size: "small" }), size: "small", suffix: compact && filterBarConfig.appliedFilterKeys().length > 0 && isSm ? (jsxs("div", { children: [jsxs("span", { "aria-hidden": true, children: ["(", filterBarConfig.appliedFilterKeys().length, ")"] }), jsxs("span", { className: "sr-only", children: [filterBarConfig.appliedFilterKeys().length, " filters applied"] })] })) : undefined, variant: "secondary", children: jsx("span", { className: "hidden sm:block", children: title ?? t("filtersBar.filtersHeading") }) }) }) }) }), jsx(PopoverContent, { cellPadding: 100, children: jsxs(Card, { className: "max-h-[min(600px,_calc(100dvh-32px))] w-[300px] overflow-y-hidden", dataTestId: "starred-filters-menu-popover", children: [jsxs("div", { className: "flex flex-col gap-1 p-1", children: [jsx(Search, { autoFocus: true, dataTestId: "starred-filters-menu-search", fieldSize: "small", id: "search-filters-list", onChange: e => setSearchText(e.currentTarget.value), onClear: () => setSearchText(""), placeholder: t("filtersBar.searchFiltersPlaceholder"), value: searchText }), jsxs("div", { className: "flex h-7 items-center justify-between gap-1 px-3", children: [jsx(Text, { className: "text-secondary-400", size: "small", children: jsx(FiltersAppliedCountLabel, { filterBarConfig: filterBarConfig }) }), filterBarConfig.appliedFilterKeys().length > 0 ? (jsx(ResetFiltersButton, { resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })) : null] })] }), jsx(Separator, {}), jsxs(CardBody, { className: "gap-1 p-1", density: "none", disableGap: true, children: [jsx(GroupedFiltersList, { className: "flex flex-col gap-1", filterBarConfig: filterBarConfig, filtersGrouped: searchText
|
|
849
|
+
? searchResultsGrouped
|
|
850
|
+
: showCustomFilters
|
|
851
|
+
? filtersToShowGrouped
|
|
852
|
+
: removeCustomFields(filtersToShowGrouped) }), hasCustomFields && !showCustomFilters && !searchText ? (jsx(CustomFieldsHiddenGroup, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, onShow: () => {
|
|
853
|
+
setShowCustomFilters(true);
|
|
854
|
+
} })) : null] })] }) })] }));
|
|
855
|
+
} }), showDirectlyFilters.length > 0 ? (jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: showDirectlyFilters })) : null, !compact ? (jsxs(Fragment, { children: [appliedFilters.filter(filter => !filter.showDirectly).length > 0 ? (jsx("div", { className: "h-4 w-[1px] bg-slate-300", "data-testid": "applied-filters-buttons" })) : null, jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: appliedFilters }), filterBarConfig.appliedFilterKeys().length > 0 ? (jsx(ResetFiltersButton, { resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })) : null] })) : null] }));
|
|
856
|
+
};
|
|
857
|
+
const Separator = () => jsx("hr", { className: "border-secondary-200", role: "separator" });
|
|
858
|
+
const FilterButtonTooltipLabel = ({ filterBarConfig, }) => {
|
|
859
|
+
const [t] = useTranslation();
|
|
860
|
+
switch (filterBarConfig.appliedFilterKeys().length) {
|
|
861
|
+
case 0:
|
|
862
|
+
return t("filtersBar.appliedFiltersTooltip.none");
|
|
863
|
+
case 1:
|
|
864
|
+
return filterBarConfig.appliedFilterKeys()[0]
|
|
865
|
+
? t("filtersBar.appliedFiltersTooltip.singular", {
|
|
866
|
+
filterName: filterBarConfig.getFilterTitle(filterBarConfig.appliedFilterKeys()[0]),
|
|
867
|
+
})
|
|
868
|
+
: null; // should never happen though
|
|
869
|
+
default:
|
|
870
|
+
return (jsxs(Fragment, { children: [t("filtersBar.appliedFiltersTooltip.plural", { count: filterBarConfig.appliedFilterKeys().length }), jsx("ul", { className: "list-inside", children: filterBarConfig.appliedFilterKeys().map((appliedFilterKey, index) => (jsx("li", { className: "list-disc", children: filterBarConfig.getFilterTitle(appliedFilterKey) }, index))) })] }));
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
const FiltersAppliedCountLabel = ({ filterBarConfig, }) => {
|
|
874
|
+
const [t] = useTranslation();
|
|
875
|
+
switch (filterBarConfig.appliedFilterKeys().length) {
|
|
876
|
+
case 0:
|
|
877
|
+
return t("filtersBar.appliedFiltersTooltip.none");
|
|
878
|
+
case 1:
|
|
879
|
+
return filterBarConfig.appliedFilterKeys()[0] ? t("filtersMenu.appliedFiltersLabel.singular") : null;
|
|
880
|
+
default:
|
|
881
|
+
return jsx(Fragment, { children: t("filtersMenu.appliedFiltersLabel.plural", { count: filterBarConfig.appliedFilterKeys().length }) });
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
const CustomFieldsHiddenGroup = ({ appliedCustomFields, filterBarConfig, onShow, }) => {
|
|
885
|
+
const [t] = useTranslation();
|
|
886
|
+
return (jsxs(Fragment, { children: [jsx(Separator, {}), jsxs("div", { children: [jsx(Button, { "aria-controls": "filters-list", className: "text-primary-600 w-full justify-between px-3", onClick: onShow, prefix: jsx(Text, { className: "text-secondary-400", size: "small", uppercase: true, weight: "bold", children: t("filtersBar.groups.CUSTOM_FIELDS") }), size: "small", variant: "ghost-neutral", children: t("filtersBar.showAll") }), appliedCustomFields.length > 0 ? (jsx("ul", { "aria-label": "Visible custom fields", "data-testid": "applied-custom-fields-list", children: jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: appliedCustomFields, visualStyle: "list-item" }) })) : null] })] }));
|
|
887
|
+
};
|
|
888
|
+
const removeCustomFields = (filtersGrouped) => filtersGrouped.filter(group => group.key !== "CUSTOM_FIELDS");
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Filter is a React component that renders a filter element based on the provided filter definition and state.
|
|
892
|
+
*
|
|
893
|
+
* @returns {ReactElement} - Returns the Filter component.
|
|
894
|
+
*/
|
|
895
|
+
const FilterTableComponent = ({ filterKey, filterBarDefinition, filterBarConfig, }) => {
|
|
896
|
+
const ensureFilterKey = useCallback((key) => {
|
|
897
|
+
// eslint-disable-next-line local-rules/no-typescript-assertion
|
|
898
|
+
return key;
|
|
899
|
+
}, []);
|
|
900
|
+
const filters = useMemo(() => {
|
|
901
|
+
if (Array.isArray(filterKey)) {
|
|
902
|
+
return filterKey.map(key => filterBarDefinition[ensureFilterKey(key)]).filter(Boolean);
|
|
903
|
+
}
|
|
904
|
+
return [];
|
|
905
|
+
}, [filterKey, filterBarDefinition, ensureFilterKey]);
|
|
906
|
+
if (Array.isArray(filterKey)) {
|
|
907
|
+
return (jsx(MoreMenu, { customButton: jsx(IconButton, { icon: jsx(Icon, { color: filterKey.some(key => filterBarConfig.appliedFilterKeys().includes(key)) ? "primary" : undefined, name: "Filter", size: "small" }), onClick: event => event.stopPropagation(), size: "extraSmall", variant: "ghost-neutral" }), children: () => (jsx(MenuList, { children: filters.map((value, index) => value && (jsx(FilterComponent, { filter: value, filterBarActions: filterBarConfig, filterState: filterBarConfig, visualStyle: "list-item" }, index))) })) }));
|
|
908
|
+
}
|
|
909
|
+
const filter = filterBarDefinition[ensureFilterKey(filterKey)];
|
|
910
|
+
if (!filter) {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
return (jsx("div", { onClick: event => event.stopPropagation(), children: jsx(FilterComponent, { asIcon: "Filter", filter: filter, filterBarActions: filterBarConfig, filterState: filterBarConfig, size: "extraSmall" }) }));
|
|
628
914
|
};
|
|
629
915
|
|
|
630
916
|
/**
|
|
@@ -852,263 +1138,11 @@ const HierarchicalCheckboxFilter = ({ filterDefinition, filterBarActions, option
|
|
|
852
1138
|
return (jsxs(Fragment, { children: [jsx(FilterHeader, { ...filterDefinition, ...filterBarActions, filterHasChanges: filterBarActions.appliedFilterKeys().includes(filterDefinition.filterKey), loading: loading, ...searchHeaderProps }), jsx("div", { className: "max-h-60 overflow-y-auto", children: jsx(FilterResults, { loading: loading, results: filteredOptions, children: results => (jsx(Fragment, { children: results.map(option => (jsx(RenderHierarchicalOption, { cascadeSelection: cascadeSelection, filterBarActions: filterBarActions, filterDefinition: filterDefinition, logEvent: logEvent, option: option, optionsMap: optionsMap, selectedValues: selectedValues, setValue: setValue }, option.key))) })) }) })] }));
|
|
853
1139
|
};
|
|
854
1140
|
|
|
855
|
-
/**
|
|
856
|
-
* Custom React hook for grouping and filtering a list of filters.
|
|
857
|
-
*
|
|
858
|
-
* @param {FilterDefinition[]} filterDefinitions - The list of filter definitions to group and filter.
|
|
859
|
-
* @param {string[]} hiddenFilters - An array of filter keys representing filters to be hidden.
|
|
860
|
-
* @returns {UseGroupFiltersReturnType} An object containing the grouped filters.
|
|
861
|
-
*/
|
|
862
|
-
const useStarredGroupFilters = (filterDefinitions, hiddenFilters) => {
|
|
863
|
-
const [t] = useTranslation();
|
|
864
|
-
const filtersGrouped = useMemo(() => {
|
|
865
|
-
const keys = uniqueKeysFromGroups(filterDefinitions);
|
|
866
|
-
return keys
|
|
867
|
-
.map(key => ({
|
|
868
|
-
key,
|
|
869
|
-
title: t(`filtersBar.groups.${key}`),
|
|
870
|
-
filters: filterDefinitions.filter(rawFilter => rawFilter.group === key && hiddenFilters.includes(rawFilter.filterKey) === false),
|
|
871
|
-
}))
|
|
872
|
-
.filter(filter => filter.filters.length > 0);
|
|
873
|
-
}, [filterDefinitions, hiddenFilters, t]);
|
|
874
|
-
return useMemo(() => ({ filtersGrouped }), [filtersGrouped]);
|
|
875
|
-
};
|
|
876
|
-
const uniqueKeysFromGroups = (filters) => [...new Set(filters.map(filter => filter.group))];
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* Returns the two first values, appends counter if more.
|
|
880
|
-
*
|
|
881
|
-
* @param {string[]} [input] Array of values to reduce
|
|
882
|
-
* @returns {*} {string}
|
|
883
|
-
*/
|
|
884
|
-
const reduceFilterText = (input) => {
|
|
885
|
-
if (!Array.isArray(input)) {
|
|
886
|
-
return input;
|
|
887
|
-
}
|
|
888
|
-
// Only first two elements
|
|
889
|
-
const firstTwo = input.slice(0, 2);
|
|
890
|
-
const remainder = input.slice(2, input.length).length;
|
|
891
|
-
const firstTwoJoined = firstTwo.join(", ");
|
|
892
|
-
const remainderStr = remainder > 0 ? `, +${remainder}` : "";
|
|
893
|
-
return firstTwoJoined + remainderStr;
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Filter is a React component that renders a filter element based on the provided filter definition and state.
|
|
898
|
-
*
|
|
899
|
-
* @returns {ReactElement} - Returns the Filter component.
|
|
900
|
-
*/
|
|
901
|
-
const Filter = ({ filter, filterBarActions, filterState, }) => {
|
|
902
|
-
const values = filterBarActions.getValuesByKey(filter.filterKey);
|
|
903
|
-
const getFilterText = () => {
|
|
904
|
-
if (!values) {
|
|
905
|
-
return undefined;
|
|
906
|
-
}
|
|
907
|
-
if (filter.valueAsText) {
|
|
908
|
-
return reduceFilterText(filter.valueAsText(values));
|
|
909
|
-
}
|
|
910
|
-
else if (filter.type === "valueNameArray") {
|
|
911
|
-
return reduceFilterText(values.map(value => value.name));
|
|
912
|
-
}
|
|
913
|
-
else if (filter.type === "valueName") {
|
|
914
|
-
return values.name;
|
|
915
|
-
}
|
|
916
|
-
return values.toString();
|
|
917
|
-
};
|
|
918
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
919
|
-
const setValue = (callback) => {
|
|
920
|
-
const newValue = callback(filterBarActions.getValuesByKey(filter.filterKey));
|
|
921
|
-
if (filter.type === "string") {
|
|
922
|
-
filterBarActions.setStringValue(filter.filterKey, newValue);
|
|
923
|
-
}
|
|
924
|
-
else if (filter.type === "stringArray") {
|
|
925
|
-
filterBarActions.setArrayValue(filter.filterKey, newValue);
|
|
926
|
-
}
|
|
927
|
-
else if (filter.type === "dateRange") {
|
|
928
|
-
filterBarActions.setDateRange(filter.filterKey, newValue);
|
|
929
|
-
}
|
|
930
|
-
else if (filter.type === "area") {
|
|
931
|
-
filterBarActions.setArea(newValue);
|
|
932
|
-
}
|
|
933
|
-
else if (filter.type === "valueNameArray") {
|
|
934
|
-
filterBarActions.setArrayObjectValue(filter.filterKey, newValue);
|
|
935
|
-
}
|
|
936
|
-
else if (filter.type === "valueName") {
|
|
937
|
-
filterBarActions.setObjectValue(filter.filterKey, newValue);
|
|
938
|
-
}
|
|
939
|
-
else if (filter.type === "minMax") {
|
|
940
|
-
filterBarActions.setMinMaxValue(filter.filterKey, newValue);
|
|
941
|
-
}
|
|
942
|
-
else if (filter.type === "boolean") {
|
|
943
|
-
filterBarActions.setBooleanValue(filter.filterKey, newValue);
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
filterBarActions.setNumberValue(filter.filterKey, newValue);
|
|
947
|
-
}
|
|
948
|
-
};
|
|
949
|
-
const text = getFilterText();
|
|
950
|
-
const activeFilterText = Array.isArray(text) ? text.join(", ") : text;
|
|
951
|
-
const showDirectly = filter.showDirectly || false;
|
|
952
|
-
return showDirectly ? (jsx(Fragment, { children: filter.component({
|
|
953
|
-
filterDefinition: filter,
|
|
954
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
955
|
-
value: values,
|
|
956
|
-
setValue,
|
|
957
|
-
filterBarActions,
|
|
958
|
-
filterState,
|
|
959
|
-
}) })) : (jsx(Filter$1, { activeLabel: activeFilterText, dataTestId: `${filter.filterKey}-filter-button`, isActive: Boolean(text?.length), popoverProps: { placement: "right-start" }, title: filter.title, withStickyHeader: true, children: filter.component({
|
|
960
|
-
filterDefinition: filter,
|
|
961
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
962
|
-
value: values,
|
|
963
|
-
setValue,
|
|
964
|
-
filterBarActions,
|
|
965
|
-
filterState,
|
|
966
|
-
}) }));
|
|
967
|
-
};
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* ResetFiltersButton is a React component that provides a button for resetting filters.
|
|
971
|
-
*
|
|
972
|
-
* @returns {ReactElement | null} The rendered ResetFiltersButton component, or null if no filters have been applied.
|
|
973
|
-
*/
|
|
974
|
-
const ResetFiltersButton = ({ filtersHaveBeenApplied, resetFiltersToInitialState, dataTestId, className, }) => {
|
|
975
|
-
const [t] = useTranslation();
|
|
976
|
-
if (!filtersHaveBeenApplied) {
|
|
977
|
-
return null;
|
|
978
|
-
}
|
|
979
|
-
return (jsx(Button, { className: className, dataTestId: dataTestId ?? "reset-filters-button", onClick: () => {
|
|
980
|
-
resetFiltersToInitialState();
|
|
981
|
-
}, size: "small", variant: "ghost-neutral", children: t("filtersBar.resetFilters") }));
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
/**
|
|
985
|
-
* StarredFiltersMenu is a React component that displays a menu of starred filters within a filter bar.
|
|
986
|
-
*
|
|
987
|
-
* @returns {ReactElement} - Returns the StarredFiltersMenu component.
|
|
988
|
-
*/
|
|
989
|
-
const StarredFiltersMenu = ({ filterBarDefinition, updateStarredFilters, starredFilterKeys, hiddenFilters = [], className, dataTestId, }) => {
|
|
990
|
-
const [t] = useTranslation();
|
|
991
|
-
const { logEvent } = useAnalytics(FilterEvents);
|
|
992
|
-
const numberOfShowDirectlyFilters = objectValues(filterBarDefinition).filter(filter => filter.showDirectly).length; //TODO: this is a bit of a silly workaround to get the count to match without changing any other logic as I am pressed for time...but it works for now.🙈
|
|
993
|
-
const hideInStarredMenu = useMemo(() => {
|
|
994
|
-
return objectValues(filterBarDefinition)
|
|
995
|
-
.map(filter => {
|
|
996
|
-
const showInStarredMenu = filter.showInStarredMenu ? filter.showInStarredMenu() : true;
|
|
997
|
-
const showInFilterBar = filter.showInFilterBar ? filter.showInFilterBar() : true;
|
|
998
|
-
return filter.showDirectly || !showInStarredMenu || !showInFilterBar ? filter.filterKey : null;
|
|
999
|
-
})
|
|
1000
|
-
.filter(truthy);
|
|
1001
|
-
}, [filterBarDefinition]);
|
|
1002
|
-
const { filtersGrouped } = useStarredGroupFilters(objectValues(filterBarDefinition), [
|
|
1003
|
-
...hideInStarredMenu,
|
|
1004
|
-
...hiddenFilters,
|
|
1005
|
-
]);
|
|
1006
|
-
const hiddenFiltersCount = useMemo(() => {
|
|
1007
|
-
const nonHiddenStarredFilterKeys = starredFilterKeys.filter(key => !hideInStarredMenu.includes(key));
|
|
1008
|
-
return (filtersGrouped.map(group => group.filters).flat().length +
|
|
1009
|
-
hiddenFilters.length -
|
|
1010
|
-
nonHiddenStarredFilterKeys.length +
|
|
1011
|
-
numberOfShowDirectlyFilters);
|
|
1012
|
-
}, [filtersGrouped, hiddenFilters, starredFilterKeys, hideInStarredMenu, numberOfShowDirectlyFilters]);
|
|
1013
|
-
const getHiddenFiltersLabel = () => {
|
|
1014
|
-
switch (hiddenFiltersCount) {
|
|
1015
|
-
case 0:
|
|
1016
|
-
return t("filtersBar.hiddenFilters.toggleFilters");
|
|
1017
|
-
case 1:
|
|
1018
|
-
return t("filtersBar.hiddenFilters.singular");
|
|
1019
|
-
default:
|
|
1020
|
-
return t("filtersBar.hiddenFilters.plural", { count: hiddenFiltersCount });
|
|
1021
|
-
}
|
|
1022
|
-
};
|
|
1023
|
-
return (jsxs(Popover, { placement: "bottom-start", children: [jsx(PopoverTrigger, { children: jsx(Button, { className: className, dataTestId: dataTestId ?? "starred-filters-menu", size: "small", variant: "ghost", children: getHiddenFiltersLabel() }) }), jsx(PopoverContent, { children: jsx(MenuList, { className: "overflow-hidden", dataTestId: "starred-filters-menu-popover", children: jsx("div", { className: "max-h-[55dvh] w-72 overflow-y-auto px-3 pb-2", children: filtersGrouped.map((group, idx) => {
|
|
1024
|
-
const isLast = idx === filtersGrouped.length - 1;
|
|
1025
|
-
return (jsxs("div", { children: [jsx(Text, { className: "pb-1 pt-2", dataTestId: "starred-filters-group-title", size: "small", subtle: true, uppercase: true, children: group.title }), jsx("div", { className: "grid", children: group.filters.map(filter => {
|
|
1026
|
-
return !filter.isHidden ? (jsx(ToggleSwitchOption, { className: cvaInteractableItem({
|
|
1027
|
-
selected: false,
|
|
1028
|
-
cursor: "pointer",
|
|
1029
|
-
focused: "auto",
|
|
1030
|
-
className: "py-1 pl-2 pr-3",
|
|
1031
|
-
}), "data-testid": `${filter.filterKey}-star-filter`, id: `visible-filters-toggle-${filter.filterKey}`, label: filter.title, onChange: toggled => {
|
|
1032
|
-
updateStarredFilters(filter.filterKey);
|
|
1033
|
-
logEvent("Starring Filter - Toggled", {
|
|
1034
|
-
type: filter.filterKey,
|
|
1035
|
-
value: toggled.toString(),
|
|
1036
|
-
});
|
|
1037
|
-
}, showInputFocus: false, size: "medium", toggled: starredFilterKeys.includes(filter.filterKey) }, filter.filterKey)) : null;
|
|
1038
|
-
}) }), !isLast && jsx("div", { className: "mt-2 h-[1px] w-full bg-neutral-300" })] }, group.key));
|
|
1039
|
-
}) }) }) })] }));
|
|
1040
|
-
};
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* StarredFilters is a React component that displays a list of starred filters based on the provided filter bar configuration.
|
|
1044
|
-
*
|
|
1045
|
-
* @template TFilterBarDefinition - The type representing the filter bar definition.
|
|
1046
|
-
* @returns {ReactElement} - Returns the StarredFilters component.
|
|
1047
|
-
*/
|
|
1048
|
-
const StarredFilters = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], compact, dataTestId, className, title, }) => {
|
|
1049
|
-
const [t] = useTranslation();
|
|
1050
|
-
const { isLg } = useViewportBreakpoints();
|
|
1051
|
-
const isCompactMode = useMemo(() => compact ?? !isLg, [compact, isLg]);
|
|
1052
|
-
const hideInMenu = useMemo(() => {
|
|
1053
|
-
return objectValues(filterBarDefinition)
|
|
1054
|
-
.map(filter => {
|
|
1055
|
-
const showInFilterBar = filter.showInFilterBar ? filter.showInFilterBar() : true;
|
|
1056
|
-
const showInStarredMenu = filter.showInStarredMenu ? filter.showInStarredMenu() : true;
|
|
1057
|
-
const showMenuAnywayBecauseFilterHasChanged = filterBarConfig.appliedFilterKeys().includes(filter.filterKey);
|
|
1058
|
-
return (!showInFilterBar || !showInStarredMenu) && !showMenuAnywayBecauseFilterHasChanged
|
|
1059
|
-
? filter.filterKey
|
|
1060
|
-
: null;
|
|
1061
|
-
})
|
|
1062
|
-
.filter(truthy);
|
|
1063
|
-
}, [filterBarConfig, filterBarDefinition]);
|
|
1064
|
-
const { filtersGrouped } = useStarredGroupFilters(objectValues(filterBarDefinition), [
|
|
1065
|
-
...hideInMenu,
|
|
1066
|
-
...hiddenFilters,
|
|
1067
|
-
]);
|
|
1068
|
-
const { appliedFilters, filtersToShow, showDirectlyFilters } = useMemo(() => {
|
|
1069
|
-
const allFilters = filtersGrouped.map(group => group.filters).flat();
|
|
1070
|
-
const starredFilters = allFilters.filter(filter => {
|
|
1071
|
-
return (filterBarConfig.starredFilterKeys.includes(filter.filterKey) &&
|
|
1072
|
-
!filter.showDirectly);
|
|
1073
|
-
});
|
|
1074
|
-
return {
|
|
1075
|
-
appliedFilters: starredFilters.filter(filter => filterBarConfig.appliedFilterKeys().includes(filter.filterKey)),
|
|
1076
|
-
filtersToShow: starredFilters.filter(filter => !filter.showDirectly),
|
|
1077
|
-
showDirectlyFilters: allFilters.filter(filter => filter.showDirectly),
|
|
1078
|
-
};
|
|
1079
|
-
}, [filterBarConfig, filtersGrouped]);
|
|
1080
|
-
return (jsxs("div", { className: twMerge("flex flex-wrap items-center gap-2", className), "data-testid": dataTestId, children: [jsx(Popover, { placement: "bottom-start", children: modalState => (jsxs(Fragment, { children: [jsx(PopoverTrigger, { children: jsx("div", { "data-testid": "starred-filters-menu-trigger", children: jsxs(Tooltip, { disabled: !isCompactMode || modalState.isOpen, label: jsx(FilterButtonTooltipLabel, { filterBarConfig: filterBarConfig }), children: [jsx(Button, { className: "@xs:flex hidden", prefix: jsx(Icon, { color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Funnel", size: "small" }), size: "small", suffix: isCompactMode && filterBarConfig.appliedFilterKeys().length > 0
|
|
1081
|
-
? `(${filterBarConfig.appliedFilterKeys().length})`
|
|
1082
|
-
: undefined, variant: "secondary", children: title ? title : t("filtersBar.filtersHeading") }), jsx(IconButton, { className: "@xs:hidden", icon: jsx(Icon, { color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Funnel", size: "small" }), size: "small", variant: "secondary" })] }) }) }), jsx(PopoverContent, { cellPadding: 100, children: jsxs(Card, { className: "max-h-[min(600px,_calc(100dvh-32px))] overflow-hidden sm:w-[350px]", children: [filtersToShow.length > 0 ? (jsx(CardBody, { density: "dense", children: jsx("div", { className: "flex h-full min-w-min flex-col gap-2", children: jsx(FiltersList, { filterBarConfig: filterBarConfig, filters: filtersToShow }) }) })) : null, jsxs(CardFooter, { className: filtersToShow.length === 0 ? "border-none" : undefined, density: "dense", children: [jsx(StarredFiltersMenu, { className: "mr-auto", filterBarDefinition: filterBarDefinition, hiddenFilters: hiddenFilters, starredFilterKeys: filterBarConfig.starredFilterKeys, updateStarredFilters: filterBarConfig.updateStarredFilters }), !isCompactMode ? null : (jsx(ResetFiltersButton, { filtersHaveBeenApplied: filterBarConfig.appliedFilterKeys().length > 0, resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState }))] })] }) })] })) }), showDirectlyFilters.length > 0 ? (jsx(FiltersList, { filterBarConfig: filterBarConfig, filters: showDirectlyFilters })) : null, !isCompactMode ? (jsxs(Fragment, { children: [appliedFilters.filter(filter => !filter.showDirectly).length > 0 ? (jsx("div", { className: "h-4 w-[1px] bg-slate-300" })) : null, jsx(FiltersList, { filterBarConfig: filterBarConfig, filters: appliedFilters }), jsx(ResetFiltersButton, { filtersHaveBeenApplied: filterBarConfig.appliedFilterKeys().length > 0, resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })] })) : null] }));
|
|
1083
|
-
};
|
|
1084
|
-
const FiltersList = ({ filters, filterBarConfig }) => {
|
|
1085
|
-
return filters.length === 0
|
|
1086
|
-
? null
|
|
1087
|
-
: filters.map(filter => {
|
|
1088
|
-
return (jsx(Filter, { filter: filter, filterBarActions: filterBarConfig, filterState: { values: filterBarConfig.values, setters: filterBarConfig.setters } }, `filter-${filter.filterKey}`));
|
|
1089
|
-
});
|
|
1090
|
-
};
|
|
1091
|
-
const FilterButtonTooltipLabel = ({ filterBarConfig, }) => {
|
|
1092
|
-
const [t] = useTranslation();
|
|
1093
|
-
switch (filterBarConfig.appliedFilterKeys().length) {
|
|
1094
|
-
case 0:
|
|
1095
|
-
return t("filtersBar.appliedFiltersTooltip.none");
|
|
1096
|
-
case 1:
|
|
1097
|
-
return filterBarConfig.appliedFilterKeys()[0]
|
|
1098
|
-
? t("filtersBar.appliedFiltersTooltip.singular", {
|
|
1099
|
-
filterName: filterBarConfig.getFilterTitle(filterBarConfig.appliedFilterKeys()[0]),
|
|
1100
|
-
})
|
|
1101
|
-
: null; // should never happen though
|
|
1102
|
-
default:
|
|
1103
|
-
return (jsxs(Fragment, { children: [t("filtersBar.appliedFiltersTooltip.plural", { count: filterBarConfig.appliedFilterKeys().length }), jsx("ul", { className: "list-inside", children: filterBarConfig.appliedFilterKeys().map((appliedFilterKey, index) => (jsx("li", { className: "list-disc", children: filterBarConfig.getFilterTitle(appliedFilterKey) }, index))) })] }));
|
|
1104
|
-
}
|
|
1105
|
-
};
|
|
1106
|
-
|
|
1107
1141
|
/**
|
|
1108
1142
|
* The FilterBar component serves as a wrapper for managing filters.
|
|
1109
1143
|
*/
|
|
1110
|
-
const FilterBar = ({ hiddenFilters, className, filterBarDefinition, filterBarConfig, compact, title, }) => {
|
|
1111
|
-
return (jsx(
|
|
1144
|
+
const FilterBar = ({ hiddenFilters, className, filterBarDefinition, filterBarConfig, compact = true, title, }) => {
|
|
1145
|
+
return (jsx(FiltersMenu, { className: className, compact: compact, dataTestId: `${filterBarConfig.name}-filterbar`, filterBarConfig: filterBarConfig, filterBarDefinition: filterBarDefinition, hiddenFilters: hiddenFilters, title: title }));
|
|
1112
1146
|
};
|
|
1113
1147
|
|
|
1114
1148
|
// Can't import jest.fn so must define a function that does nothing but mimics the jest.fn
|
|
@@ -1924,4 +1958,4 @@ const mergeFilters = (filterBarDefinition, extraFilters) => {
|
|
|
1924
1958
|
*/
|
|
1925
1959
|
setupLibraryTranslations();
|
|
1926
1960
|
|
|
1927
|
-
export { DefaultCheckboxFilter, DefaultDateRangeFilter, DefaultMinMaxFilter, DefaultRadioFilter, DynamicFilterList, FilterBar, FilterEvents, FilterHeader, FilterResults, HierarchicalCheckboxFilter,
|
|
1961
|
+
export { DefaultCheckboxFilter, DefaultDateRangeFilter, DefaultMinMaxFilter, DefaultRadioFilter, DynamicFilterList, FilterBar, FilterComponent, FilterEvents, FilterHeader, FilterResults, FilterTableComponent, FiltersMenu, FiltersRenderer, GroupedFiltersList, HierarchicalCheckboxFilter, ResetFiltersButton, areaFilterGeoJsonGeometrySchema, isAreaFilterValue, isArrayFilterValue, isBooleanValue, isDateRangeValue, isMinMaxFilterValue, isStringArrayFilterValue, isValueName, isValueNameArray, mergeFilters, mockFilterBar, toggleFilterValue, useFilterBar, useFilterBarAsync, useSearchParamAsFilter, validateFilter };
|