@trackunit/filters-filter-bar 1.7.87 → 1.7.90

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