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