@trackunit/filters-filter-bar 1.7.88 → 1.7.93

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 CHANGED
@@ -15,9 +15,9 @@ var tailwindMerge = require('tailwind-merge');
15
15
  var irisAppApi = require('@trackunit/iris-app-api');
16
16
  var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
17
17
  var dequal = require('dequal');
18
- var isEqual = require('lodash/isEqual');
19
18
  var geoJsonUtils = require('@trackunit/geo-json-utils');
20
19
  var zod = require('zod');
20
+ var reactRouter = require('@tanstack/react-router');
21
21
 
22
22
  var defaultTranslations = {
23
23
  "access.management.filter.operator.role.keyAdmin": "Key Admin",
@@ -1061,7 +1061,7 @@ const FiltersMenu = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [],
1061
1061
  setSearchText("");
1062
1062
  }
1063
1063
  }, placement: "bottom-start", children: modalState => {
1064
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx("div", { "data-testid": "starred-filters-menu-trigger", id: "starred-filters-menu-trigger", children: jsxRuntime.jsx(reactComponents.Tooltip, { disabled: !compact || modalState.isOpen, label: jsxRuntime.jsx(MultipleFilterTooltipLabel, { filterBarConfig: filterBarConfig, filters: appliedFilters }), children: jsxRuntime.jsx(reactComponents.Button, { prefix: jsxRuntime.jsx(reactComponents.Icon, { ariaHidden: true, color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Filter", size: "small" }), size: "small", suffix: compact && showAppliedFiltersCount && filterBarConfig.appliedFilterKeys().length > 0 && isSm ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("span", { "aria-hidden": true, children: ["(", filterBarConfig.appliedFilterKeys().length, ")"] }), jsxRuntime.jsxs("span", { className: "sr-only", children: [filterBarConfig.appliedFilterKeys().length, " filters applied"] })] })) : undefined, variant: "secondary", ...buttonProps, children: title !== "" ? (jsxRuntime.jsx("span", { className: "hidden sm:block", children: title ?? t("filtersBar.filtersHeading") })) : null }) }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { cellPadding: 100, children: jsxRuntime.jsx(FiltersMenuContent, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, filterBarDefinitionCount: filterBarDefinitionCount, filtersToShowGrouped: filtersToShowGrouped, hasCustomFields: hasCustomFields, removeCustomFieldsGroup: removeCustomFieldsGroup, searchResultsGrouped: searchResultsGrouped, searchText: searchText, setSearchText: setSearchText, setShowCustomFilters: setShowCustomFilters, showCustomFilters: showCustomFilters }) })] }));
1064
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx("div", { "data-testid": "starred-filters-menu-trigger", id: "starred-filters-menu-trigger", children: jsxRuntime.jsx(reactComponents.Tooltip, { disabled: !compact || modalState.isOpen, label: jsxRuntime.jsx(MultipleFilterTooltipLabel, { filterBarConfig: filterBarConfig, filters: appliedFilters }), children: !isSm ? (jsxRuntime.jsx(reactComponents.IconButton, { "aria-label": title ?? t("filtersBar.filtersHeading"), icon: jsxRuntime.jsx(reactComponents.Icon, { ariaHidden: true, color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Filter", size: "small" }), size: "small", variant: "secondary", ...buttonProps })) : (jsxRuntime.jsx(reactComponents.Button, { prefix: jsxRuntime.jsx(reactComponents.Icon, { ariaHidden: true, color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Filter", size: "small" }), size: "small", suffix: compact && showAppliedFiltersCount && filterBarConfig.appliedFilterKeys().length > 0 ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("span", { "aria-hidden": true, children: ["(", filterBarConfig.appliedFilterKeys().length, ")"] }), jsxRuntime.jsxs("span", { className: "sr-only", children: [filterBarConfig.appliedFilterKeys().length, " filters applied"] })] })) : undefined, variant: "secondary", ...buttonProps, children: title !== "" ? (title ?? t("filtersBar.filtersHeading")) : null })) }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { cellPadding: 100, children: jsxRuntime.jsx(FiltersMenuContent, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, filterBarDefinitionCount: filterBarDefinitionCount, filtersToShowGrouped: filtersToShowGrouped, hasCustomFields: hasCustomFields, removeCustomFieldsGroup: removeCustomFieldsGroup, searchResultsGrouped: searchResultsGrouped, searchText: searchText, setSearchText: setSearchText, setShowCustomFilters: setShowCustomFilters, showCustomFilters: showCustomFilters }) })] }));
1065
1065
  } }), showDirectlyFilters.length > 0 ? (jsxRuntime.jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: showDirectlyFilters })) : null, !compact ? jsxRuntime.jsx(AppliedFiltersRenderer, { appliedFilters: appliedFilters, filterBarConfig: filterBarConfig }) : null] }));
1066
1066
  };
1067
1067
 
