@trackunit/filters-filter-bar 1.3.40 → 1.3.43
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 +417 -329
- package/index.esm.js +417 -330
- package/package.json +10 -10
- package/src/lib/components/FilterHeader.d.ts +3 -6
- package/src/lib/hooks/mockFilterBar.d.ts +42 -2
- package/src/lib/hooks/types.d.ts +15 -0
- package/src/lib/hooks/useFilterBar.d.ts +7 -62
- package/src/lib/hooks/useFilterBarAsync.d.ts +13 -0
- package/src/lib/hooks/useSearchParamAsFilter.d.ts +3 -3
- package/src/lib/hooks/utils/useFilterBarActions.d.ts +20 -0
- package/src/lib/hooks/utils/useFilterBarPersistence.d.ts +17 -0
- package/src/lib/hooks/utils/useGenericSetValue.d.ts +11 -0
- package/src/lib/index.d.ts +1 -0
- package/src/lib/types/FilterTypes.d.ts +8 -12
- package/src/lib/utils/createFilterSetters.d.ts +5 -0
- package/src/lib/utils/createFilterValues.d.ts +5 -0
- package/src/lib/utils/createInitialState.d.ts +5 -2
- package/src/lib/utils/validateFilter.d.ts +5 -1
package/index.cjs.js
CHANGED
|
@@ -367,13 +367,13 @@ const DynamicFilterList = ({ rowCount, keyMapper, labelMapper, onChange, checked
|
|
|
367
367
|
*
|
|
368
368
|
* @returns {ReactElement} - Returns the FilterHeader component.
|
|
369
369
|
*/
|
|
370
|
-
const FilterHeader = ({ filterKey, title, searchEnabled, searchProps,
|
|
370
|
+
const FilterHeader = ({ filterKey, title, searchEnabled, searchProps, filterHasChanges, resetIndividualFilterToInitialState, onResetFilter, loading = false, children, className, dataTestId, }) => {
|
|
371
371
|
const [t] = useTranslation();
|
|
372
372
|
const handleResetFilter = () => {
|
|
373
373
|
resetIndividualFilterToInitialState(filterKey);
|
|
374
374
|
onResetFilter?.();
|
|
375
375
|
};
|
|
376
|
-
return (jsxRuntime.jsxs(reactFilterComponents.FilterHeader, { className: className, dataTestId: dataTestId ?? `${filterKey}-filter-header`, loading: loading, onReset: handleResetFilter, resetLabel: t("filtersBar.resetFilter"), showReset:
|
|
376
|
+
return (jsxRuntime.jsxs(reactFilterComponents.FilterHeader, { className: className, dataTestId: dataTestId ?? `${filterKey}-filter-header`, loading: loading, onReset: handleResetFilter, resetLabel: t("filtersBar.resetFilter"), showReset: filterHasChanges, title: title, children: [searchEnabled ? (jsxRuntime.jsx(reactFormComponents.Search, { autoFocus: true, fieldSize: "small", id: `${filterKey}-search`, onChange: e => searchProps.onChange(e.currentTarget.value), onKeyDown: e => {
|
|
377
377
|
if (e.key === "Enter" && searchProps.onEnter) {
|
|
378
378
|
searchProps.onEnter(searchProps.value);
|
|
379
379
|
}
|
|
@@ -502,7 +502,7 @@ const DefaultCheckboxFilter = ({ filterDefinition, filterBarActions, options, lo
|
|
|
502
502
|
setMultipleValues(selectValues);
|
|
503
503
|
}
|
|
504
504
|
};
|
|
505
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(FilterHeader, { ...filterDefinition, ...filterBarActions, loading: loading, searchEnabled: true, searchProps: {
|
|
505
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(FilterHeader, { ...filterDefinition, ...filterBarActions, filterHasChanges: filterBarActions.appliedFilterKeys().includes(filterDefinition.filterKey), loading: loading, searchEnabled: true, searchProps: {
|
|
506
506
|
value: customSearch?.value ?? searchText,
|
|
507
507
|
onChange: customSearch?.onChange ?? setSearchText,
|
|
508
508
|
count: undefinedCount ? filteredOptions.length - 1 : filteredOptions.length,
|
|
@@ -587,8 +587,8 @@ const DefaultMinMaxFilter = ({ filterDefinition, filterName, value, setValue, fi
|
|
|
587
587
|
}, [value]);
|
|
588
588
|
const { logEvent } = reactCoreHooks.useAnalytics(FilterEvents);
|
|
589
589
|
const handleApply = () => {
|
|
590
|
-
const realMinValue = minValue === 0 ? undefined : minValue ?? undefined;
|
|
591
|
-
const realMaxValue = maxValue === 0 ? undefined : maxValue ?? undefined;
|
|
590
|
+
const realMinValue = minValue === 0 ? undefined : (minValue ?? undefined);
|
|
591
|
+
const realMaxValue = maxValue === 0 ? undefined : (maxValue ?? undefined);
|
|
592
592
|
logEvent("Filters Applied - V2", {
|
|
593
593
|
type: filterName ?? `${stringTs.capitalize(filterDefinition.filterKey)}Filter`,
|
|
594
594
|
value: JSON.stringify({ min: realMinValue, max: realMaxValue }),
|
|
@@ -597,7 +597,7 @@ const DefaultMinMaxFilter = ({ filterDefinition, filterName, value, setValue, fi
|
|
|
597
597
|
return realMinValue || realMaxValue ? { min: realMinValue, max: realMaxValue } : {};
|
|
598
598
|
});
|
|
599
599
|
};
|
|
600
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactFilterComponents.FilterHeader, { onReset: () => filterBarActions.resetIndividualFilterToInitialState(filterDefinition.filterKey), resetLabel: t("filtersBar.resetFilter"), showReset: filterBarActions.
|
|
600
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactFilterComponents.FilterHeader, { onReset: () => filterBarActions.resetIndividualFilterToInitialState(filterDefinition.filterKey), resetLabel: t("filtersBar.resetFilter"), showReset: filterBarActions.appliedFilterKeys().includes(filterDefinition.filterKey), title: filterDefinition.title }), jsxRuntime.jsxs(reactFilterComponents.FilterBody, { children: [jsxRuntime.jsxs("div", { className: "flex gap-4 px-1", children: [jsxRuntime.jsx(reactFormComponents.NumberField, { addonAfter: unit, className: "w-40", label: t("filtersBar.defaultMinMaxFilters.min"), max: filterDefinition.type === "minMax" ? filterDefinition.maximumNumber : undefined, min: filterDefinition.type === "minMax" ? filterDefinition.minimumNumber : undefined, onChange: e => setMinValue(e.target.value === "" ? undefined : Number(e.target.value)), value: minValue ?? "" }), jsxRuntime.jsx(reactFormComponents.NumberField, { addonAfter: unit, className: "w-40", label: t("filtersBar.defaultMinMaxFilters.max"), max: filterDefinition.type === "minMax" ? filterDefinition.maximumNumber : undefined, min: filterDefinition.type === "minMax" ? filterDefinition.minimumNumber : undefined, onChange: e => setMaxValue(e.target.value === "" ? undefined : Number(e.target.value)), value: maxValue ?? "" })] }), jsxRuntime.jsx(reactFilterComponents.FilterFooter, { children: jsxRuntime.jsx(reactComponents.Button, { onClick: handleApply, size: "small", variant: "ghost", children: t("filtersBar.defaultMinMaxFilters.apply") }) })] })] }));
|
|
601
601
|
};
|
|
602
602
|
|
|
603
603
|
/**
|
|
@@ -619,7 +619,7 @@ const DefaultRadioFilter = ({ filterDefinition, filterBarActions, options, loadi
|
|
|
619
619
|
}
|
|
620
620
|
};
|
|
621
621
|
const selectedRadioId = filteredOptions.find(option => filterBarActions.objectIncludesValue(filterDefinition.filterKey, option.key));
|
|
622
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(FilterHeader, { ...filterBarActions, ...filterDefinition, loading: loading, searchEnabled: true, searchProps: {
|
|
622
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(FilterHeader, { ...filterBarActions, ...filterDefinition, filterHasChanges: filterBarActions.appliedFilterKeys().includes(filterDefinition.filterKey), loading: loading, searchEnabled: true, searchProps: {
|
|
623
623
|
value: customSearch?.value ?? searchText,
|
|
624
624
|
onChange: customSearch?.onChange ?? setSearchText,
|
|
625
625
|
count: filteredOptions.length,
|
|
@@ -647,7 +647,7 @@ const useStarredGroupFilters = (filterDefinitions, hiddenFilters) => {
|
|
|
647
647
|
}))
|
|
648
648
|
.filter(filter => filter.filters.length > 0);
|
|
649
649
|
}, [filterDefinitions, hiddenFilters, t]);
|
|
650
|
-
return { filtersGrouped };
|
|
650
|
+
return react.useMemo(() => ({ filtersGrouped }), [filtersGrouped]);
|
|
651
651
|
};
|
|
652
652
|
const uniqueKeysFromGroups = (filters) => [...new Set(filters.map(filter => filter.group))];
|
|
653
653
|
|
|
@@ -779,11 +779,13 @@ const StarredFiltersMenu = ({ filterBarDefinition, updateStarredFilters, starred
|
|
|
779
779
|
...hideInStarredMenu,
|
|
780
780
|
...hiddenFilters,
|
|
781
781
|
]);
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
782
|
+
const hiddenFiltersCount = react.useMemo(() => {
|
|
783
|
+
const nonHiddenStarredFilterKeys = starredFilterKeys.filter(key => !hideInStarredMenu.includes(key));
|
|
784
|
+
return (filtersGrouped.map(group => group.filters).flat().length +
|
|
785
|
+
hiddenFilters.length -
|
|
786
|
+
nonHiddenStarredFilterKeys.length +
|
|
787
|
+
numberOfShowDirectlyFilters);
|
|
788
|
+
}, [filtersGrouped, hiddenFilters, starredFilterKeys, hideInStarredMenu, numberOfShowDirectlyFilters]);
|
|
787
789
|
const getHiddenFiltersLabel = () => {
|
|
788
790
|
switch (hiddenFiltersCount) {
|
|
789
791
|
case 0:
|
|
@@ -822,13 +824,13 @@ const StarredFiltersMenu = ({ filterBarDefinition, updateStarredFilters, starred
|
|
|
822
824
|
const StarredFilters = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], compact, dataTestId, className, }) => {
|
|
823
825
|
const [t] = useTranslation();
|
|
824
826
|
const { isLg } = reactComponents.useViewportBreakpoints();
|
|
825
|
-
const isCompactMode = compact ?? !isLg;
|
|
827
|
+
const isCompactMode = react.useMemo(() => compact ?? !isLg, [compact, isLg]);
|
|
826
828
|
const hideInMenu = react.useMemo(() => {
|
|
827
829
|
return sharedUtils.objectValues(filterBarDefinition)
|
|
828
830
|
.map(filter => {
|
|
829
831
|
const showInFilterBar = filter.showInFilterBar ? filter.showInFilterBar() : true;
|
|
830
832
|
const showInStarredMenu = filter.showInStarredMenu ? filter.showInStarredMenu() : true;
|
|
831
|
-
const showMenuAnywayBecauseFilterHasChanged = filterBarConfig.
|
|
833
|
+
const showMenuAnywayBecauseFilterHasChanged = filterBarConfig.appliedFilterKeys().includes(filter.filterKey);
|
|
832
834
|
return (!showInFilterBar || !showInStarredMenu) && !showMenuAnywayBecauseFilterHasChanged
|
|
833
835
|
? filter.filterKey
|
|
834
836
|
: null;
|
|
@@ -839,17 +841,21 @@ const StarredFilters = ({ filterBarDefinition, filterBarConfig, hiddenFilters =
|
|
|
839
841
|
...hideInMenu,
|
|
840
842
|
...hiddenFilters,
|
|
841
843
|
]);
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
844
|
+
const { appliedFilters, filtersToShow, showDirectlyFilters } = react.useMemo(() => {
|
|
845
|
+
const allFilters = filtersGrouped.map(group => group.filters).flat();
|
|
846
|
+
const starredFilters = allFilters.filter(filter => {
|
|
847
|
+
return (filterBarConfig.starredFilterKeys.includes(filter.filterKey) &&
|
|
848
|
+
!filter.showDirectly);
|
|
849
|
+
});
|
|
850
|
+
return {
|
|
851
|
+
appliedFilters: starredFilters.filter(filter => filterBarConfig.appliedFilterKeys().includes(filter.filterKey)),
|
|
852
|
+
filtersToShow: starredFilters.filter(filter => !filter.showDirectly),
|
|
853
|
+
showDirectlyFilters: allFilters.filter(filter => filter.showDirectly),
|
|
854
|
+
};
|
|
855
|
+
}, [filterBarConfig, filtersGrouped]);
|
|
856
|
+
return (jsxRuntime.jsxs("div", { className: tailwindMerge.twMerge("flex flex-wrap items-center gap-2", className), "data-testid": dataTestId, children: [jsxRuntime.jsx(reactComponents.Popover, { placement: "bottom-start", children: modalState => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx("div", { "data-testid": "starred-filters-menu-trigger", children: jsxRuntime.jsxs(reactComponents.Tooltip, { disabled: !isCompactMode || modalState.isOpen, label: jsxRuntime.jsx(FilterButtonTooltipLabel, { filterBarConfig: filterBarConfig }), children: [jsxRuntime.jsx(reactComponents.Button, { className: "@xs:flex hidden", prefix: jsxRuntime.jsx(reactComponents.Icon, { color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Funnel", size: "small" }), size: "small", suffix: isCompactMode && filterBarConfig.appliedFilterKeys().length > 0
|
|
857
|
+
? `(${filterBarConfig.appliedFilterKeys().length})`
|
|
858
|
+
: undefined, variant: "secondary", children: t("filtersBar.filtersHeading") }), jsxRuntime.jsx(reactComponents.IconButton, { className: "@xs:hidden", icon: jsxRuntime.jsx(reactComponents.Icon, { color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Funnel", size: "small" }), size: "small", variant: "secondary" })] }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { cellPadding: 100, children: jsxRuntime.jsxs(reactComponents.Card, { className: "max-h-[min(600px,_calc(100dvh-32px))] overflow-hidden sm:w-[350px]", children: [filtersToShow.length > 0 ? (jsxRuntime.jsx(reactComponents.CardBody, { density: "dense", children: jsxRuntime.jsx("div", { className: "flex h-full min-w-min flex-col gap-2", children: jsxRuntime.jsx(FiltersList, { filterBarConfig: filterBarConfig, filters: filtersToShow }) }) })) : null, jsxRuntime.jsxs(reactComponents.CardFooter, { className: filtersToShow.length === 0 ? "border-none" : undefined, density: "dense", children: [jsxRuntime.jsx(StarredFiltersMenu, { className: "mr-auto", filterBarDefinition: filterBarDefinition, hiddenFilters: hiddenFilters, starredFilterKeys: filterBarConfig.starredFilterKeys, updateStarredFilters: filterBarConfig.updateStarredFilters }), !isCompactMode ? null : (jsxRuntime.jsx(ResetFiltersButton, { filtersHaveBeenApplied: filterBarConfig.appliedFilterKeys().length > 0, resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState }))] })] }) })] })) }), showDirectlyFilters.length > 0 ? (jsxRuntime.jsx(FiltersList, { filterBarConfig: filterBarConfig, filters: showDirectlyFilters })) : null, !isCompactMode ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [appliedFilters.filter(filter => !filter.showDirectly).length > 0 ? (jsxRuntime.jsx("div", { className: "h-4 w-[1px] bg-slate-300" })) : null, jsxRuntime.jsx(FiltersList, { filterBarConfig: filterBarConfig, filters: appliedFilters }), jsxRuntime.jsx(ResetFiltersButton, { filtersHaveBeenApplied: filterBarConfig.appliedFilterKeys().length > 0, resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })] })) : null] }));
|
|
853
859
|
};
|
|
854
860
|
const FiltersList = ({ filters, filterBarConfig }) => {
|
|
855
861
|
return filters.length === 0
|
|
@@ -860,17 +866,17 @@ const FiltersList = ({ filters, filterBarConfig }) => {
|
|
|
860
866
|
};
|
|
861
867
|
const FilterButtonTooltipLabel = ({ filterBarConfig, }) => {
|
|
862
868
|
const [t] = useTranslation();
|
|
863
|
-
switch (filterBarConfig.appliedFilterKeys.length) {
|
|
869
|
+
switch (filterBarConfig.appliedFilterKeys().length) {
|
|
864
870
|
case 0:
|
|
865
871
|
return t("filtersBar.appliedFiltersTooltip.none");
|
|
866
872
|
case 1:
|
|
867
|
-
return filterBarConfig.appliedFilterKeys[0]
|
|
873
|
+
return filterBarConfig.appliedFilterKeys()[0]
|
|
868
874
|
? t("filtersBar.appliedFiltersTooltip.singular", {
|
|
869
|
-
filterName: filterBarConfig.getFilterTitle(filterBarConfig.appliedFilterKeys[0]),
|
|
875
|
+
filterName: filterBarConfig.getFilterTitle(filterBarConfig.appliedFilterKeys()[0]),
|
|
870
876
|
})
|
|
871
877
|
: null; // should never happen though
|
|
872
878
|
default:
|
|
873
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [t("filtersBar.appliedFiltersTooltip.plural", { count: filterBarConfig.appliedFilterKeys.length }), jsxRuntime.jsx("ul", { className: "list-inside", children: filterBarConfig.appliedFilterKeys.map((appliedFilterKey, index) => (jsxRuntime.jsx("li", { className: "list-disc", children: filterBarConfig.getFilterTitle(appliedFilterKey) }, index))) })] }));
|
|
879
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [t("filtersBar.appliedFiltersTooltip.plural", { count: filterBarConfig.appliedFilterKeys().length }), jsxRuntime.jsx("ul", { className: "list-inside", children: filterBarConfig.appliedFilterKeys().map((appliedFilterKey, index) => (jsxRuntime.jsx("li", { className: "list-disc", children: filterBarConfig.getFilterTitle(appliedFilterKey) }, index))) })] }));
|
|
874
880
|
}
|
|
875
881
|
};
|
|
876
882
|
|
|
@@ -887,11 +893,11 @@ const doNothing = (args) => { };
|
|
|
887
893
|
const mockFilterBar = {
|
|
888
894
|
filterBarConfig: {
|
|
889
895
|
isFilterIncludedByKey: doNothing,
|
|
890
|
-
appliedFilterKeys: [],
|
|
896
|
+
appliedFilterKeys: () => [],
|
|
891
897
|
arrayIncludesValue: doNothing,
|
|
892
898
|
getFilterTitle: doNothing,
|
|
893
899
|
getFilterBarName: doNothing,
|
|
894
|
-
initialState: {
|
|
900
|
+
initialState: { customerType: [] },
|
|
895
901
|
name: "test",
|
|
896
902
|
objectArrayIncludesValue: doNothing,
|
|
897
903
|
resetFiltersToInitialState: doNothing,
|
|
@@ -908,7 +914,6 @@ const mockFilterBar = {
|
|
|
908
914
|
toggleArrayObjectValue: doNothing,
|
|
909
915
|
toggleArrayValue: doNothing,
|
|
910
916
|
values: { customerType: [] },
|
|
911
|
-
filterHasChanged: doNothing,
|
|
912
917
|
starredFilterKeys: [],
|
|
913
918
|
getValuesByKey: doNothing,
|
|
914
919
|
setters: {
|
|
@@ -919,10 +924,35 @@ const mockFilterBar = {
|
|
|
919
924
|
setObjectValue: doNothing,
|
|
920
925
|
toggleObjectValue: doNothing,
|
|
921
926
|
},
|
|
922
|
-
dataLoaded: doNothing(),
|
|
923
927
|
filterBarDefinition: {},
|
|
928
|
+
name: "test",
|
|
929
|
+
onValuesChange: doNothing,
|
|
924
930
|
};
|
|
925
931
|
|
|
932
|
+
/**
|
|
933
|
+
*
|
|
934
|
+
*/
|
|
935
|
+
const createFilterSetters = (mainFilters, setValue) => mainFilters.reduce((prev, curr) => {
|
|
936
|
+
const key = curr.filterKey;
|
|
937
|
+
return {
|
|
938
|
+
...prev,
|
|
939
|
+
[`set${stringTs.capitalize(key)}`]: (callback) => setValue(key, callback),
|
|
940
|
+
};
|
|
941
|
+
// eslint-disable-next-line local-rules/no-typescript-assertion
|
|
942
|
+
}, {});
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
*
|
|
946
|
+
*/
|
|
947
|
+
const createFilterValues = (mainFilters, useDefaultValues = false) => mainFilters.reduce((prev, curr) => {
|
|
948
|
+
const key = curr.filterKey;
|
|
949
|
+
const type = curr.type;
|
|
950
|
+
return {
|
|
951
|
+
...prev,
|
|
952
|
+
[key]: useDefaultValues ? (curr.defaultValue ?? getInitialValueFromType(type)) : getInitialValueFromType(type),
|
|
953
|
+
};
|
|
954
|
+
}, {});
|
|
955
|
+
|
|
926
956
|
/**
|
|
927
957
|
* A helper function that returns a default value based on the filter type.
|
|
928
958
|
*
|
|
@@ -955,253 +985,40 @@ const getInitialValueFromType = (type) => {
|
|
|
955
985
|
* @template TFilterBarDefinition - The type representing the filter bar definition.
|
|
956
986
|
* @returns {FilterBarConfig<TFilterBarDefinition>} - Returns an initial filter bar configuration object.
|
|
957
987
|
*/
|
|
958
|
-
const createInitialState = (name, mainFilters,
|
|
988
|
+
const createInitialState = ({ name, mainFilters, setValue, }) => {
|
|
959
989
|
const defaultStarredKeys = mainFilters
|
|
960
990
|
.filter(f => f.default)
|
|
991
|
+
// eslint-disable-next-line local-rules/no-typescript-assertion
|
|
961
992
|
.map(f => f.filterKey);
|
|
962
|
-
const values = mainFilters
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
return {
|
|
966
|
-
...prev,
|
|
967
|
-
[key]:
|
|
968
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
969
|
-
"filtered" in initialState && initialState.filtered[key]
|
|
970
|
-
? initialState.filtered[key]
|
|
971
|
-
: (curr.defaultValue ?? getInitialValueFromType(type)),
|
|
972
|
-
};
|
|
973
|
-
}, {});
|
|
974
|
-
const setters = mainFilters.reduce((prev, curr) => {
|
|
975
|
-
const key = curr.filterKey;
|
|
976
|
-
return {
|
|
977
|
-
...prev,
|
|
978
|
-
[`set${stringTs.capitalize(key)}`]: (callback) => setValue(key, callback),
|
|
979
|
-
};
|
|
980
|
-
}, {});
|
|
993
|
+
const values = createFilterValues(mainFilters, true);
|
|
994
|
+
const initialState = createFilterValues(mainFilters);
|
|
995
|
+
const setters = createFilterSetters(mainFilters, setValue);
|
|
981
996
|
return {
|
|
982
997
|
name,
|
|
983
|
-
initialState: { filtered: values, empty: "empty" in initialState ? initialState.empty : {} },
|
|
984
998
|
starredFilterKeys: defaultStarredKeys,
|
|
985
999
|
values,
|
|
986
1000
|
setters,
|
|
1001
|
+
initialState,
|
|
987
1002
|
};
|
|
988
1003
|
};
|
|
989
1004
|
|
|
990
|
-
const areaFilterGeoJsonGeometrySchema = zod.z.union([geoJsonUtils.geoJsonPolygonSchema, geoJsonUtils.geoJsonMultiPolygonSchema]);
|
|
991
|
-
|
|
992
|
-
const hasValue = (value) => {
|
|
993
|
-
if (value === undefined || value === null) {
|
|
994
|
-
return false;
|
|
995
|
-
}
|
|
996
|
-
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
997
|
-
if (typeof value === "object" && Object.keys(value).length === 0) {
|
|
998
|
-
return false;
|
|
999
|
-
}
|
|
1000
|
-
return !(Array.isArray(value) && value.length === 0);
|
|
1001
|
-
};
|
|
1002
|
-
const isNotRightType = (filterDefinition, foundFilter) => {
|
|
1003
|
-
return ((filterDefinition.type === "valueNameArray" && !isValueNameArray(foundFilter)) ||
|
|
1004
|
-
(filterDefinition.type === "valueName" && !isValueName(foundFilter)) ||
|
|
1005
|
-
(filterDefinition.type === "stringArray" && !isStringArrayFilterValue(foundFilter)) ||
|
|
1006
|
-
(filterDefinition.type === "dateRange" && !isDateRangeValue(foundFilter)) ||
|
|
1007
|
-
(filterDefinition.type === "area" && !isAreaFilterValue(foundFilter)) ||
|
|
1008
|
-
(filterDefinition.type === "minMax" && !isMinMaxFilterValue(foundFilter)) ||
|
|
1009
|
-
(filterDefinition.type === "boolean" && !isBooleanValue(foundFilter)) ||
|
|
1010
|
-
(filterDefinition.type === "string" && typeof foundFilter !== "string") ||
|
|
1011
|
-
(filterDefinition.type === "number" && typeof foundFilter !== "number"));
|
|
1012
|
-
};
|
|
1013
|
-
/**
|
|
1014
|
-
*
|
|
1015
|
-
*/
|
|
1016
|
-
const isMinMaxFilterValue = (value) => {
|
|
1017
|
-
return value
|
|
1018
|
-
? // eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1019
|
-
typeof value === "object" && (Object.keys(value).includes("min") || Object.keys(value).includes("max"))
|
|
1020
|
-
: false;
|
|
1021
|
-
};
|
|
1022
|
-
/**
|
|
1023
|
-
*
|
|
1024
|
-
*/
|
|
1025
|
-
const isDateRangeValue = (value) => {
|
|
1026
|
-
return value
|
|
1027
|
-
? // eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1028
|
-
typeof value === "object" && (Object.keys(value).includes("from") || Object.keys(value).includes("to"))
|
|
1029
|
-
: false;
|
|
1030
|
-
};
|
|
1031
|
-
/**
|
|
1032
|
-
* {
|
|
1033
|
-
type: "Polygon";
|
|
1034
|
-
coordinates: [number, number][][];
|
|
1035
|
-
}
|
|
1036
|
-
*/
|
|
1037
|
-
const isAreaFilterValue = (value) => {
|
|
1038
|
-
return areaFilterGeoJsonGeometrySchema.safeParse(value).success;
|
|
1039
|
-
};
|
|
1040
|
-
/**
|
|
1041
|
-
*
|
|
1042
|
-
*/
|
|
1043
|
-
const isArrayFilterValue = (value) => {
|
|
1044
|
-
return Array.isArray(value);
|
|
1045
|
-
};
|
|
1046
|
-
/**
|
|
1047
|
-
*
|
|
1048
|
-
*/
|
|
1049
|
-
const isStringArrayFilterValue = (value) => {
|
|
1050
|
-
return isArrayFilterValue(value) && value.every(item => typeof item === "string");
|
|
1051
|
-
};
|
|
1052
|
-
/**
|
|
1053
|
-
*
|
|
1054
|
-
*/
|
|
1055
|
-
const isBooleanValue = (value) => {
|
|
1056
|
-
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1057
|
-
return value ? typeof value === "object" && Object.keys(value).includes("booleanValue") : false;
|
|
1058
|
-
};
|
|
1059
|
-
/**
|
|
1060
|
-
* Type guard to check if a value is a single ValueName object
|
|
1061
|
-
*/
|
|
1062
|
-
const isValueName = (value) => {
|
|
1063
|
-
return (typeof value === "object" &&
|
|
1064
|
-
value !== null &&
|
|
1065
|
-
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1066
|
-
Object.keys(value).includes("name") &&
|
|
1067
|
-
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1068
|
-
Object.keys(value).includes("value"));
|
|
1069
|
-
};
|
|
1070
|
-
/**
|
|
1071
|
-
* Type guard to check if a value is an array of ValueName objects
|
|
1072
|
-
*/
|
|
1073
|
-
const isValueNameArray = (value) => {
|
|
1074
|
-
return isArrayFilterValue(value) && value.every(isValueName);
|
|
1075
|
-
};
|
|
1076
|
-
/**
|
|
1077
|
-
* Validates a filter configuration against filter definitions.
|
|
1078
|
-
*
|
|
1079
|
-
* @template TFilterBarDefinition - The type of the filter bar definition.
|
|
1080
|
-
* @param {FilterBarConfig<TFilterBarDefinition>} filter - The filter configuration to validate.
|
|
1081
|
-
* @param {FilterDefinition[]} filterDefinitions - An array of filter definitions to validate against.
|
|
1082
|
-
* @returns {boolean} - Returns `true` if the filter configuration is valid, otherwise `false`.
|
|
1083
|
-
*/
|
|
1084
|
-
const validateFilter = (filter, filterDefinitions) => {
|
|
1085
|
-
const stateKeys = [];
|
|
1086
|
-
let inBadState = false;
|
|
1087
|
-
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1088
|
-
for (const key of Object.keys(filter?.values || {})) {
|
|
1089
|
-
if (filterDefinitions.find(filterDefinition => filterDefinition.filterKey === key)) {
|
|
1090
|
-
stateKeys.push(key);
|
|
1091
|
-
}
|
|
1092
|
-
else {
|
|
1093
|
-
inBadState = true;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
filterDefinitions.forEach(filterDefinition => {
|
|
1097
|
-
const foundFilter = filter?.values && filter.values[filterDefinition.filterKey];
|
|
1098
|
-
if (filter) {
|
|
1099
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1100
|
-
if (foundFilter && hasValue(foundFilter) && isNotRightType(filterDefinition, foundFilter)) {
|
|
1101
|
-
inBadState = true;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
else {
|
|
1105
|
-
inBadState = true;
|
|
1106
|
-
}
|
|
1107
|
-
});
|
|
1108
|
-
stateKeys.sort((a, b) => a.localeCompare(b));
|
|
1109
|
-
const filterKeysNotEqual = !isEqual(stateKeys, filterDefinitions.map(f => f.filterKey).sort((a, b) => a.localeCompare(b)));
|
|
1110
|
-
return !(inBadState || filterKeysNotEqual);
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
1005
|
/**
|
|
1114
|
-
* Custom hook for managing a filter bar's
|
|
1006
|
+
* Custom hook for managing a filter bar's actions .
|
|
1115
1007
|
*
|
|
1116
1008
|
* @template TFilterBarDefinition - A generic type for the filter bar definition.
|
|
1117
1009
|
* @returns {object} An object containing filter bar configuration and actions.
|
|
1118
1010
|
*/
|
|
1119
|
-
const
|
|
1120
|
-
const [asyncLoadedFilterBarDefinitions, setAsyncLoadedFilterBarDefinitions] = react.useState();
|
|
1121
|
-
const internalFilterBarDefinitions = react.useMemo(() => asyncLoadedFilterBarDefinitions ?? filterBarDefinition, [filterBarDefinition, asyncLoadedFilterBarDefinitions]);
|
|
1122
|
-
const { clientSideUserId } = reactCoreHooks.useCurrentUser();
|
|
1123
|
-
const setValue = react.useCallback((key, callback) => {
|
|
1124
|
-
setFilterBarConfig(prevState => {
|
|
1125
|
-
return {
|
|
1126
|
-
...prevState,
|
|
1127
|
-
values: {
|
|
1128
|
-
...prevState.values,
|
|
1129
|
-
[key]: callback(prevState.values[key]),
|
|
1130
|
-
},
|
|
1131
|
-
};
|
|
1132
|
-
});
|
|
1133
|
-
}, []);
|
|
1134
|
-
const [initialStoredFilters] = react.useState(() => localStorage.getItem(`filter-${name}-${clientSideUserId}`) || "{}");
|
|
1135
|
-
const loadData = react.useCallback((updatedFilterDefinitionsValues) => {
|
|
1136
|
-
let initialFilterBarConfig;
|
|
1137
|
-
const storedFilters = initialStoredFilters;
|
|
1138
|
-
if (storedFilters && storedFilters !== "undefined") {
|
|
1139
|
-
const loadedFilterBarConfig = JSON.parse(storedFilters);
|
|
1140
|
-
if (validateFilter(loadedFilterBarConfig, updatedFilterDefinitionsValues)) {
|
|
1141
|
-
initialFilterBarConfig = {
|
|
1142
|
-
...loadedFilterBarConfig,
|
|
1143
|
-
initialState: initialState || loadedFilterBarConfig.initialState,
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
//WHY WE NEED THIS?
|
|
1148
|
-
//For filters that are not visible, and we want to set the default value to the initial state.
|
|
1149
|
-
//To do this for a changing default value as in customers and sites we would need to recreate the initialFilterBarConfig every time the default value changes.
|
|
1150
|
-
//This mean that filterbars that have this functionality wouldn't be able to save the state of the filterbar.
|
|
1151
|
-
//Another option would be to create a new filterbar for each customer or site. This also has its drawbacks. Would raise it with the frontend community.
|
|
1152
|
-
const hasNonVisibleDefaultValues = updatedFilterDefinitionsValues.some(value => value.showInStarredMenu &&
|
|
1153
|
-
!value.showInStarredMenu() &&
|
|
1154
|
-
value.showInFilterBar &&
|
|
1155
|
-
!value.showInFilterBar() &&
|
|
1156
|
-
value.defaultValue?.toString &&
|
|
1157
|
-
value.defaultValue.toString().length > 0);
|
|
1158
|
-
if (initialFilterBarConfig === undefined || hasNonVisibleDefaultValues) {
|
|
1159
|
-
initialFilterBarConfig = createInitialState(name, updatedFilterDefinitionsValues, initialState || {}, setValue);
|
|
1160
|
-
}
|
|
1161
|
-
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1162
|
-
Object.keys(initialFilterBarConfig.values).forEach(key => {
|
|
1163
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1164
|
-
initialFilterBarConfig.setters[`set${stringTs.capitalize(key)}`] = (callback) => setValue(key, callback);
|
|
1165
|
-
});
|
|
1166
|
-
return initialFilterBarConfig;
|
|
1167
|
-
}, [initialState, name, setValue, initialStoredFilters]);
|
|
1168
|
-
const dataLoaded = react.useCallback((loadedFilterDefinitionsValues) => {
|
|
1169
|
-
if (!loadAsync) {
|
|
1170
|
-
throw new Error("You must pass in loadAsync to useFilterBar when loading filter data asynchronously");
|
|
1171
|
-
}
|
|
1172
|
-
setAsyncLoadedFilterBarDefinitions(loadedFilterDefinitionsValues);
|
|
1173
|
-
setFilterBarConfig(prev => loadData(sharedUtils.objectValues(loadedFilterDefinitionsValues)));
|
|
1174
|
-
}, [loadAsync, loadData]);
|
|
1175
|
-
const [filterBarConfig, setFilterBarConfig] = react.useState(() => {
|
|
1176
|
-
let initialFilterBarConfig;
|
|
1177
|
-
if (!loadAsync) {
|
|
1178
|
-
initialFilterBarConfig = loadData(sharedUtils.objectValues(internalFilterBarDefinitions));
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
initialFilterBarConfig = createInitialState(name, sharedUtils.objectValues(internalFilterBarDefinitions), initialState || {}, setValue);
|
|
1182
|
-
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1183
|
-
Object.keys(initialFilterBarConfig.values).forEach(key => {
|
|
1184
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1185
|
-
initialFilterBarConfig.setters[`set${stringTs.capitalize(key)}`] = (callback) => setValue(key, callback);
|
|
1186
|
-
});
|
|
1187
|
-
}
|
|
1188
|
-
return initialFilterBarConfig;
|
|
1189
|
-
});
|
|
1190
|
-
react.useEffect(() => {
|
|
1191
|
-
onValuesChange?.(filterBarConfig.values);
|
|
1192
|
-
}, [filterBarConfig.values, filterBarConfig, onValuesChange]);
|
|
1193
|
-
react.useEffect(() => {
|
|
1194
|
-
localStorage.setItem(`filter-${name}-${clientSideUserId}`, JSON.stringify(filterBarConfig));
|
|
1195
|
-
}, [filterBarConfig, name, clientSideUserId]);
|
|
1011
|
+
const useFilterBarActions = ({ name, filterBarConfig, filterBarDefinition, setFilterBarConfig, setValue, initialState, }) => {
|
|
1196
1012
|
const filterMapGetter = react.useMemo(() => {
|
|
1197
1013
|
return {
|
|
1198
1014
|
getFilterBarName: () => {
|
|
1199
1015
|
return filterBarConfig.name;
|
|
1200
1016
|
},
|
|
1201
1017
|
getFilterTitle(key) {
|
|
1202
|
-
return
|
|
1018
|
+
return filterBarDefinition[key]?.title ?? key;
|
|
1203
1019
|
},
|
|
1204
1020
|
arrayIncludesValue(key, value) {
|
|
1021
|
+
// eslint-disable-next-line local-rules/no-typescript-assertion
|
|
1205
1022
|
const filter = filterBarConfig.values[key];
|
|
1206
1023
|
return filter?.includes(value) || false;
|
|
1207
1024
|
},
|
|
@@ -1212,58 +1029,37 @@ const useFilterBar = ({ name, onValuesChange, filterBarDefinition, loadAsync, in
|
|
|
1212
1029
|
const values = filterBarConfig.values[key];
|
|
1213
1030
|
return Boolean(values);
|
|
1214
1031
|
},
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
// eslint-disable-next-line
|
|
1219
|
-
|
|
1220
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1221
|
-
const currentFilters = JSON.parse(JSON.stringify(filterBarConfig.values || {}));
|
|
1222
|
-
return sharedUtils.objectKeys(currentFilters)
|
|
1032
|
+
appliedFilterKeys() {
|
|
1033
|
+
const initialStateEmptyValues = JSON.parse(JSON.stringify(initialState ? initialState : {}));
|
|
1034
|
+
const currentFilters = JSON.parse(JSON.stringify(filterBarConfig.values));
|
|
1035
|
+
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1036
|
+
return Object.keys(currentFilters)
|
|
1223
1037
|
.filter(filterKey => {
|
|
1224
|
-
const isFilterValueEqualToInitialStateValue = dequal.dequal(currentFilters[filterKey],
|
|
1225
|
-
const emptyStateValue = initialStateEmptyValues[filterKey];
|
|
1226
|
-
// If we passed an initialState's empty state, we have to compare whether this field is different
|
|
1227
|
-
// from the empty state. If the field is different from the empty state, it means that it is an active filter.
|
|
1228
|
-
if (emptyStateValue) {
|
|
1229
|
-
const isFilterValueEqualToEmptyStateValue = dequal.dequal(currentFilters[filterKey], emptyStateValue);
|
|
1230
|
-
return !isFilterValueEqualToEmptyStateValue;
|
|
1231
|
-
}
|
|
1232
|
-
// Otherwise, we need to check whether our filter's field value equals the initial state's field value
|
|
1233
|
-
// The initialState value is created based on the `initialState` passed to this hook, and some magic
|
|
1234
|
-
// done in the `createInitialState` function.
|
|
1038
|
+
const isFilterValueEqualToInitialStateValue = dequal.dequal(currentFilters[filterKey], initialStateEmptyValues[filterKey]);
|
|
1235
1039
|
return !isFilterValueEqualToInitialStateValue;
|
|
1236
1040
|
})
|
|
1237
1041
|
.map(key => String(key));
|
|
1238
1042
|
},
|
|
1239
|
-
filterHasChanged(key) {
|
|
1240
|
-
const initialStateFilteredValue = JSON.parse(
|
|
1241
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1242
|
-
JSON.stringify(filterBarConfig.initialState?.filtered?.[key] || {}));
|
|
1243
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1244
|
-
const currentFilter = JSON.parse(JSON.stringify(filterBarConfig.values[key] || {}));
|
|
1245
|
-
return !dequal.dequal(currentFilter, initialStateFilteredValue);
|
|
1246
|
-
},
|
|
1247
1043
|
objectArrayIncludesValue(key, value) {
|
|
1044
|
+
// eslint-disable-next-line local-rules/no-typescript-assertion
|
|
1248
1045
|
const filter = filterBarConfig.values[key];
|
|
1249
1046
|
return filter?.find(f => f.value === value) !== undefined || false;
|
|
1250
1047
|
},
|
|
1251
1048
|
objectIncludesValue(key, value) {
|
|
1049
|
+
// eslint-disable-next-line local-rules/no-typescript-assertion
|
|
1252
1050
|
const filter = filterBarConfig.values[key];
|
|
1253
1051
|
return filter?.value === value || false;
|
|
1254
1052
|
},
|
|
1255
1053
|
};
|
|
1256
|
-
}, [
|
|
1257
|
-
filterBarConfig.initialState.empty,
|
|
1258
|
-
filterBarConfig.initialState.filtered,
|
|
1259
|
-
filterBarConfig.name,
|
|
1260
|
-
filterBarConfig.values,
|
|
1261
|
-
internalFilterBarDefinitions,
|
|
1262
|
-
]);
|
|
1054
|
+
}, [filterBarDefinition, filterBarConfig.name, filterBarConfig.values, initialState]);
|
|
1263
1055
|
const filterMapActions = react.useMemo(() => {
|
|
1264
1056
|
// Reset an individual filter to its initial state
|
|
1265
1057
|
const resetIndividualFilterToInitialState = (key) => {
|
|
1266
|
-
const tmpInitialState = createInitialState(
|
|
1058
|
+
const tmpInitialState = createInitialState({
|
|
1059
|
+
name,
|
|
1060
|
+
mainFilters: sharedUtils.objectValues(filterBarDefinition),
|
|
1061
|
+
setValue,
|
|
1062
|
+
});
|
|
1267
1063
|
setFilterBarConfig(prevState => {
|
|
1268
1064
|
return {
|
|
1269
1065
|
...prevState,
|
|
@@ -1476,20 +1272,315 @@ const useFilterBar = ({ name, onValuesChange, filterBarDefinition, loadAsync, in
|
|
|
1476
1272
|
setFilterBarConfig(prevState => {
|
|
1477
1273
|
return {
|
|
1478
1274
|
...prevState,
|
|
1479
|
-
values: createInitialState(
|
|
1275
|
+
values: createInitialState({
|
|
1276
|
+
name,
|
|
1277
|
+
mainFilters: sharedUtils.objectValues(filterBarDefinition),
|
|
1278
|
+
setValue,
|
|
1279
|
+
}).values,
|
|
1480
1280
|
};
|
|
1481
1281
|
});
|
|
1482
1282
|
},
|
|
1483
1283
|
resetIndividualFilterToInitialState,
|
|
1484
1284
|
};
|
|
1485
|
-
}, [
|
|
1285
|
+
}, [name, setFilterBarConfig, setValue, filterBarDefinition]);
|
|
1286
|
+
return react.useMemo(() => ({ filterMapGetter, filterMapActions }), [filterMapGetter, filterMapActions]);
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
const areaFilterGeoJsonGeometrySchema = zod.z.union([geoJsonUtils.geoJsonPolygonSchema, geoJsonUtils.geoJsonMultiPolygonSchema]);
|
|
1290
|
+
|
|
1291
|
+
const hasValue = (value) => {
|
|
1292
|
+
if (value === undefined || value === null) {
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1296
|
+
if (typeof value === "object" && Object.keys(value).length === 0) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
return !(Array.isArray(value) && value.length === 0);
|
|
1300
|
+
};
|
|
1301
|
+
const isNotRightType = (filterDefinition, foundFilter) => {
|
|
1302
|
+
return ((filterDefinition.type === "valueNameArray" && !isValueNameArray(foundFilter)) ||
|
|
1303
|
+
(filterDefinition.type === "valueName" && !isValueName(foundFilter)) ||
|
|
1304
|
+
(filterDefinition.type === "stringArray" && !isStringArrayFilterValue(foundFilter)) ||
|
|
1305
|
+
(filterDefinition.type === "dateRange" && !isDateRangeValue(foundFilter)) ||
|
|
1306
|
+
(filterDefinition.type === "area" && !isAreaFilterValue(foundFilter)) ||
|
|
1307
|
+
(filterDefinition.type === "minMax" && !isMinMaxFilterValue(foundFilter)) ||
|
|
1308
|
+
(filterDefinition.type === "boolean" && !isBooleanValue(foundFilter)) ||
|
|
1309
|
+
(filterDefinition.type === "string" && typeof foundFilter !== "string") ||
|
|
1310
|
+
(filterDefinition.type === "number" && typeof foundFilter !== "number"));
|
|
1311
|
+
};
|
|
1312
|
+
/**
|
|
1313
|
+
*
|
|
1314
|
+
*/
|
|
1315
|
+
const isMinMaxFilterValue = (value) => {
|
|
1316
|
+
return value
|
|
1317
|
+
? // eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1318
|
+
typeof value === "object" && (Object.keys(value).includes("min") || Object.keys(value).includes("max"))
|
|
1319
|
+
: false;
|
|
1320
|
+
};
|
|
1321
|
+
/**
|
|
1322
|
+
*
|
|
1323
|
+
*/
|
|
1324
|
+
const isDateRangeValue = (value) => {
|
|
1325
|
+
return value
|
|
1326
|
+
? // eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1327
|
+
typeof value === "object" && (Object.keys(value).includes("from") || Object.keys(value).includes("to"))
|
|
1328
|
+
: false;
|
|
1329
|
+
};
|
|
1330
|
+
/**
|
|
1331
|
+
* {
|
|
1332
|
+
type: "Polygon";
|
|
1333
|
+
coordinates: [number, number][][];
|
|
1334
|
+
}
|
|
1335
|
+
*/
|
|
1336
|
+
const isAreaFilterValue = (value) => {
|
|
1337
|
+
return areaFilterGeoJsonGeometrySchema.safeParse(value).success;
|
|
1338
|
+
};
|
|
1339
|
+
/**
|
|
1340
|
+
*
|
|
1341
|
+
*/
|
|
1342
|
+
const isArrayFilterValue = (value) => {
|
|
1343
|
+
return Array.isArray(value);
|
|
1344
|
+
};
|
|
1345
|
+
/**
|
|
1346
|
+
*
|
|
1347
|
+
*/
|
|
1348
|
+
const isStringArrayFilterValue = (value) => {
|
|
1349
|
+
return isArrayFilterValue(value) && value.every(item => typeof item === "string");
|
|
1350
|
+
};
|
|
1351
|
+
/**
|
|
1352
|
+
*
|
|
1353
|
+
*/
|
|
1354
|
+
const isBooleanValue = (value) => {
|
|
1355
|
+
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1356
|
+
return value ? typeof value === "object" && Object.keys(value).includes("booleanValue") : false;
|
|
1357
|
+
};
|
|
1358
|
+
/**
|
|
1359
|
+
* Type guard to check if a value is a single ValueName object
|
|
1360
|
+
*/
|
|
1361
|
+
const isValueName = (value) => {
|
|
1362
|
+
return (typeof value === "object" &&
|
|
1363
|
+
value !== null &&
|
|
1364
|
+
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1365
|
+
Object.keys(value).includes("name") &&
|
|
1366
|
+
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1367
|
+
Object.keys(value).includes("value"));
|
|
1368
|
+
};
|
|
1369
|
+
/**
|
|
1370
|
+
* Type guard to check if a value is an array of ValueName objects
|
|
1371
|
+
*/
|
|
1372
|
+
const isValueNameArray = (value) => {
|
|
1373
|
+
return isArrayFilterValue(value) && value.every(isValueName);
|
|
1374
|
+
};
|
|
1375
|
+
/**
|
|
1376
|
+
* Validates a filter configuration against filter definitions.
|
|
1377
|
+
*
|
|
1378
|
+
* @template TFilterBarDefinition - The type of the filter bar definition.
|
|
1379
|
+
* @param {FilterBarConfig<TFilterBarDefinition>} filter - The filter configuration to validate.
|
|
1380
|
+
* @param {FilterDefinition[]} filterDefinitions - An array of filter definitions to validate against.
|
|
1381
|
+
* @returns {boolean} - Returns `true` if the filter configuration is valid, otherwise `false`.
|
|
1382
|
+
*/
|
|
1383
|
+
const validateFilter = ({ values, starredFilterKeys, filterDefinitions, }) => {
|
|
1384
|
+
const stateKeys = [];
|
|
1385
|
+
let inBadState = false;
|
|
1386
|
+
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1387
|
+
for (const key of Object.keys(values)) {
|
|
1388
|
+
if (filterDefinitions.find(filterDefinition => filterDefinition.filterKey === key)) {
|
|
1389
|
+
stateKeys.push(key);
|
|
1390
|
+
}
|
|
1391
|
+
else {
|
|
1392
|
+
inBadState = true;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
filterDefinitions.forEach(filterDefinition => {
|
|
1396
|
+
const foundFilter = values[filterDefinition.filterKey];
|
|
1397
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1398
|
+
if (foundFilter && hasValue(foundFilter) && isNotRightType(filterDefinition, foundFilter)) {
|
|
1399
|
+
inBadState = true;
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
if (starredFilterKeys.length > 0) {
|
|
1403
|
+
const allKeys = filterDefinitions.map(f => f.filterKey);
|
|
1404
|
+
const filteredStarredFilterKeys = starredFilterKeys.filter(key => allKeys.includes(key));
|
|
1405
|
+
if (filteredStarredFilterKeys.length !== starredFilterKeys.length) {
|
|
1406
|
+
inBadState = true;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
stateKeys.sort((a, b) => a.localeCompare(b));
|
|
1410
|
+
const filterKeysNotEqual = !isEqual(stateKeys, filterDefinitions.map(f => f.filterKey).sort((a, b) => a.localeCompare(b)));
|
|
1411
|
+
return !(inBadState || filterKeysNotEqual);
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
const getPersistenceKey = (name, clientSideUserId) => `filter-${name}-${clientSideUserId}`;
|
|
1415
|
+
/**
|
|
1416
|
+
* Custom hook for managing the persistence of filter bar configurations.
|
|
1417
|
+
*
|
|
1418
|
+
* @template TFilterBarDefinition - The type of the filter bar definition.
|
|
1419
|
+
* @param {FilterBarPersistenceProps<TFilterBarDefinition>} props - The props for the filter bar persistence.
|
|
1420
|
+
* @returns { object } An object containing the loadData and saveData functions.
|
|
1421
|
+
*/
|
|
1422
|
+
const useFilterBarPersistence = ({ name, setValue, }) => {
|
|
1423
|
+
const { clientSideUserId } = reactCoreHooks.useCurrentUser();
|
|
1424
|
+
const [initialStoredFilters] = react.useState(() => localStorage.getItem(getPersistenceKey(name, clientSideUserId)) || "{}");
|
|
1425
|
+
const saveData = react.useCallback((filterBarConfig) => {
|
|
1426
|
+
const toPersist = {
|
|
1427
|
+
values: filterBarConfig.values ?? {},
|
|
1428
|
+
starredFilterKeys: filterBarConfig.starredFilterKeys ?? [],
|
|
1429
|
+
};
|
|
1430
|
+
localStorage.setItem(getPersistenceKey(name, clientSideUserId), JSON.stringify(toPersist));
|
|
1431
|
+
}, [name, clientSideUserId]);
|
|
1432
|
+
const loadData = react.useCallback((updatedFilterDefinitionsValues) => {
|
|
1433
|
+
let initialFilterBarConfig;
|
|
1434
|
+
const storedFilters = initialStoredFilters;
|
|
1435
|
+
const hasNonVisibleDefaultValues = updatedFilterDefinitionsValues.some(value => value.showInStarredMenu &&
|
|
1436
|
+
!value.showInStarredMenu() &&
|
|
1437
|
+
value.showInFilterBar &&
|
|
1438
|
+
!value.showInFilterBar() &&
|
|
1439
|
+
value.defaultValue?.toString &&
|
|
1440
|
+
value.defaultValue.toString().length > 0);
|
|
1441
|
+
const initialStateValues = createInitialState({
|
|
1442
|
+
name,
|
|
1443
|
+
mainFilters: updatedFilterDefinitionsValues,
|
|
1444
|
+
setValue,
|
|
1445
|
+
});
|
|
1446
|
+
if (storedFilters && storedFilters !== "undefined") {
|
|
1447
|
+
const loadedFilterBarConfigValues = JSON.parse(storedFilters);
|
|
1448
|
+
if (!loadedFilterBarConfigValues.values) {
|
|
1449
|
+
loadedFilterBarConfigValues.values = {};
|
|
1450
|
+
}
|
|
1451
|
+
if (!loadedFilterBarConfigValues.starredFilterKeys) {
|
|
1452
|
+
loadedFilterBarConfigValues.starredFilterKeys = [];
|
|
1453
|
+
}
|
|
1454
|
+
if (validateFilter({
|
|
1455
|
+
values: loadedFilterBarConfigValues.values,
|
|
1456
|
+
starredFilterKeys: loadedFilterBarConfigValues.starredFilterKeys,
|
|
1457
|
+
filterDefinitions: updatedFilterDefinitionsValues,
|
|
1458
|
+
})) {
|
|
1459
|
+
initialFilterBarConfig = {
|
|
1460
|
+
values: loadedFilterBarConfigValues.values,
|
|
1461
|
+
name: name,
|
|
1462
|
+
starredFilterKeys: loadedFilterBarConfigValues.starredFilterKeys,
|
|
1463
|
+
setters: initialStateValues.setters,
|
|
1464
|
+
initialState: initialStateValues.initialState,
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (initialFilterBarConfig === undefined || hasNonVisibleDefaultValues) {
|
|
1469
|
+
initialFilterBarConfig = initialStateValues;
|
|
1470
|
+
}
|
|
1471
|
+
// eslint-disable-next-line no-autofix/local-rules/prefer-custom-object-keys
|
|
1472
|
+
Object.keys(initialFilterBarConfig.values).forEach(key => {
|
|
1473
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1474
|
+
initialFilterBarConfig.setters[`set${stringTs.capitalize(key)}`] = (callback) => setValue(key, callback);
|
|
1475
|
+
});
|
|
1476
|
+
return initialFilterBarConfig;
|
|
1477
|
+
}, [name, setValue, initialStoredFilters]);
|
|
1478
|
+
return react.useMemo(() => ({ loadData, saveData }), [loadData, saveData]);
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Generic hook for setting the value of a filter bar.
|
|
1483
|
+
*
|
|
1484
|
+
* @template TFilterBarDefinition - The type of the filter bar definition.
|
|
1485
|
+
* @returns {object} An object containing the setValue function.
|
|
1486
|
+
*/
|
|
1487
|
+
const useGenericSetValue = () => {
|
|
1488
|
+
const setValue = react.useCallback((setFilterBarConfig, key, callback) => {
|
|
1489
|
+
setFilterBarConfig(prevState => {
|
|
1490
|
+
return {
|
|
1491
|
+
...prevState,
|
|
1492
|
+
values: {
|
|
1493
|
+
...prevState.values,
|
|
1494
|
+
[key]: callback(prevState.values[key]),
|
|
1495
|
+
},
|
|
1496
|
+
};
|
|
1497
|
+
});
|
|
1498
|
+
}, []);
|
|
1499
|
+
return react.useMemo(() => ({ setValue }), [setValue]);
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Custom hook for managing a filter bar's state and actions.
|
|
1504
|
+
*
|
|
1505
|
+
* @template TFilterBarDefinition - A generic type for the filter bar definition.
|
|
1506
|
+
* @returns {object} An object containing filter bar configuration and actions.
|
|
1507
|
+
*/
|
|
1508
|
+
const useFilterBar = ({ name, onValuesChange, filterBarDefinition, }) => {
|
|
1509
|
+
const { setValue } = useGenericSetValue();
|
|
1510
|
+
const { loadData, saveData } = useFilterBarPersistence({
|
|
1511
|
+
name,
|
|
1512
|
+
setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
|
|
1513
|
+
});
|
|
1514
|
+
const [filterBarConfig, setFilterBarConfig] = react.useState(() => {
|
|
1515
|
+
return loadData(sharedUtils.objectValues(filterBarDefinition));
|
|
1516
|
+
});
|
|
1517
|
+
const { filterMapActions, filterMapGetter } = useFilterBarActions({
|
|
1518
|
+
name,
|
|
1519
|
+
filterBarConfig,
|
|
1520
|
+
filterBarDefinition,
|
|
1521
|
+
initialState: filterBarConfig.initialState,
|
|
1522
|
+
setFilterBarConfig,
|
|
1523
|
+
setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
|
|
1524
|
+
});
|
|
1525
|
+
react.useEffect(() => {
|
|
1526
|
+
onValuesChange?.(filterBarConfig.values);
|
|
1527
|
+
saveData(filterBarConfig);
|
|
1528
|
+
}, [filterBarConfig.values, filterBarConfig, onValuesChange, saveData]);
|
|
1529
|
+
return react.useMemo(() => {
|
|
1530
|
+
return {
|
|
1531
|
+
filterBarConfig: { ...filterBarConfig, ...filterMapActions, ...filterMapGetter },
|
|
1532
|
+
filterBarDefinition,
|
|
1533
|
+
name,
|
|
1534
|
+
onValuesChange,
|
|
1535
|
+
};
|
|
1536
|
+
}, [filterBarConfig, filterMapActions, filterMapGetter, filterBarDefinition, name, onValuesChange]);
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Custom hook for managing a filter bar's state and actions.
|
|
1541
|
+
*
|
|
1542
|
+
* @template TFilterBarDefinition - A generic type for the filter bar definition.
|
|
1543
|
+
* @returns {object} An object containing filter bar configuration and actions.
|
|
1544
|
+
*/
|
|
1545
|
+
const useFilterBarAsync = ({ name, onValuesChange, filterBarDefinition, }) => {
|
|
1546
|
+
const [isDataLoaded, setIsDataLoaded] = react.useState(false);
|
|
1547
|
+
const [asyncLoadedFilterBarDefinitions, setAsyncLoadedFilterBarDefinitions] = react.useState();
|
|
1548
|
+
const internalFilterBarDefinitions = react.useMemo(() => asyncLoadedFilterBarDefinitions ?? filterBarDefinition, [filterBarDefinition, asyncLoadedFilterBarDefinitions]);
|
|
1549
|
+
const { setValue } = useGenericSetValue();
|
|
1550
|
+
const { loadData, saveData } = useFilterBarPersistence({
|
|
1551
|
+
name,
|
|
1552
|
+
setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
|
|
1553
|
+
});
|
|
1554
|
+
const [filterBarConfig, setFilterBarConfig] = react.useState(() => {
|
|
1555
|
+
return loadData(sharedUtils.objectValues(internalFilterBarDefinitions));
|
|
1556
|
+
});
|
|
1557
|
+
const { filterMapActions, filterMapGetter } = useFilterBarActions({
|
|
1558
|
+
name,
|
|
1559
|
+
filterBarConfig,
|
|
1560
|
+
filterBarDefinition,
|
|
1561
|
+
initialState: filterBarConfig.initialState,
|
|
1562
|
+
setFilterBarConfig,
|
|
1563
|
+
setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
|
|
1564
|
+
});
|
|
1565
|
+
const dataLoaded = react.useCallback((loadedFilterDefinitionsValues) => {
|
|
1566
|
+
setIsDataLoaded(true);
|
|
1567
|
+
setAsyncLoadedFilterBarDefinitions(loadedFilterDefinitionsValues);
|
|
1568
|
+
setFilterBarConfig(_ => loadData(sharedUtils.objectValues(loadedFilterDefinitionsValues)));
|
|
1569
|
+
}, [loadData]);
|
|
1570
|
+
react.useEffect(() => {
|
|
1571
|
+
if (isDataLoaded) {
|
|
1572
|
+
onValuesChange?.(filterBarConfig.values);
|
|
1573
|
+
saveData(filterBarConfig);
|
|
1574
|
+
}
|
|
1575
|
+
}, [filterBarConfig.values, filterBarConfig, onValuesChange, saveData, isDataLoaded]);
|
|
1486
1576
|
return react.useMemo(() => {
|
|
1487
1577
|
return {
|
|
1488
1578
|
filterBarConfig: { ...filterBarConfig, ...filterMapActions, ...filterMapGetter },
|
|
1489
1579
|
filterBarDefinition: internalFilterBarDefinitions,
|
|
1490
1580
|
dataLoaded,
|
|
1581
|
+
name,
|
|
1491
1582
|
};
|
|
1492
|
-
}, [filterBarConfig, filterMapActions, filterMapGetter, internalFilterBarDefinitions, dataLoaded]);
|
|
1583
|
+
}, [filterBarConfig, filterMapActions, filterMapGetter, internalFilterBarDefinitions, dataLoaded, name]);
|
|
1493
1584
|
};
|
|
1494
1585
|
|
|
1495
1586
|
/**
|
|
@@ -1506,47 +1597,43 @@ const useFilterBar = ({ name, onValuesChange, filterBarDefinition, loadAsync, in
|
|
|
1506
1597
|
*/
|
|
1507
1598
|
const useSearchParamAsFilter = ({ searchParamName, filterName, search, zodSchema, errorHandler, }) => {
|
|
1508
1599
|
return react.useMemo(() => {
|
|
1509
|
-
if (sharedUtils.objectKeys(search).includes(searchParamName)) {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1600
|
+
if (!sharedUtils.objectKeys(search).includes(searchParamName)) {
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
const foundParam = search[searchParamName];
|
|
1604
|
+
try {
|
|
1605
|
+
const getJsonParsedVal = () => {
|
|
1513
1606
|
if (zodSchema._def.typeName === "ZodString") {
|
|
1514
|
-
|
|
1607
|
+
return foundParam ? foundParam + "" : foundParam;
|
|
1515
1608
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
jsonParsed = JSON.parse(foundParam);
|
|
1519
|
-
}
|
|
1520
|
-
else {
|
|
1521
|
-
jsonParsed = foundParam;
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
const zodParsed = zodSchema.safeParse(jsonParsed);
|
|
1525
|
-
if (zodParsed.success) {
|
|
1526
|
-
return zodParsed.data;
|
|
1609
|
+
if (typeof search === "string" && typeof foundParam === "string") {
|
|
1610
|
+
return JSON.parse(foundParam);
|
|
1527
1611
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
catch (e) {
|
|
1538
|
-
captureUrlParseException(errorHandler, {
|
|
1539
|
-
filterName,
|
|
1540
|
-
param: typeof foundParam === "string" ? foundParam : JSON.stringify(foundParam),
|
|
1541
|
-
jsonParsed: "parse json error",
|
|
1542
|
-
zodParseError: null,
|
|
1543
|
-
});
|
|
1612
|
+
return foundParam;
|
|
1613
|
+
};
|
|
1614
|
+
const jsonParsed = getJsonParsedVal();
|
|
1615
|
+
const zodParsed = zodSchema.safeParse(jsonParsed);
|
|
1616
|
+
if (zodParsed.success) {
|
|
1617
|
+
return zodParsed.data;
|
|
1544
1618
|
}
|
|
1619
|
+
captureUrlParseException(errorHandler, {
|
|
1620
|
+
filterName,
|
|
1621
|
+
param: typeof foundParam === "string" ? foundParam : JSON.stringify(foundParam),
|
|
1622
|
+
jsonParsed: jsonParsed,
|
|
1623
|
+
zodParseError: zodParsed.error,
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
catch (e) {
|
|
1627
|
+
captureUrlParseException(errorHandler, {
|
|
1628
|
+
filterName,
|
|
1629
|
+
param: typeof foundParam === "string" ? foundParam : JSON.stringify(foundParam),
|
|
1630
|
+
jsonParsed: "parse json error",
|
|
1631
|
+
zodParseError: null,
|
|
1632
|
+
});
|
|
1545
1633
|
}
|
|
1546
|
-
return null;
|
|
1547
1634
|
}, [search, searchParamName, zodSchema, errorHandler, filterName]);
|
|
1548
1635
|
};
|
|
1549
|
-
const captureUrlParseException = (errorHandler, { filterName, param, jsonParsed: parsed
|
|
1636
|
+
const captureUrlParseException = (errorHandler, { filterName, param, jsonParsed: parsed }) => errorHandler.captureException(new Error(JSON.stringify({
|
|
1550
1637
|
info: `Received invalid values for ${filterName} from URL query params. Can't set ${filterName} filter with this. Please fix the URL query params (or schema).`,
|
|
1551
1638
|
param,
|
|
1552
1639
|
parsed,
|
|
@@ -1596,5 +1683,6 @@ exports.mergeFilters = mergeFilters;
|
|
|
1596
1683
|
exports.mockFilterBar = mockFilterBar;
|
|
1597
1684
|
exports.toggleFilterValue = toggleFilterValue;
|
|
1598
1685
|
exports.useFilterBar = useFilterBar;
|
|
1686
|
+
exports.useFilterBarAsync = useFilterBarAsync;
|
|
1599
1687
|
exports.useSearchParamAsFilter = useSearchParamAsFilter;
|
|
1600
1688
|
exports.validateFilter = validateFilter;
|