@trackunit/filters-filter-bar 1.3.89 → 1.3.92
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 +227 -0
- package/index.esm.js +228 -2
- package/package.json +11 -10
- package/src/lib/components/DefaultFilterTypes.d.ts +14 -0
- package/src/lib/components/HierarchicalCheckboxFilter/HierarchicalCheckboxFilter.d.ts +58 -0
- package/src/lib/components/HierarchicalCheckboxFilter/HierarchicalCheckboxFilter.utils.d.ts +13 -0
- package/src/lib/components/HierarchicalCheckboxFilter/RenderHierarchicalOption.d.ts +20 -0
- package/src/lib/components/HierarchicalCheckboxFilter/useHierarchicalOptionState.d.ts +25 -0
- package/src/lib/components/index.d.ts +1 -0
package/index.cjs.js
CHANGED
|
@@ -11,6 +11,7 @@ var reactCoreContextsApi = require('@trackunit/react-core-contexts-api');
|
|
|
11
11
|
var reactFormComponents = require('@trackunit/react-form-components');
|
|
12
12
|
var reactDateAndTimeComponents = require('@trackunit/react-date-and-time-components');
|
|
13
13
|
var sharedUtils = require('@trackunit/shared-utils');
|
|
14
|
+
var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
|
|
14
15
|
var tailwindMerge = require('tailwind-merge');
|
|
15
16
|
var dequal = require('dequal');
|
|
16
17
|
var isEqual = require('lodash/isEqual');
|
|
@@ -628,6 +629,231 @@ const DefaultRadioFilter = ({ filterDefinition, filterBarActions, options, loadi
|
|
|
628
629
|
}, value: selectedRadioId?.key || "", children: jsxRuntime.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" }) })) })] }));
|
|
629
630
|
};
|
|
630
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Recursively gets all descendant keys for a given hierarchical option, including the option itself.
|
|
634
|
+
*
|
|
635
|
+
* @param option The starting hierarchical filter option.
|
|
636
|
+
* @returns An array of string keys representing the option and all its descendants.
|
|
637
|
+
*/
|
|
638
|
+
const getAllDescendantKeys = (option) => {
|
|
639
|
+
let keys = [option.key];
|
|
640
|
+
if (option.children) {
|
|
641
|
+
option.children.forEach(child => {
|
|
642
|
+
keys = keys.concat(getAllDescendantKeys(child));
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return keys;
|
|
646
|
+
};
|
|
647
|
+
/**
|
|
648
|
+
* Recursively filters hierarchical options based on a search term.
|
|
649
|
+
* Keeps an option if its label matches or if any descendant matches.
|
|
650
|
+
*/
|
|
651
|
+
const filterHierarchicalOptions = (options, searchTerm, parentMatches = false // Track if the parent already matched
|
|
652
|
+
) => {
|
|
653
|
+
if (!searchTerm) {
|
|
654
|
+
return options; // Return all options if search term is empty
|
|
655
|
+
}
|
|
656
|
+
const lowerCaseSearchTerm = searchTerm.toLowerCase();
|
|
657
|
+
return options.reduce((acc, option) => {
|
|
658
|
+
const labelMatches = option.label.toLowerCase().includes(lowerCaseSearchTerm);
|
|
659
|
+
const matches = labelMatches || parentMatches;
|
|
660
|
+
let filteredChildren = [];
|
|
661
|
+
if (option.children && option.children.length > 0) {
|
|
662
|
+
// Pass `matches` as `parentMatches` for children
|
|
663
|
+
filteredChildren = filterHierarchicalOptions(option.children, searchTerm, matches);
|
|
664
|
+
}
|
|
665
|
+
// Include the option if:
|
|
666
|
+
// 1. Its label matches OR
|
|
667
|
+
// 2. Its parent matched (meaning it's part of a matched branch) OR
|
|
668
|
+
// 3. It has children that matched
|
|
669
|
+
if (matches || filteredChildren.length > 0) {
|
|
670
|
+
acc.push({
|
|
671
|
+
...option,
|
|
672
|
+
// Only include children if they (or their descendants) matched
|
|
673
|
+
children: filteredChildren.length > 0 ? filteredChildren : undefined,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return acc;
|
|
677
|
+
}, []);
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Custom hook to manage the state and interactions of a single hierarchical option.
|
|
682
|
+
* Calculates checked/indeterminate state and provides a handler for value changes,
|
|
683
|
+
* including cascading logic.
|
|
684
|
+
*/
|
|
685
|
+
const useHierarchicalOptionState = ({ options, option, selectedValues, cascadeSelection, optionsMap, setValue, filterDefinition, logEvent, }) => {
|
|
686
|
+
const allDescendantKeys = react.useMemo(() => (cascadeSelection ? getAllDescendantKeys(option) : [option.key]), [option, cascadeSelection]);
|
|
687
|
+
const hasChildren = react.useMemo(() => (option.children ?? []).length > 0, [option.children]);
|
|
688
|
+
const parentMap = react.useMemo(() => {
|
|
689
|
+
const map = new Map();
|
|
690
|
+
const traverse = (opts, parentKey) => {
|
|
691
|
+
opts.forEach(opt => {
|
|
692
|
+
if (parentKey) {
|
|
693
|
+
map.set(opt.key, parentKey);
|
|
694
|
+
}
|
|
695
|
+
if (opt.children && Array.isArray(opt.children) && opt.children.length > 0) {
|
|
696
|
+
traverse(opt.children, opt.key);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
traverse(options);
|
|
701
|
+
return map;
|
|
702
|
+
}, [options]);
|
|
703
|
+
const { isChecked, isIndeterminate } = react.useMemo(() => {
|
|
704
|
+
if (!cascadeSelection || !hasChildren) {
|
|
705
|
+
const selfChecked = selectedValues.some(v => v.value === option.key);
|
|
706
|
+
return { isChecked: selfChecked, isIndeterminate: false };
|
|
707
|
+
}
|
|
708
|
+
const selectedKeySet = new Set(selectedValues.map(v => v.value));
|
|
709
|
+
let allChildrenChecked = true;
|
|
710
|
+
let someChildrenChecked = false;
|
|
711
|
+
const descendantKeysOnly = allDescendantKeys.filter(k => k !== option.key);
|
|
712
|
+
if (descendantKeysOnly.length === 0) {
|
|
713
|
+
return { isChecked: selectedKeySet.has(option.key), isIndeterminate: false };
|
|
714
|
+
}
|
|
715
|
+
for (const key of descendantKeysOnly) {
|
|
716
|
+
if (selectedKeySet.has(key)) {
|
|
717
|
+
someChildrenChecked = true;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
allChildrenChecked = false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const checked = allChildrenChecked;
|
|
724
|
+
const indeterminate = someChildrenChecked && !allChildrenChecked;
|
|
725
|
+
return { isChecked: checked, isIndeterminate: indeterminate };
|
|
726
|
+
}, [selectedValues, option.key, allDescendantKeys, cascadeSelection, hasChildren]);
|
|
727
|
+
const handleSelect = react.useCallback(() => {
|
|
728
|
+
logEvent("Filters Applied - V2", {
|
|
729
|
+
type: `${stringTs.capitalize(filterDefinition.filterKey)}Filter`,
|
|
730
|
+
value: option.key,
|
|
731
|
+
});
|
|
732
|
+
setValue(prev => {
|
|
733
|
+
let currentSelectedValues = prev ? [...prev] : [];
|
|
734
|
+
const selectedKeySet = new Set(currentSelectedValues.map(v => v.value));
|
|
735
|
+
if (!cascadeSelection) {
|
|
736
|
+
// Non-cascading: Toggle only the current option
|
|
737
|
+
if (selectedKeySet.has(option.key)) {
|
|
738
|
+
currentSelectedValues = currentSelectedValues.filter(f => f.value !== option.key);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
currentSelectedValues.push({ name: option.label, value: option.key });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
const keysToToggle = getAllDescendantKeys(option);
|
|
746
|
+
const shouldBeChecked = !isChecked;
|
|
747
|
+
if (!shouldBeChecked) {
|
|
748
|
+
const keysToRemove = new Set(getAllDescendantKeys(option));
|
|
749
|
+
let currentParentKey = parentMap.get(option.key);
|
|
750
|
+
while (currentParentKey) {
|
|
751
|
+
keysToRemove.add(currentParentKey);
|
|
752
|
+
currentParentKey = parentMap.get(currentParentKey);
|
|
753
|
+
}
|
|
754
|
+
currentSelectedValues = currentSelectedValues.filter(f => !keysToRemove.has(f.value));
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
// Add the option and all descendants if they are not already selected
|
|
758
|
+
const itemsToAdd = [];
|
|
759
|
+
keysToToggle.forEach(key => {
|
|
760
|
+
if (!selectedKeySet.has(key)) {
|
|
761
|
+
const optionDetails = optionsMap.get(key);
|
|
762
|
+
// Ensure we have details before adding, fallback to key if not found (shouldn't happen ideally)
|
|
763
|
+
itemsToAdd.push({ name: optionDetails?.label ?? key, value: key });
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
currentSelectedValues.push(...itemsToAdd);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return currentSelectedValues;
|
|
770
|
+
});
|
|
771
|
+
}, [cascadeSelection, filterDefinition.filterKey, logEvent, setValue, isChecked, option, optionsMap, parentMap]);
|
|
772
|
+
return {
|
|
773
|
+
isChecked,
|
|
774
|
+
isIndeterminate,
|
|
775
|
+
handleSelect,
|
|
776
|
+
};
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Renders a single option and its children recursively.
|
|
781
|
+
* Uses the `useHierarchicalOptionState` hook for state management and interaction logic.
|
|
782
|
+
*/
|
|
783
|
+
const RenderHierarchicalOption = ({ option, filterDefinition, filterBarActions, setValue, logEvent, selectedValues, cascadeSelection, optionsMap, }) => {
|
|
784
|
+
const level = option.level ?? 0;
|
|
785
|
+
const { isChecked, isIndeterminate, handleSelect } = useHierarchicalOptionState({
|
|
786
|
+
option,
|
|
787
|
+
selectedValues,
|
|
788
|
+
cascadeSelection,
|
|
789
|
+
optionsMap,
|
|
790
|
+
setValue,
|
|
791
|
+
options: Array.from(optionsMap.values()),
|
|
792
|
+
filterDefinition,
|
|
793
|
+
logEvent,
|
|
794
|
+
});
|
|
795
|
+
return (jsxRuntime.jsxs(react.Fragment, { children: [jsxRuntime.jsx("div", { className: cvaOptionItem({ indentationLevel: level > 6 ? 6 : level }), children: jsxRuntime.jsx(reactFilterComponents.CheckBoxFilterItem, { checked: isChecked, className: "rounded-none", dataTestId: `hierarchical-filter-check-box-${option.key}`, indeterminate: isIndeterminate, itemCount: option.count, label: option.label, name: `hierarchical-filter-check-box-${option.key}`, onChange: handleSelect }) }), option.children?.map(child => (jsxRuntime.jsx(RenderHierarchicalOption, { cascadeSelection: cascadeSelection, filterBarActions: filterBarActions, filterDefinition: filterDefinition, logEvent: logEvent, option: child, optionsMap: optionsMap, selectedValues: selectedValues, setValue: setValue }, child.key)))] }, option.key));
|
|
796
|
+
};
|
|
797
|
+
const cvaOptionItem = cssClassVarianceUtilities.cvaMerge(["m-1"], {
|
|
798
|
+
variants: {
|
|
799
|
+
indentationLevel: {
|
|
800
|
+
0: ["pl-0"],
|
|
801
|
+
1: ["pl-6"],
|
|
802
|
+
2: ["pl-12"],
|
|
803
|
+
3: ["pl-18"],
|
|
804
|
+
4: ["pl-24"],
|
|
805
|
+
5: ["pl-30"],
|
|
806
|
+
6: ["pl-36"],
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
defaultVariants: {
|
|
810
|
+
indentationLevel: 0,
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* `HierarchicalCheckboxFilter` is a React component for handling hierarchical checkbox filters.
|
|
816
|
+
* It expects options to be pre-processed into a hierarchical structure.
|
|
817
|
+
*
|
|
818
|
+
* Supports optional cascading selection behavior via the `cascadeSelection` prop.
|
|
819
|
+
*/
|
|
820
|
+
const HierarchicalCheckboxFilter = ({ filterDefinition, filterBarActions, options, loading, setValue, value, cascadeSelection = false, customSearch, }) => {
|
|
821
|
+
const { logEvent } = reactCoreHooks.useAnalytics(FilterEvents);
|
|
822
|
+
const selectedValues = value ?? [];
|
|
823
|
+
// Local state for client-side search (used if customSearch is not provided)
|
|
824
|
+
const [localSearchText, setLocalSearchText] = react.useState("");
|
|
825
|
+
// Determine if we are using server-side search (customSearch) or client-side search
|
|
826
|
+
const isServerSearch = !!customSearch;
|
|
827
|
+
const currentSearchText = isServerSearch ? customSearch.value : localSearchText;
|
|
828
|
+
const currentSetSearchText = isServerSearch ? customSearch.onChange : setLocalSearchText;
|
|
829
|
+
const searchHeaderProps = {
|
|
830
|
+
searchEnabled: true,
|
|
831
|
+
searchProps: { value: currentSearchText, onChange: currentSetSearchText },
|
|
832
|
+
};
|
|
833
|
+
const optionsMap = react.useMemo(() => {
|
|
834
|
+
const map = new Map();
|
|
835
|
+
const traverse = (opts) => {
|
|
836
|
+
opts.forEach(opt => {
|
|
837
|
+
map.set(opt.key, opt);
|
|
838
|
+
if (opt.children) {
|
|
839
|
+
traverse(opt.children);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
|
+
traverse(options);
|
|
844
|
+
return map;
|
|
845
|
+
}, [options]);
|
|
846
|
+
// Filter options based on search term, preserving hierarchy
|
|
847
|
+
const filteredOptions = react.useMemo(() => {
|
|
848
|
+
// Only apply client-side filtering if not doing server-side search
|
|
849
|
+
if (isServerSearch) {
|
|
850
|
+
return options; // Assume server provides filtered options
|
|
851
|
+
}
|
|
852
|
+
return filterHierarchicalOptions(options, localSearchText);
|
|
853
|
+
}, [options, isServerSearch, localSearchText]);
|
|
854
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(FilterHeader, { ...filterDefinition, ...filterBarActions, filterHasChanges: filterBarActions.appliedFilterKeys().includes(filterDefinition.filterKey), loading: loading, ...searchHeaderProps }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-y-auto", children: jsxRuntime.jsx(FilterResults, { loading: loading, results: filteredOptions, children: results => (jsxRuntime.jsx(jsxRuntime.Fragment, { children: results.map(option => (jsxRuntime.jsx(RenderHierarchicalOption, { cascadeSelection: cascadeSelection, filterBarActions: filterBarActions, filterDefinition: filterDefinition, logEvent: logEvent, option: option, optionsMap: optionsMap, selectedValues: selectedValues, setValue: setValue }, option.key))) })) }) })] }));
|
|
855
|
+
};
|
|
856
|
+
|
|
631
857
|
/**
|
|
632
858
|
* Custom React hook for grouping and filtering a list of filters.
|
|
633
859
|
*
|
|
@@ -1709,6 +1935,7 @@ exports.FilterBar = FilterBar;
|
|
|
1709
1935
|
exports.FilterEvents = FilterEvents;
|
|
1710
1936
|
exports.FilterHeader = FilterHeader;
|
|
1711
1937
|
exports.FilterResults = FilterResults;
|
|
1938
|
+
exports.HierarchicalCheckboxFilter = HierarchicalCheckboxFilter;
|
|
1712
1939
|
exports.StarredFilters = StarredFilters;
|
|
1713
1940
|
exports.areaFilterGeoJsonGeometrySchema = areaFilterGeoJsonGeometrySchema;
|
|
1714
1941
|
exports.isAreaFilterValue = isAreaFilterValue;
|
package/index.esm.js
CHANGED
|
@@ -3,12 +3,13 @@ import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-l
|
|
|
3
3
|
import { VirtualizedList, Text, Button, Popover, PopoverTrigger, PopoverContent, MenuList, cvaInteractableItem, useViewportBreakpoints, Tooltip, Icon, IconButton, Card, CardBody, CardFooter } from '@trackunit/react-components';
|
|
4
4
|
import { useAnalytics, useTextSearch, useCurrentUser } from '@trackunit/react-core-hooks';
|
|
5
5
|
import { FilterBody, RadioFilterItem, CheckBoxFilterItem, FilterHeader as FilterHeader$1, FilterFooter, Filter as Filter$1 } from '@trackunit/react-filter-components';
|
|
6
|
-
import { useRef, useMemo, useState, useEffect, useCallback } from 'react';
|
|
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
9
|
import { Search, NumberField, RadioGroup, ToggleSwitchOption } 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';
|
|
12
13
|
import { twMerge } from 'tailwind-merge';
|
|
13
14
|
import { dequal } from 'dequal';
|
|
14
15
|
import isEqual from 'lodash/isEqual';
|
|
@@ -626,6 +627,231 @@ const DefaultRadioFilter = ({ filterDefinition, filterBarActions, options, loadi
|
|
|
626
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" }) })) })] }));
|
|
627
628
|
};
|
|
628
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Recursively gets all descendant keys for a given hierarchical option, including the option itself.
|
|
632
|
+
*
|
|
633
|
+
* @param option The starting hierarchical filter option.
|
|
634
|
+
* @returns An array of string keys representing the option and all its descendants.
|
|
635
|
+
*/
|
|
636
|
+
const getAllDescendantKeys = (option) => {
|
|
637
|
+
let keys = [option.key];
|
|
638
|
+
if (option.children) {
|
|
639
|
+
option.children.forEach(child => {
|
|
640
|
+
keys = keys.concat(getAllDescendantKeys(child));
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return keys;
|
|
644
|
+
};
|
|
645
|
+
/**
|
|
646
|
+
* Recursively filters hierarchical options based on a search term.
|
|
647
|
+
* Keeps an option if its label matches or if any descendant matches.
|
|
648
|
+
*/
|
|
649
|
+
const filterHierarchicalOptions = (options, searchTerm, parentMatches = false // Track if the parent already matched
|
|
650
|
+
) => {
|
|
651
|
+
if (!searchTerm) {
|
|
652
|
+
return options; // Return all options if search term is empty
|
|
653
|
+
}
|
|
654
|
+
const lowerCaseSearchTerm = searchTerm.toLowerCase();
|
|
655
|
+
return options.reduce((acc, option) => {
|
|
656
|
+
const labelMatches = option.label.toLowerCase().includes(lowerCaseSearchTerm);
|
|
657
|
+
const matches = labelMatches || parentMatches;
|
|
658
|
+
let filteredChildren = [];
|
|
659
|
+
if (option.children && option.children.length > 0) {
|
|
660
|
+
// Pass `matches` as `parentMatches` for children
|
|
661
|
+
filteredChildren = filterHierarchicalOptions(option.children, searchTerm, matches);
|
|
662
|
+
}
|
|
663
|
+
// Include the option if:
|
|
664
|
+
// 1. Its label matches OR
|
|
665
|
+
// 2. Its parent matched (meaning it's part of a matched branch) OR
|
|
666
|
+
// 3. It has children that matched
|
|
667
|
+
if (matches || filteredChildren.length > 0) {
|
|
668
|
+
acc.push({
|
|
669
|
+
...option,
|
|
670
|
+
// Only include children if they (or their descendants) matched
|
|
671
|
+
children: filteredChildren.length > 0 ? filteredChildren : undefined,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return acc;
|
|
675
|
+
}, []);
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Custom hook to manage the state and interactions of a single hierarchical option.
|
|
680
|
+
* Calculates checked/indeterminate state and provides a handler for value changes,
|
|
681
|
+
* including cascading logic.
|
|
682
|
+
*/
|
|
683
|
+
const useHierarchicalOptionState = ({ options, option, selectedValues, cascadeSelection, optionsMap, setValue, filterDefinition, logEvent, }) => {
|
|
684
|
+
const allDescendantKeys = useMemo(() => (cascadeSelection ? getAllDescendantKeys(option) : [option.key]), [option, cascadeSelection]);
|
|
685
|
+
const hasChildren = useMemo(() => (option.children ?? []).length > 0, [option.children]);
|
|
686
|
+
const parentMap = useMemo(() => {
|
|
687
|
+
const map = new Map();
|
|
688
|
+
const traverse = (opts, parentKey) => {
|
|
689
|
+
opts.forEach(opt => {
|
|
690
|
+
if (parentKey) {
|
|
691
|
+
map.set(opt.key, parentKey);
|
|
692
|
+
}
|
|
693
|
+
if (opt.children && Array.isArray(opt.children) && opt.children.length > 0) {
|
|
694
|
+
traverse(opt.children, opt.key);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
traverse(options);
|
|
699
|
+
return map;
|
|
700
|
+
}, [options]);
|
|
701
|
+
const { isChecked, isIndeterminate } = useMemo(() => {
|
|
702
|
+
if (!cascadeSelection || !hasChildren) {
|
|
703
|
+
const selfChecked = selectedValues.some(v => v.value === option.key);
|
|
704
|
+
return { isChecked: selfChecked, isIndeterminate: false };
|
|
705
|
+
}
|
|
706
|
+
const selectedKeySet = new Set(selectedValues.map(v => v.value));
|
|
707
|
+
let allChildrenChecked = true;
|
|
708
|
+
let someChildrenChecked = false;
|
|
709
|
+
const descendantKeysOnly = allDescendantKeys.filter(k => k !== option.key);
|
|
710
|
+
if (descendantKeysOnly.length === 0) {
|
|
711
|
+
return { isChecked: selectedKeySet.has(option.key), isIndeterminate: false };
|
|
712
|
+
}
|
|
713
|
+
for (const key of descendantKeysOnly) {
|
|
714
|
+
if (selectedKeySet.has(key)) {
|
|
715
|
+
someChildrenChecked = true;
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
allChildrenChecked = false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const checked = allChildrenChecked;
|
|
722
|
+
const indeterminate = someChildrenChecked && !allChildrenChecked;
|
|
723
|
+
return { isChecked: checked, isIndeterminate: indeterminate };
|
|
724
|
+
}, [selectedValues, option.key, allDescendantKeys, cascadeSelection, hasChildren]);
|
|
725
|
+
const handleSelect = useCallback(() => {
|
|
726
|
+
logEvent("Filters Applied - V2", {
|
|
727
|
+
type: `${capitalize(filterDefinition.filterKey)}Filter`,
|
|
728
|
+
value: option.key,
|
|
729
|
+
});
|
|
730
|
+
setValue(prev => {
|
|
731
|
+
let currentSelectedValues = prev ? [...prev] : [];
|
|
732
|
+
const selectedKeySet = new Set(currentSelectedValues.map(v => v.value));
|
|
733
|
+
if (!cascadeSelection) {
|
|
734
|
+
// Non-cascading: Toggle only the current option
|
|
735
|
+
if (selectedKeySet.has(option.key)) {
|
|
736
|
+
currentSelectedValues = currentSelectedValues.filter(f => f.value !== option.key);
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
currentSelectedValues.push({ name: option.label, value: option.key });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
const keysToToggle = getAllDescendantKeys(option);
|
|
744
|
+
const shouldBeChecked = !isChecked;
|
|
745
|
+
if (!shouldBeChecked) {
|
|
746
|
+
const keysToRemove = new Set(getAllDescendantKeys(option));
|
|
747
|
+
let currentParentKey = parentMap.get(option.key);
|
|
748
|
+
while (currentParentKey) {
|
|
749
|
+
keysToRemove.add(currentParentKey);
|
|
750
|
+
currentParentKey = parentMap.get(currentParentKey);
|
|
751
|
+
}
|
|
752
|
+
currentSelectedValues = currentSelectedValues.filter(f => !keysToRemove.has(f.value));
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
// Add the option and all descendants if they are not already selected
|
|
756
|
+
const itemsToAdd = [];
|
|
757
|
+
keysToToggle.forEach(key => {
|
|
758
|
+
if (!selectedKeySet.has(key)) {
|
|
759
|
+
const optionDetails = optionsMap.get(key);
|
|
760
|
+
// Ensure we have details before adding, fallback to key if not found (shouldn't happen ideally)
|
|
761
|
+
itemsToAdd.push({ name: optionDetails?.label ?? key, value: key });
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
currentSelectedValues.push(...itemsToAdd);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return currentSelectedValues;
|
|
768
|
+
});
|
|
769
|
+
}, [cascadeSelection, filterDefinition.filterKey, logEvent, setValue, isChecked, option, optionsMap, parentMap]);
|
|
770
|
+
return {
|
|
771
|
+
isChecked,
|
|
772
|
+
isIndeterminate,
|
|
773
|
+
handleSelect,
|
|
774
|
+
};
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Renders a single option and its children recursively.
|
|
779
|
+
* Uses the `useHierarchicalOptionState` hook for state management and interaction logic.
|
|
780
|
+
*/
|
|
781
|
+
const RenderHierarchicalOption = ({ option, filterDefinition, filterBarActions, setValue, logEvent, selectedValues, cascadeSelection, optionsMap, }) => {
|
|
782
|
+
const level = option.level ?? 0;
|
|
783
|
+
const { isChecked, isIndeterminate, handleSelect } = useHierarchicalOptionState({
|
|
784
|
+
option,
|
|
785
|
+
selectedValues,
|
|
786
|
+
cascadeSelection,
|
|
787
|
+
optionsMap,
|
|
788
|
+
setValue,
|
|
789
|
+
options: Array.from(optionsMap.values()),
|
|
790
|
+
filterDefinition,
|
|
791
|
+
logEvent,
|
|
792
|
+
});
|
|
793
|
+
return (jsxs(Fragment$1, { children: [jsx("div", { className: cvaOptionItem({ indentationLevel: level > 6 ? 6 : level }), children: jsx(CheckBoxFilterItem, { checked: isChecked, className: "rounded-none", dataTestId: `hierarchical-filter-check-box-${option.key}`, indeterminate: isIndeterminate, itemCount: option.count, label: option.label, name: `hierarchical-filter-check-box-${option.key}`, onChange: handleSelect }) }), option.children?.map(child => (jsx(RenderHierarchicalOption, { cascadeSelection: cascadeSelection, filterBarActions: filterBarActions, filterDefinition: filterDefinition, logEvent: logEvent, option: child, optionsMap: optionsMap, selectedValues: selectedValues, setValue: setValue }, child.key)))] }, option.key));
|
|
794
|
+
};
|
|
795
|
+
const cvaOptionItem = cvaMerge(["m-1"], {
|
|
796
|
+
variants: {
|
|
797
|
+
indentationLevel: {
|
|
798
|
+
0: ["pl-0"],
|
|
799
|
+
1: ["pl-6"],
|
|
800
|
+
2: ["pl-12"],
|
|
801
|
+
3: ["pl-18"],
|
|
802
|
+
4: ["pl-24"],
|
|
803
|
+
5: ["pl-30"],
|
|
804
|
+
6: ["pl-36"],
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
defaultVariants: {
|
|
808
|
+
indentationLevel: 0,
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* `HierarchicalCheckboxFilter` is a React component for handling hierarchical checkbox filters.
|
|
814
|
+
* It expects options to be pre-processed into a hierarchical structure.
|
|
815
|
+
*
|
|
816
|
+
* Supports optional cascading selection behavior via the `cascadeSelection` prop.
|
|
817
|
+
*/
|
|
818
|
+
const HierarchicalCheckboxFilter = ({ filterDefinition, filterBarActions, options, loading, setValue, value, cascadeSelection = false, customSearch, }) => {
|
|
819
|
+
const { logEvent } = useAnalytics(FilterEvents);
|
|
820
|
+
const selectedValues = value ?? [];
|
|
821
|
+
// Local state for client-side search (used if customSearch is not provided)
|
|
822
|
+
const [localSearchText, setLocalSearchText] = useState("");
|
|
823
|
+
// Determine if we are using server-side search (customSearch) or client-side search
|
|
824
|
+
const isServerSearch = !!customSearch;
|
|
825
|
+
const currentSearchText = isServerSearch ? customSearch.value : localSearchText;
|
|
826
|
+
const currentSetSearchText = isServerSearch ? customSearch.onChange : setLocalSearchText;
|
|
827
|
+
const searchHeaderProps = {
|
|
828
|
+
searchEnabled: true,
|
|
829
|
+
searchProps: { value: currentSearchText, onChange: currentSetSearchText },
|
|
830
|
+
};
|
|
831
|
+
const optionsMap = useMemo(() => {
|
|
832
|
+
const map = new Map();
|
|
833
|
+
const traverse = (opts) => {
|
|
834
|
+
opts.forEach(opt => {
|
|
835
|
+
map.set(opt.key, opt);
|
|
836
|
+
if (opt.children) {
|
|
837
|
+
traverse(opt.children);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
};
|
|
841
|
+
traverse(options);
|
|
842
|
+
return map;
|
|
843
|
+
}, [options]);
|
|
844
|
+
// Filter options based on search term, preserving hierarchy
|
|
845
|
+
const filteredOptions = useMemo(() => {
|
|
846
|
+
// Only apply client-side filtering if not doing server-side search
|
|
847
|
+
if (isServerSearch) {
|
|
848
|
+
return options; // Assume server provides filtered options
|
|
849
|
+
}
|
|
850
|
+
return filterHierarchicalOptions(options, localSearchText);
|
|
851
|
+
}, [options, isServerSearch, localSearchText]);
|
|
852
|
+
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
|
+
};
|
|
854
|
+
|
|
629
855
|
/**
|
|
630
856
|
* Custom React hook for grouping and filtering a list of filters.
|
|
631
857
|
*
|
|
@@ -1698,4 +1924,4 @@ const mergeFilters = (filterBarDefinition, extraFilters) => {
|
|
|
1698
1924
|
*/
|
|
1699
1925
|
setupLibraryTranslations();
|
|
1700
1926
|
|
|
1701
|
-
export { DefaultCheckboxFilter, DefaultDateRangeFilter, DefaultMinMaxFilter, DefaultRadioFilter, DynamicFilterList, FilterBar, FilterEvents, FilterHeader, FilterResults, StarredFilters, areaFilterGeoJsonGeometrySchema, isAreaFilterValue, isArrayFilterValue, isBooleanValue, isDateRangeValue, isMinMaxFilterValue, isStringArrayFilterValue, isValueName, isValueNameArray, mergeFilters, mockFilterBar, toggleFilterValue, useFilterBar, useFilterBarAsync, useSearchParamAsFilter, validateFilter };
|
|
1927
|
+
export { DefaultCheckboxFilter, DefaultDateRangeFilter, DefaultMinMaxFilter, DefaultRadioFilter, DynamicFilterList, FilterBar, FilterEvents, FilterHeader, FilterResults, HierarchicalCheckboxFilter, StarredFilters, areaFilterGeoJsonGeometrySchema, isAreaFilterValue, isArrayFilterValue, isBooleanValue, isDateRangeValue, isMinMaxFilterValue, isStringArrayFilterValue, isValueName, isValueNameArray, mergeFilters, mockFilterBar, toggleFilterValue, useFilterBar, useFilterBarAsync, useSearchParamAsFilter, validateFilter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/filters-filter-bar",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.92",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
"tailwind-merge": "^2.0.0",
|
|
15
15
|
"string-ts": "^2.0.0",
|
|
16
16
|
"zod": "3.22.4",
|
|
17
|
-
"@trackunit/react-components": "1.4.
|
|
18
|
-
"@trackunit/react-core-hooks": "1.3.
|
|
19
|
-
"@trackunit/react-filter-components": "1.3.
|
|
20
|
-
"@trackunit/react-date-and-time-components": "1.3.
|
|
21
|
-
"@trackunit/shared-utils": "1.5.
|
|
22
|
-
"@trackunit/react-form-components": "1.3.
|
|
23
|
-
"@trackunit/react-core-contexts-api": "1.4.
|
|
24
|
-
"@trackunit/geo-json-utils": "1.3.
|
|
25
|
-
"@trackunit/i18n-library-translation": "1.3.
|
|
17
|
+
"@trackunit/react-components": "1.4.66",
|
|
18
|
+
"@trackunit/react-core-hooks": "1.3.56",
|
|
19
|
+
"@trackunit/react-filter-components": "1.3.76",
|
|
20
|
+
"@trackunit/react-date-and-time-components": "1.3.74",
|
|
21
|
+
"@trackunit/shared-utils": "1.5.53",
|
|
22
|
+
"@trackunit/react-form-components": "1.3.76",
|
|
23
|
+
"@trackunit/react-core-contexts-api": "1.4.54",
|
|
24
|
+
"@trackunit/geo-json-utils": "1.3.54",
|
|
25
|
+
"@trackunit/i18n-library-translation": "1.3.58",
|
|
26
|
+
"@trackunit/css-class-variance-utilities": "1.3.53"
|
|
26
27
|
},
|
|
27
28
|
"module": "./index.esm.js",
|
|
28
29
|
"main": "./index.cjs.js",
|
|
@@ -18,6 +18,20 @@ export interface FilterOption {
|
|
|
18
18
|
*/
|
|
19
19
|
prefix?: ReactNode;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Represents a filter option within a hierarchical structure.
|
|
23
|
+
*/
|
|
24
|
+
export interface HierarchicalFilterOption extends FilterOption {
|
|
25
|
+
/**
|
|
26
|
+
* Child options for this node in the hierarchy.
|
|
27
|
+
*/
|
|
28
|
+
children?: HierarchicalFilterOption[];
|
|
29
|
+
/**
|
|
30
|
+
* The nesting level of this option (0 for root).
|
|
31
|
+
* Added during hierarchy processing.
|
|
32
|
+
*/
|
|
33
|
+
level?: number;
|
|
34
|
+
}
|
|
21
35
|
export interface DefaultFilterProps<TReturnType> extends FilterViewProps<TReturnType> {
|
|
22
36
|
/**
|
|
23
37
|
* An array of filter options to be displayed.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { FilterDefinition, FilterStateSetterCallback, ValueName } from "../../types/FilterTypes";
|
|
2
|
+
import { DefaultFilterProps, HierarchicalFilterOption } from "../DefaultFilterTypes";
|
|
3
|
+
/**
|
|
4
|
+
* Props for the HierarchicalCheckboxFilter component.
|
|
5
|
+
* Extends base filter props but requires hierarchical options and ValueName[] type.
|
|
6
|
+
*/
|
|
7
|
+
export interface HierarchicalFilterProps extends Omit<DefaultFilterProps<ValueName[]>, "options" | "customSearch" | "showRequestMoreUseSearch" | "showUndefinedOptionWithCountAtBottom" | "totalRecords"> {
|
|
8
|
+
/**
|
|
9
|
+
* An array of hierarchical filter options to be displayed.
|
|
10
|
+
* The structure (parent-child relationships and levels) should be pre-processed.
|
|
11
|
+
*/
|
|
12
|
+
options: HierarchicalFilterOption[];
|
|
13
|
+
/**
|
|
14
|
+
* The filter definition, constrained to the 'valueNameArray' type for this component.
|
|
15
|
+
*/
|
|
16
|
+
filterDefinition: FilterDefinition & {
|
|
17
|
+
type: "valueNameArray";
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Callback function to update the filter's value state.
|
|
21
|
+
*/
|
|
22
|
+
setValue: FilterStateSetterCallback<ValueName[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Enable cascading selection behavior.
|
|
25
|
+
* - Checking/unchecking a parent affects all descendants.
|
|
26
|
+
* - Parent shows indeterminate state if only some children are checked.
|
|
27
|
+
*
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
cascadeSelection?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Optional prop to enable server-side searching.
|
|
33
|
+
* - If provided, the component assumes filtering is handled externally (server-side).
|
|
34
|
+
* It will use the provided `value` for the search input and call `onChange` when it changes.
|
|
35
|
+
* Client-side filtering using `filterHierarchicalOptions` will be skipped.
|
|
36
|
+
* - If not provided, the component will handle filtering client-side using internal state
|
|
37
|
+
* and the `filterHierarchicalOptions` function.
|
|
38
|
+
*/
|
|
39
|
+
customSearch?: {
|
|
40
|
+
/**
|
|
41
|
+
* Current search string
|
|
42
|
+
*/
|
|
43
|
+
value: string;
|
|
44
|
+
/**
|
|
45
|
+
* Callback when search string changed
|
|
46
|
+
*
|
|
47
|
+
* @param value Updated search string
|
|
48
|
+
*/
|
|
49
|
+
onChange: (value: string) => void;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `HierarchicalCheckboxFilter` is a React component for handling hierarchical checkbox filters.
|
|
54
|
+
* It expects options to be pre-processed into a hierarchical structure.
|
|
55
|
+
*
|
|
56
|
+
* Supports optional cascading selection behavior via the `cascadeSelection` prop.
|
|
57
|
+
*/
|
|
58
|
+
export declare const HierarchicalCheckboxFilter: ({ filterDefinition, filterBarActions, options, loading, setValue, value, cascadeSelection, customSearch, }: HierarchicalFilterProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HierarchicalFilterOption } from "../DefaultFilterTypes";
|
|
2
|
+
/**
|
|
3
|
+
* Recursively gets all descendant keys for a given hierarchical option, including the option itself.
|
|
4
|
+
*
|
|
5
|
+
* @param option The starting hierarchical filter option.
|
|
6
|
+
* @returns An array of string keys representing the option and all its descendants.
|
|
7
|
+
*/
|
|
8
|
+
export declare const getAllDescendantKeys: (option: HierarchicalFilterOption) => string[];
|
|
9
|
+
/**
|
|
10
|
+
* Recursively filters hierarchical options based on a search term.
|
|
11
|
+
* Keeps an option if its label matches or if any descendant matches.
|
|
12
|
+
*/
|
|
13
|
+
export declare const filterHierarchicalOptions: (options: HierarchicalFilterOption[], searchTerm: string, parentMatches?: boolean) => HierarchicalFilterOption[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useAnalytics } from "@trackunit/react-core-hooks";
|
|
2
|
+
import { ValueName } from "../../types/FilterTypes";
|
|
3
|
+
import { HierarchicalFilterOption } from "../DefaultFilterTypes";
|
|
4
|
+
import { HierarchicalFilterProps } from "./HierarchicalCheckboxFilter";
|
|
5
|
+
interface RenderHierarchicalOptionProps {
|
|
6
|
+
option: HierarchicalFilterOption;
|
|
7
|
+
filterDefinition: HierarchicalFilterProps["filterDefinition"];
|
|
8
|
+
filterBarActions: HierarchicalFilterProps["filterBarActions"];
|
|
9
|
+
setValue: HierarchicalFilterProps["setValue"];
|
|
10
|
+
logEvent: ReturnType<typeof useAnalytics>["logEvent"];
|
|
11
|
+
selectedValues: ValueName[];
|
|
12
|
+
cascadeSelection: boolean;
|
|
13
|
+
optionsMap: Map<string, HierarchicalFilterOption>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Renders a single option and its children recursively.
|
|
17
|
+
* Uses the `useHierarchicalOptionState` hook for state management and interaction logic.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RenderHierarchicalOption: ({ option, filterDefinition, filterBarActions, setValue, logEvent, selectedValues, cascadeSelection, optionsMap, }: RenderHierarchicalOptionProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useAnalytics } from "@trackunit/react-core-hooks";
|
|
2
|
+
import { ValueName } from "../../types/FilterTypes";
|
|
3
|
+
import { HierarchicalFilterOption } from "../DefaultFilterTypes";
|
|
4
|
+
import { HierarchicalFilterProps } from "./HierarchicalCheckboxFilter";
|
|
5
|
+
interface UseHierarchicalOptionStateProps {
|
|
6
|
+
options: HierarchicalFilterOption[];
|
|
7
|
+
option: HierarchicalFilterOption;
|
|
8
|
+
selectedValues: ValueName[];
|
|
9
|
+
cascadeSelection: boolean;
|
|
10
|
+
optionsMap: Map<string, HierarchicalFilterOption>;
|
|
11
|
+
setValue: HierarchicalFilterProps["setValue"];
|
|
12
|
+
filterDefinition: HierarchicalFilterProps["filterDefinition"];
|
|
13
|
+
logEvent: ReturnType<typeof useAnalytics>["logEvent"];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Custom hook to manage the state and interactions of a single hierarchical option.
|
|
17
|
+
* Calculates checked/indeterminate state and provides a handler for value changes,
|
|
18
|
+
* including cascading logic.
|
|
19
|
+
*/
|
|
20
|
+
export declare const useHierarchicalOptionState: ({ options, option, selectedValues, cascadeSelection, optionsMap, setValue, filterDefinition, logEvent, }: UseHierarchicalOptionStateProps) => {
|
|
21
|
+
isChecked: boolean;
|
|
22
|
+
isIndeterminate: boolean;
|
|
23
|
+
handleSelect: () => void;
|
|
24
|
+
};
|
|
25
|
+
export {};
|