@@ -1437,6 +1437,23 @@ const createInitialState = ({ name, mainFilters, setValue, }) => {
1437
1437
  * Dont move these to FilterTypes.ts since rollup will mess up the type.d.ts file by moving jsdocs in random places.
1438
1438
  */
1439
1439
  const areaFilterGeoJsonGeometrySchema = zod.z.union([geoJsonUtils.geoJsonPolygonSchema, geoJsonUtils.geoJsonMultiPolygonSchema]);
1440
+ const stringSchema = zod.z.string();
1441
+ const stringArraySchema = zod.z.array(zod.z.string());
1442
+ const numberSchema = zod.z.number();
1443
+ const booleanSchema = zod.z.boolean();
1444
+ const valueNameSchema = zod.z.object({
1445
+ value: stringSchema,
1446
+ name: stringSchema,
1447
+ });
1448
+ const minMaxFilterSchema = zod.z.object({
1449
+ min: numberSchema.optional(),
1450
+ max: numberSchema.optional(),
1451
+ });
1452
+ const dateRangeSchema = zod.z.object({
1453
+ from: stringSchema.optional(),
1454
+ to: stringSchema.optional(),
1455
+ });
1456
+ const valueNameArraySchema = zod.z.array(valueNameSchema);
1440
1457
 
1441
1458
  const hasValue = (value) => {
1442
1459
  if (value === undefined || value === null) {
@@ -1542,9 +1559,49 @@ const validateFilter = ({ values, filterDefinitions, }) => {
1542
1559
  inBadState = true;
1543
1560
  }
1544
1561
  });
1545
- stateKeys.sort((a, b) => a.localeCompare(b));
1546
- const filterKeysNotEqual = !isEqual(stateKeys, filterDefinitions.map(f => f.filterKey).sort((a, b) => a.localeCompare(b)));
1547
- return !(inBadState || filterKeysNotEqual);
1562
+ // check that all keys in stateKeys are in filterDefinitions
1563
+ stateKeys.forEach(stateKey => {
1564
+ if (!filterDefinitions.find(filterDefinition => filterDefinition.filterKey === stateKey)) {
1565
+ inBadState = true;
1566
+ }
1567
+ });
1568
+ return !inBadState;
1569
+ };
1570
+
1571
+ /**
1572
+ * Generic hook for setting the value of a filter bar.
1573
+ *
1574
+ * @template TFilterBarDefinition - The type of the filter bar definition.
1575
+ * @returns {object} An object containing the setValue function.
1576
+ */
1577
+ const useIsDefaultValue = () => {
1578
+ const isDefaultValue = react.useCallback((key, filterValue, filterBarDefinition, initialState) => {
1579
+ const filterDefinition = filterBarDefinition[key];
1580
+ if ((initialState && dequal.dequal(filterValue, initialState[key])) ||
1581
+ dequal.dequal(filterValue, filterDefinition?.defaultValue)) {
1582
+ return true;
1583
+ }
1584
+ if (!filterValue) {
1585
+ return true;
1586
+ }
1587
+ if (Array.isArray(filterValue)) {
1588
+ return filterValue.length === 0;
1589
+ }
1590
+ if (isMinMaxFilterValue(filterValue)) {
1591
+ return filterValue.min === undefined && filterValue.max === undefined;
1592
+ }
1593
+ if (isValueName(filterValue)) {
1594
+ return filterValue.value === "";
1595
+ }
1596
+ if (isDateRangeValue(filterValue)) {
1597
+ return filterValue.from === undefined && filterValue.to === undefined;
1598
+ }
1599
+ if (typeof filterValue === "object") {
1600
+ return sharedUtils.objectKeys(filterValue).length === 0;
1601
+ }
1602
+ return false;
1603
+ }, []);
1604
+ return react.useMemo(() => ({ isDefaultValue }), [isDefaultValue]);
1548
1605
  };
1549
1606
 
1550
1607
  /**
@@ -1554,6 +1611,7 @@ const validateFilter = ({ values, filterDefinitions, }) => {
1554
1611
  * @returns {object} An object containing filter bar configuration and actions.
1555
1612
  */
1556
1613
  const useFilterBarActions = ({ name, filterBarConfig, filterBarDefinition, setFilterBarConfig, setValue, initialState, }) => {
1614
+ const { isDefaultValue } = useIsDefaultValue();
1557
1615
  const filterMapGetter = react.useMemo(() => {
1558
1616
  return {
1559
1617
  getFilterBarName: () => {
@@ -1566,28 +1624,7 @@ const useFilterBarActions = ({ name, filterBarConfig, filterBarDefinition, setFi
1566
1624
  const filter = filterBarConfig.values[key];
1567
1625
  return filter?.includes(value) || false;
1568
1626
  },
1569
- isDefaultValue(key, filterValue) {
1570
- const filterDefinition = filterBarDefinition[key];
1571
- if (dequal.dequal(filterValue, initialState?.[key]) || dequal.dequal(filterValue, filterDefinition?.defaultValue)) {
1572
- return true;
1573
- }
1574
- if (!filterValue) {
1575
- return true;
1576
- }
1577
- if (Array.isArray(filterValue)) {
1578
- return filterValue.length === 0;
1579
- }
1580
- if (isMinMaxFilterValue(filterValue)) {
1581
- return filterValue.min === undefined && filterValue.max === undefined;
1582
- }
1583
- if (isValueName(filterValue)) {
1584
- return filterValue.value === "";
1585
- }
1586
- if (typeof filterValue === "object") {
1587
- return sharedUtils.objectKeys(filterValue).length === 0;
1588
- }
1589
- return false;
1590
- },
1627
+ isDefaultValue: (key, value) => isDefaultValue(key, value, filterBarDefinition, initialState),
1591
1628
  getValuesByKey(key) {
1592
1629
  return filterBarConfig.values[key];
1593
1630
  },
@@ -1614,7 +1651,7 @@ const useFilterBarActions = ({ name, filterBarConfig, filterBarDefinition, setFi
1614
1651
  return filter?.value === value || false;
1615
1652
  },
1616
1653
  };
1617
- }, [filterBarDefinition, filterBarConfig.name, filterBarConfig.values, initialState]);
1654
+ }, [filterBarDefinition, filterBarConfig.name, filterBarConfig.values, initialState, isDefaultValue]);
1618
1655
  const filterMapActions = react.useMemo(() => {
1619
1656
  // Reset an individual filter to its initial state
1620
1657
  const resetIndividualFilterToInitialState = (key) => {
@@ -1831,6 +1868,127 @@ const useFilterBarActions = ({ name, filterBarConfig, filterBarDefinition, setFi
1831
1868
  return react.useMemo(() => ({ filterMapGetter, filterMapActions }), [filterMapGetter, filterMapActions]);
1832
1869
  };
1833
1870
 
1871
+ /**
1872
+ * Hook to sync filter values with URL
1873
+ *
1874
+ * @returns {object} Object containing loadFromUrl and saveToUrl functions
1875
+ */
1876
+ const useFilterUrlSync = () => {
1877
+ const getZodSchema = react.useCallback((filterType) => {
1878
+ if (filterType === "string") {
1879
+ return stringSchema;
1880
+ }
1881
+ else if (filterType === "stringArray") {
1882
+ return stringArraySchema;
1883
+ }
1884
+ else if (filterType === "dateRange") {
1885
+ return dateRangeSchema;
1886
+ }
1887
+ else if (filterType === "area") {
1888
+ return areaFilterGeoJsonGeometrySchema;
1889
+ }
1890
+ else if (filterType === "valueNameArray") {
1891
+ return valueNameArraySchema;
1892
+ }
1893
+ else if (filterType === "valueName") {
1894
+ return valueNameSchema;
1895
+ }
1896
+ else if (filterType === "minMax") {
1897
+ return minMaxFilterSchema;
1898
+ }
1899
+ else if (filterType === "boolean") {
1900
+ return booleanSchema;
1901
+ }
1902
+ return numberSchema;
1903
+ }, []);
1904
+ const getJsonParsedVal = react.useCallback((filterValue, zodSchema) => {
1905
+ if (!zodSchema) {
1906
+ return filterValue;
1907
+ }
1908
+ if (zodSchema._def.typeName === "ZodString") {
1909
+ return filterValue ? filterValue + "" : filterValue;
1910
+ }
1911
+ if (typeof filterValue === "string") {
1912
+ try {
1913
+ return JSON.parse(filterValue);
1914
+ }
1915
+ catch (e) {
1916
+ return filterValue;
1917
+ }
1918
+ }
1919
+ return filterValue;
1920
+ }, []);
1921
+ const getFilterValuesFromUrl = react.useCallback((definitions, search) => {
1922
+ const loadedValues = {};
1923
+ definitions.forEach(filter => {
1924
+ const filterValue = search[filter.filterKey];
1925
+ if (filterValue) {
1926
+ const zodSchema = filter.zodSchema || getZodSchema(filter.type);
1927
+ const jsonParsed = getJsonParsedVal(filterValue, zodSchema);
1928
+ const zodParsed = zodSchema.safeParse(jsonParsed);
1929
+ if (zodParsed.success) {
1930
+ loadedValues[filter.filterKey] = zodParsed.data;
1931
+ }
1932
+ }
1933
+ });
1934
+ return loadedValues;
1935
+ }, [getJsonParsedVal, getZodSchema]);
1936
+ const getFilterValuesToUrl = react.useCallback((values, definitions, setEmptyAndDefaultValues, isDefaultValue) => {
1937
+ const urlObject = {};
1938
+ definitions.forEach(filter => {
1939
+ const value = values?.[filter.filterKey];
1940
+ if (value) {
1941
+ if (setEmptyAndDefaultValues) {
1942
+ urlObject[filter.filterKey] = value;
1943
+ }
1944
+ else if (!isDefaultValue(filter.filterKey, value)) {
1945
+ if (Array.isArray(value) && value.length === 0) {
1946
+ delete urlObject[filter.filterKey];
1947
+ }
1948
+ else {
1949
+ urlObject[filter.filterKey] = value;
1950
+ }
1951
+ }
1952
+ else {
1953
+ delete urlObject[filter.filterKey];
1954
+ }
1955
+ }
1956
+ });
1957
+ return urlObject;
1958
+ }, []);
1959
+ return react.useMemo(() => ({
1960
+ getFilterValuesFromUrl,
1961
+ getFilterValuesToUrl,
1962
+ }), [getFilterValuesFromUrl, getFilterValuesToUrl]);
1963
+ };
1964
+
1965
+ /**
1966
+ * This hook provides functionality to:
1967
+ * - Get the length of the search parameters excluding a specific key
1968
+ */
1969
+ const useSearchUtils = () => {
1970
+ const location = reactRouter.useLocation();
1971
+ const getSearchParamsLengthExcluding = react.useCallback((excludeKey, searchParams) => {
1972
+ return sharedUtils.objectKeys(searchParams)
1973
+ .filter(key => key !== excludeKey)
1974
+ .reduce((totalLength, key) => {
1975
+ const keyLength = encodeURIComponent(String(key)).length;
1976
+ const value = searchParams[key] !== null ? encodeURIComponent(searchParams[key]?.toString() ?? "") : "";
1977
+ const valueLength = value.length;
1978
+ // Add 1 for '=' and 1 for '&' (except for the first param)
1979
+ return totalLength + keyLength + valueLength + (totalLength > 0 ? 2 : 1);
1980
+ }, 0);
1981
+ }, []);
1982
+ const getUrlLengthWithSearchParam = react.useCallback((key, value, prev) => {
1983
+ const searchParamsLength = getSearchParamsLengthExcluding(key, prev);
1984
+ const urlLength = location.href.length - (location.searchStr.length || 0) + (location.hash.length || 0) + searchParamsLength;
1985
+ // 1 === '&' and 1 === '='
1986
+ return urlLength + 1 + key.length + 1 + value.length;
1987
+ }, [getSearchParamsLengthExcluding, location]);
1988
+ return react.useMemo(() => ({ getSearchParamsLengthExcluding, getUrlLengthWithSearchParam }), [getSearchParamsLengthExcluding, getUrlLengthWithSearchParam]);
1989
+ };
1990
+
1991
+ const MAX_URL_LENGTH = 5000;
1834
1992
  /**
1835
1993
  * Get the persistence key for the filter bar.
1836
1994
  *
@@ -1842,20 +2000,131 @@ const getPersistenceKey = (name, clientSideUserId) => `filter-${name}-${clientSi
1842
2000
  /**
1843
2001
  * Custom hook for managing the persistence of filter bar configurations.
1844
2002
  *
2003
+ * This hook provides a complete state management solution for filter bars with automatic
2004
+ * URL synchronization and localStorage persistence. It maintains the following state:
2005
+ *
2006
+ * ## State Management Flow:
2007
+ *
2008
+ * 1. **Initial Load**:
2009
+ * - Loads from localStorage (if no custom loadData or saveData function is provided)
2010
+ * - Updates with state from URL search parameters
2011
+ * - Ensures URL is updated with the final state after load
2012
+ *
2013
+ * 2. **Save Operations**:
2014
+ * - Updates localStorage with new filter values
2015
+ * - Updates URL search parameters to reflect current state
2016
+ * - Maintains synchronization between localStorage and URL
2017
+ *
2018
+ * 3. **URL Change Detection**:
2019
+ * - Listens for changes in URL search parameters
2020
+ * - Compares URL state with last known state to detect external changes
2021
+ * - Triggers refreshData callback when external URL changes are detected
2022
+ * - Only updates URL when no custom loadData or saveData function provided
2023
+ *
2024
+ * ## Behavior Modes:
2025
+ *
2026
+ * **Default Mode** (no custom functions):
2027
+ * - Uses localStorage for persistence
2028
+ * - Automatically syncs with URL search parameters
2029
+ * - Handles URL updates and change detection
2030
+ *
2031
+ * **Custom Mode** (with loadData/saveData functions):
2032
+ * - Does NOT sync with URL or use localStorage
2033
+ * - Relies entirely on provided functions for persistence
2034
+ * - Does not provide URL change detection via refreshData callback
2035
+ *
2036
+ * The hook prevents infinite loops by:
2037
+ * - Tracking what state was last sent to the URL
2038
+ * - Only updating URL when state actually changes
2039
+ * - Distinguishing between internal state changes and external URL changes
2040
+ *
1845
2041
  * @template TFilterBarDefinition - The type of the filter bar definition.
1846
2042
  * @param {FilterBarPersistenceProps<TFilterBarDefinition>} props - The props for the filter bar persistence.
1847
2043
  * @returns { object } An object containing the loadData and saveData functions.
1848
2044
  */
1849
- const useFilterBarPersistence = ({ name, setValue, loadData: inputLoadData, saveData: inputSaveData, }) => {
2045
+ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue, loadData: inputLoadData, saveData: inputSaveData, }) => {
1850
2046
  const { clientSideUserId } = reactCoreHooks.useCurrentUser();
2047
+ const search = reactRouter.useSearch({ strict: false });
2048
+ const location = reactRouter.useLocation();
2049
+ const navigate = reactRouter.useNavigate();
2050
+ const { encode, decode } = reactCoreHooks.useCustomEncoding();
2051
+ const { getUrlLengthWithSearchParam } = useSearchUtils();
2052
+ const updateSearch = react.useCallback(async (searchParams) => {
2053
+ if (!inputLoadData && !inputSaveData) {
2054
+ // should check if the state has actually changed from what we last sent to the URL
2055
+ if (!searchParams[name] ||
2056
+ (typeof searchParams[name] === "string" &&
2057
+ !dequal.dequal(decode(searchParams[name]), typeof search[name] === "string" ? decode(search[name]) : search[name]))) {
2058
+ return requestAnimationFrame(async () => {
2059
+ const replace = !search[name];
2060
+ await navigate({
2061
+ to: ".",
2062
+ search: (prev) => {
2063
+ if (getUrlLengthWithSearchParam(name, searchParams[name] || "", prev) <= MAX_URL_LENGTH) {
2064
+ return { ...prev, ...searchParams };
2065
+ }
2066
+ else {
2067
+ // eslint-disable-next-line no-console
2068
+ console.log(`URL too long, skipping sync of filters to the browsers URL, to avoid crashing the browser, limit is ${MAX_URL_LENGTH}, was: ${getUrlLengthWithSearchParam(name, searchParams[name] || "", prev)}`);
2069
+ const newSearchParams = { ...prev, [name]: undefined };
2070
+ return newSearchParams;
2071
+ }
2072
+ },
2073
+ hash: location.hash,
2074
+ replace,
2075
+ });
2076
+ });
2077
+ }
2078
+ }
2079
+ return Promise.resolve();
2080
+ }, [navigate, name, search, inputLoadData, inputSaveData, decode, getUrlLengthWithSearchParam, location.hash]);
1851
2081
  const getFromLocalStorage = react.useCallback((persistenceKey) => {
1852
2082
  return localStorage.getItem(getPersistenceKey(persistenceKey, clientSideUserId));
1853
2083
  }, [clientSideUserId]);
1854
2084
  const lastName = react.useRef(name);
1855
2085
  const [initialStoredFilters] = react.useState(() => (!inputLoadData && getFromLocalStorage(name)) || "{}");
1856
- const [lastSavedState, setLastSavedState] = react.useState(undefined);
2086
+ const { getFilterValuesFromUrl, getFilterValuesToUrl } = useFilterUrlSync();
2087
+ const lastSavedStateRef = react.useRef(undefined);
2088
+ const lastSearchUpdateRef = react.useRef(undefined);
2089
+ const refreshDataRef = react.useRef(refreshData);
2090
+ react.useEffect(() => {
2091
+ refreshDataRef.current = refreshData;
2092
+ }, [refreshData]);
2093
+ // Add this useEffect to detect external URL changes
2094
+ react.useEffect(() => {
2095
+ if (!inputLoadData && !inputSaveData) {
2096
+ requestAnimationFrame(() => {
2097
+ const currentSearchValue = search[name];
2098
+ let currentSearchValueParsed;
2099
+ let lastSearchUpdateRefParsed;
2100
+ try {
2101
+ currentSearchValueParsed = currentSearchValue ? decode(currentSearchValue) : undefined;
2102
+ }
2103
+ catch (error) {
2104
+ // Invalid compressed data, treat as undefined
2105
+ currentSearchValueParsed = undefined;
2106
+ }
2107
+ try {
2108
+ lastSearchUpdateRefParsed = lastSearchUpdateRef.current ? decode(lastSearchUpdateRef.current) : undefined;
2109
+ }
2110
+ catch (error) {
2111
+ // Invalid compressed data, treat as undefined
2112
+ lastSearchUpdateRefParsed = undefined;
2113
+ }
2114
+ // H4sIADGRuGgAA3WTy07eQAxGXwVlXUtje27uruqWd0C2x1NaAa2AdoN49xq6-LOgUpQo0onny7H9ctzHsy591q8_73_dxXM8xNPT8fnl-KN3v-P4fHy5vj4-HQ96__5yd3e8fjqeQh_9Nt9v48fVfayr9f3b1YrHBH3f3GDBaj4Zui-HuvsADeO8KeFGrmYrj8hCbzTZrF0coRMRVEYHGcKAsW1Iw8qiF3ox06zbASth1t4GVsqCUvIxqO9R-EJ7w2aDJnjHCbV6JtmlgOSHQzB4DLvQtiwiRGB3aUk3h0lLgdyjFiyD8USPhd55BYzakkbaSbeA2jfmFaV7v9BMErKLguWpUNdQEKwTsqqXNETL_eRkbqojy1K0pCtVkK4LJM2VPtk6lQtdcIpSs2RGno9aQG0LSJrmvgrKHCeDFWWQN0iNNZMYgczFQNtH0VbqKifaq7uVBFdvBIgRIBoClcLXQFlC7VR7E7l2PdHKnH36kN5rJFY96Z0m3mhTKv-hG8cscytgU0vLnAOlTNAWdd_TR11y8m3dsxsGyJp_2XOg5hyZOxaP2Qu6nX137DbnW5L0_S-3p9CPk2hC09sG15SXgrKrHgQlpYe6ZKl9oScNb74IOmMmmbkSYmmHdYWZ7z3baWKpiWq0nA6u2fkEYFoOS-_OtibuIqfdaa0oDytgnB2saSC3YTSI0UKIc9nofWJf_wIRCrRd6AMAAA
2115
+ // Check if this is an external URL change (not from our own updates)
2116
+ // Only trigger refreshData if there's a meaningful change in the URL value
2117
+ if (search[name] !== undefined && !dequal.dequal(currentSearchValueParsed, lastSearchUpdateRefParsed)) {
2118
+ // This is an external URL change with actual data, trigger refreshData
2119
+ if (refreshDataRef.current) {
2120
+ refreshDataRef.current();
2121
+ }
2122
+ }
2123
+ });
2124
+ }
2125
+ }, [search, name, inputLoadData, inputSaveData, decode]);
1857
2126
  const saveData = react.useCallback((filterBarConfig, filterBarDefinitions) => {
1858
- const newValues = Object.assign({}, lastSavedState || {});
2127
+ const newValues = Object.assign({}, lastSavedStateRef.current || {});
1859
2128
  if (filterBarConfig.values) {
1860
2129
  Object.entries(filterBarConfig.values).forEach(([key, value]) => {
1861
2130
  const typedKey = key;
@@ -1868,41 +2137,83 @@ const useFilterBarPersistence = ({ name, setValue, loadData: inputLoadData, save
1868
2137
  const toPersist = {
1869
2138
  values: newValues,
1870
2139
  };
1871
- if (!dequal.dequal(newValues, lastSavedState)) {
1872
- setLastSavedState(newValues);
2140
+ if (!dequal.dequal(newValues, lastSavedStateRef.current)) {
2141
+ lastSavedStateRef.current = newValues;
1873
2142
  if (inputSaveData) {
1874
2143
  inputSaveData(newValues);
1875
2144
  }
1876
2145
  else {
1877
2146
  localStorage.setItem(getPersistenceKey(name, clientSideUserId), JSON.stringify(toPersist));
2147
+ const urlObject = getFilterValuesToUrl(newValues, sharedUtils.objectValues(filterBarDefinitions), false, isDefaultValue);
2148
+ const result = {};
2149
+ if (filterBarConfig.name) {
2150
+ result[filterBarConfig.name] = encode(urlObject);
2151
+ // Update the ref BEFORE updating the URL to prevent false external change detection
2152
+ lastSearchUpdateRef.current = result[filterBarConfig.name];
2153
+ }
2154
+ updateSearch(result);
1878
2155
  }
1879
2156
  }
1880
- }, [name, clientSideUserId, inputSaveData, lastSavedState]);
1881
- const loadValues = react.useCallback(() => {
1882
- if (inputLoadData) {
1883
- return inputLoadData();
2157
+ }, [name, clientSideUserId, inputSaveData, getFilterValuesToUrl, updateSearch, encode, isDefaultValue]);
2158
+ const loadFromLocalStorage = react.useCallback(() => {
2159
+ let storedFilters = null;
2160
+ if (lastName.current !== name) {
2161
+ storedFilters = localStorage.getItem(getPersistenceKey(name, clientSideUserId)) || "{}";
2162
+ lastName.current = name;
1884
2163
  }
1885
2164
  else {
1886
- let storedFilters = null;
1887
- if (lastName.current !== name) {
1888
- storedFilters = localStorage.getItem(getPersistenceKey(name, clientSideUserId)) || "{}";
1889
- lastName.current = name;
2165
+ storedFilters = initialStoredFilters;
2166
+ }
2167
+ if (storedFilters && storedFilters !== "undefined") {
2168
+ try {
2169
+ const loadedFilterBarConfigValues = JSON.parse(storedFilters);
2170
+ return loadedFilterBarConfigValues?.values || {};
1890
2171
  }
1891
- else {
1892
- storedFilters = initialStoredFilters;
2172
+ catch (error) {
2173
+ return {};
1893
2174
  }
1894
- if (storedFilters && storedFilters !== "undefined") {
1895
- try {
1896
- const loadedFilterBarConfigValues = JSON.parse(storedFilters);
1897
- return loadedFilterBarConfigValues?.values;
1898
- }
1899
- catch (error) {
1900
- return null;
2175
+ }
2176
+ return {};
2177
+ }, [clientSideUserId, name, initialStoredFilters, lastName]);
2178
+ const loadFromSearchURL = react.useCallback(() => {
2179
+ let searchParams = search || {};
2180
+ const searchParamValue = searchParams[name];
2181
+ if (searchParamValue) {
2182
+ try {
2183
+ searchParams = decode(searchParamValue);
2184
+ }
2185
+ catch (error) {
2186
+ searchParams = {};
2187
+ }
2188
+ return searchParams;
2189
+ }
2190
+ return null;
2191
+ }, [search, name, decode]);
2192
+ const loadValues = react.useCallback((filterDefinitions) => {
2193
+ if (inputLoadData) {
2194
+ return inputLoadData();
2195
+ }
2196
+ else {
2197
+ const values = loadFromLocalStorage() || {};
2198
+ const searchParams = loadFromSearchURL();
2199
+ if (searchParams && sharedUtils.objectKeys(searchParams).length > 0) {
2200
+ const valuesFromUrl = getFilterValuesFromUrl(filterDefinitions, searchParams);
2201
+ if (sharedUtils.objectKeys(valuesFromUrl).length > 0) {
2202
+ // if there are values from the URL, update ALL filters values based on the searchParams or their defaults
2203
+ filterDefinitions.forEach(filter => {
2204
+ const key = filter.filterKey;
2205
+ if (key in valuesFromUrl) {
2206
+ values[key] = valuesFromUrl[key];
2207
+ }
2208
+ else {
2209
+ values[key] = filter.defaultValue;
2210
+ }
2211
+ });
1901
2212
  }
1902
2213
  }
1903
- return null;
2214
+ return values;
1904
2215
  }
1905
- }, [inputLoadData, initialStoredFilters, clientSideUserId, name]);
2216
+ }, [inputLoadData, loadFromLocalStorage, getFilterValuesFromUrl, loadFromSearchURL]);
1906
2217
  const loadData = react.useCallback((updatedFilterDefinitionsValues) => {
1907
2218
  const initialStateValues = createInitialState({
1908
2219
  name,
@@ -1910,16 +2221,22 @@ const useFilterBarPersistence = ({ name, setValue, loadData: inputLoadData, save
1910
2221
  setValue,
1911
2222
  });
1912
2223
  const loadedFilterBarConfigValues = {
1913
- values: loadValues(),
2224
+ values: loadValues(updatedFilterDefinitionsValues),
1914
2225
  };
1915
2226
  let initialFilterBarConfig;
1916
2227
  if (loadedFilterBarConfigValues.values) {
1917
- // make sure the last saved state is the same as the loaded values no manipulation of values!
1918
- setLastSavedState(Object.assign({}, loadedFilterBarConfigValues.values));
1919
2228
  if (validateFilter({
1920
2229
  values: loadedFilterBarConfigValues.values,
1921
2230
  filterDefinitions: updatedFilterDefinitionsValues,
1922
2231
  })) {
2232
+ sharedUtils.objectKeys(initialStateValues.values).forEach(key => {
2233
+ if (loadedFilterBarConfigValues.values) {
2234
+ loadedFilterBarConfigValues.values[key] =
2235
+ loadedFilterBarConfigValues.values[key] !== undefined
2236
+ ? loadedFilterBarConfigValues.values[key]
2237
+ : initialStateValues.values[key];
2238
+ }
2239
+ });
1923
2240
  initialFilterBarConfig = {
1924
2241
  values: loadedFilterBarConfigValues.values,
1925
2242
  name: name,
@@ -1941,9 +2258,17 @@ const useFilterBarPersistence = ({ name, setValue, loadData: inputLoadData, save
1941
2258
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1942
2259
  initialFilterBarConfig.setters[`set${stringTs.capitalize(key)}`] = (callback) => setValue(key, callback);
1943
2260
  });
2261
+ const urlObject = getFilterValuesToUrl(initialFilterBarConfig.values, updatedFilterDefinitionsValues, false, isDefaultValue);
2262
+ const result = {};
2263
+ if (initialFilterBarConfig.name) {
2264
+ result[initialFilterBarConfig.name] = encode(JSON.stringify(urlObject));
2265
+ // Update the ref BEFORE updating the URL to prevent false external change detection
2266
+ lastSearchUpdateRef.current = result[initialFilterBarConfig.name];
2267
+ }
2268
+ updateSearch(result);
1944
2269
  return initialFilterBarConfig;
1945
- }, [name, setValue, loadValues]);
1946
- return react.useMemo(() => ({ loadData, saveData }), [loadData, saveData]);
2270
+ }, [name, setValue, loadValues, getFilterValuesToUrl, updateSearch, encode, isDefaultValue]);
2271
+ return react.useMemo(() => ({ loadData, saveData, getFilterValuesToUrl }), [loadData, saveData, getFilterValuesToUrl]);
1947
2272
  };
1948
2273
 
1949
2274
  /**
@@ -1976,21 +2301,18 @@ const useGenericSetValue = () => {
1976
2301
  const useFilterBar = ({ name, onValuesChange, filterBarDefinition, loadData: inputLoadData, saveData: inputSaveData, }) => {
1977
2302
  const lastName = react.useRef(name);
1978
2303
  const { setValue } = useGenericSetValue();
2304
+ const { isDefaultValue } = useIsDefaultValue();
1979
2305
  const { loadData, saveData } = useFilterBarPersistence({
1980
2306
  name,
2307
+ isDefaultValue: (key, value) => isDefaultValue(key, value, filterBarDefinition),
1981
2308
  setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
2309
+ refreshData: () => setFilterBarConfig(loadData(sharedUtils.objectValues(filterBarDefinition))),
1982
2310
  loadData: inputLoadData,
1983
2311
  saveData: inputSaveData,
1984
2312
  });
1985
2313
  const [filterBarConfig, setFilterBarConfig] = react.useState(() => {
1986
2314
  return loadData(sharedUtils.objectValues(filterBarDefinition));
1987
2315
  });
1988
- react.useEffect(() => {
1989
- if (lastName.current !== name) {
1990
- setFilterBarConfig(loadData(sharedUtils.objectValues(filterBarDefinition)));
1991
- lastName.current = name;
1992
- }
1993
- }, [filterBarDefinition, loadData, name]);
1994
2316
  const { filterMapActions, filterMapGetter } = useFilterBarActions({
1995
2317
  name,
1996
2318
  filterBarConfig,
@@ -1999,10 +2321,20 @@ const useFilterBar = ({ name, onValuesChange, filterBarDefinition, loadData: inp
1999
2321
  setFilterBarConfig,
2000
2322
  setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
2001
2323
  });
2324
+ const onValuesChangeRef = react.useRef(onValuesChange);
2325
+ react.useEffect(() => {
2326
+ onValuesChangeRef.current = onValuesChange;
2327
+ }, [onValuesChange]);
2002
2328
  react.useEffect(() => {
2003
2329
  saveData(filterBarConfig, filterBarDefinition);
2004
- onValuesChange?.(filterBarConfig.values);
2005
- }, [filterBarConfig, filterBarDefinition, onValuesChange, saveData]);
2330
+ onValuesChangeRef.current?.(filterBarConfig.values);
2331
+ }, [filterBarConfig, filterBarDefinition, saveData]);
2332
+ react.useEffect(() => {
2333
+ if (lastName.current !== name) {
2334
+ setFilterBarConfig(loadData(sharedUtils.objectValues(filterBarDefinition)));
2335
+ lastName.current = name;
2336
+ }
2337
+ }, [filterBarDefinition, loadData, name]);
2006
2338
  return react.useMemo(() => {
2007
2339
  return {
2008
2340
  filterBarConfig: { ...filterBarConfig, ...filterMapActions, ...filterMapGetter },
@@ -2022,13 +2354,20 @@ const useFilterBar = ({ name, onValuesChange, filterBarDefinition, loadData: inp
2022
2354
  const useFilterBarAsync = ({ name, onValuesChange, filterBarDefinition, loadData: inputLoadData, saveData: inputSaveData, }) => {
2023
2355
  const [isDataLoaded, setIsDataLoaded] = react.useState(false);
2024
2356
  const [asyncLoadedFilterBarDefinitions, setAsyncLoadedFilterBarDefinitions] = react.useState();
2357
+ const { isDefaultValue } = useIsDefaultValue();
2025
2358
  const internalFilterBarDefinitions = react.useMemo(() => asyncLoadedFilterBarDefinitions ?? filterBarDefinition, [filterBarDefinition, asyncLoadedFilterBarDefinitions]);
2026
2359
  const { setValue } = useGenericSetValue();
2027
2360
  const { loadData, saveData } = useFilterBarPersistence({
2028
2361
  name,
2362
+ isDefaultValue: (key, value) => isDefaultValue(key, value, filterBarDefinition, filterBarConfig.initialState),
2029
2363
  setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
2030
2364
  loadData: inputLoadData,
2031
2365
  saveData: inputSaveData,
2366
+ refreshData: () => {
2367
+ if (asyncLoadedFilterBarDefinitions) {
2368
+ setFilterBarConfig(loadData(sharedUtils.objectValues(asyncLoadedFilterBarDefinitions)));
2369
+ }
2370
+ },
2032
2371
  });
2033
2372
  const [filterBarConfig, setFilterBarConfig] = react.useState(() => {
2034
2373
  const initialConfig = {
@@ -2039,13 +2378,16 @@ const useFilterBarAsync = ({ name, onValuesChange, filterBarDefinition, loadData
2039
2378
  };
2040
2379
  return initialConfig;
2041
2380
  });
2381
+ const setValueMemoized = react.useCallback((key, callback) => {
2382
+ setValue(setFilterBarConfig, key, callback);
2383
+ }, [setFilterBarConfig, setValue]);
2042
2384
  const { filterMapActions, filterMapGetter } = useFilterBarActions({
2043
2385
  name,
2044
2386
  filterBarConfig,
2045
2387
  filterBarDefinition: internalFilterBarDefinitions,
2046
2388
  initialState: filterBarConfig.initialState,
2047
2389
  setFilterBarConfig,
2048
- setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
2390
+ setValue: setValueMemoized,
2049
2391
  });
2050
2392
  const dataLoaded = react.useCallback((loadedFilterDefinitionsValues) => {
2051
2393
  setIsDataLoaded(false);
@@ -2084,19 +2426,18 @@ const useFilterBarAsync = ({ name, onValuesChange, filterBarDefinition, loadData
2084
2426
  * The parameter is validated using the provided Zod schema.
2085
2427
  *
2086
2428
  * @template T - A Zod schema used for validation.
2087
- * @param {string} searchParamName - The name of the search parameter in the URL.
2088
2429
  * @param {string} filterName - The name of the filter.
2089
2430
  * @param {object} search - The current search state, provided by `useSearch` from @tanstack/react-router.
2090
2431
  * @param {T} zodSchema - The Zod schema for validating the filter data.
2091
2432
  * @param {ErrorHandlingContextValue} errorHandler - Error handling context for capturing and reporting exceptions.
2092
2433
  * @returns {any} The parsed filter data if validation succeeds, otherwise `null`.
2093
2434
  */
2094
- const useSearchParamAsFilter = ({ searchParamName, filterName, search, zodSchema, errorHandler, }) => {
2435
+ const useSearchParamAsFilter = ({ filterName, search, zodSchema, errorHandler, }) => {
2095
2436
  return react.useMemo(() => {
2096
- if (!sharedUtils.objectKeys(search).includes(searchParamName)) {
2437
+ if (!sharedUtils.objectKeys(search).includes(filterName)) {
2097
2438
  return null;
2098
2439
  }
2099
- const foundParam = search[searchParamName];
2440
+ const foundParam = search[filterName];
2100
2441
  try {
2101
2442
  const getJsonParsedVal = () => {
2102
2443
  if (zodSchema._def.typeName === "ZodString") {
@@ -2127,7 +2468,7 @@ const useSearchParamAsFilter = ({ searchParamName, filterName, search, zodSchema
2127
2468
  zodParseError: null,
2128
2469
  });
2129
2470
  }
2130
- }, [search, searchParamName, zodSchema, errorHandler, filterName]);
2471
+ }, [search, zodSchema, errorHandler, filterName]);
2131
2472
  };
2132
2473
  const captureUrlParseException = (errorHandler, { filterName, param, jsonParsed: parsed }) => errorHandler.captureException(new Error(JSON.stringify({
2133
2474
  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).`,
@@ -2178,6 +2519,8 @@ exports.MultipleFilterTooltipLabel = MultipleFilterTooltipLabel;
2178
2519
  exports.ResetFiltersButton = ResetFiltersButton;
2179
2520
  exports.TooltipLabel = TooltipLabel;
2180
2521
  exports.areaFilterGeoJsonGeometrySchema = areaFilterGeoJsonGeometrySchema;
2522
+ exports.booleanSchema = booleanSchema;
2523
+ exports.dateRangeSchema = dateRangeSchema;
2181
2524
  exports.isAreaFilterValue = isAreaFilterValue;
2182
2525
  exports.isArrayFilterValue = isArrayFilterValue;
2183
2526
  exports.isBooleanValue = isBooleanValue;
@@ -2187,10 +2530,16 @@ exports.isStringArrayFilterValue = isStringArrayFilterValue;
2187
2530
  exports.isValueName = isValueName;
2188
2531
  exports.isValueNameArray = isValueNameArray;
2189
2532
  exports.mergeFilters = mergeFilters;
2533
+ exports.minMaxFilterSchema = minMaxFilterSchema;
2190
2534
  exports.mockFilterBar = mockFilterBar;
2535
+ exports.numberSchema = numberSchema;
2536
+ exports.stringArraySchema = stringArraySchema;
2537
+ exports.stringSchema = stringSchema;
2191
2538
  exports.toggleFilterValue = toggleFilterValue;
2192
2539
  exports.useFilterBar = useFilterBar;
2193
2540
  exports.useFilterBarAsync = useFilterBarAsync;
2194
2541
  exports.useFiltersMenu = useFiltersMenu;
2195
2542
  exports.useSearchParamAsFilter = useSearchParamAsFilter;
2196
2543
  exports.validateFilter = validateFilter;
2544
+ exports.valueNameArraySchema = valueNameArraySchema;
2545
+ exports.valueNameSchema = valueNameSchema;