@trackunit/filters-filter-bar 1.21.4 → 1.21.7

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
@@ -17,7 +17,6 @@ var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities
17
17
  var dequal = require('dequal');
18
18
  var geoJsonUtils = require('@trackunit/geo-json-utils');
19
19
  var zod = require('zod');
20
- var reactRouter = require('@tanstack/react-router');
21
20
 
22
21
  var defaultTranslations = {
23
22
  "access.management.filter.operator.role.keyAdmin": "Key Admin",
@@ -2045,125 +2044,31 @@ const useFilterUrlSync = () => {
2045
2044
  }), [getFilterValuesFromUrl, getFilterValuesToUrl]);
2046
2045
  };
2047
2046
 
2048
- /**
2049
- * This hook provides functionality to:
2050
- * - Get the length of the search parameters excluding a specific key
2051
- */
2052
- const useSearchUtils = () => {
2053
- const location = reactRouter.useLocation();
2054
- const getSearchParamsLengthExcluding = react.useCallback((excludeKey, searchParams) => {
2055
- return sharedUtils.objectKeys(searchParams)
2056
- .filter(key => key !== excludeKey)
2057
- .reduce((totalLength, key) => {
2058
- const keyLength = encodeURIComponent(String(key)).length;
2059
- const value = searchParams[key] !== null ? encodeURIComponent(searchParams[key]?.toString() ?? "") : "";
2060
- const valueLength = value.length;
2061
- // Add 1 for '=' and 1 for '&' (except for the first param)
2062
- return totalLength + keyLength + valueLength + (totalLength > 0 ? 2 : 1);
2063
- }, 0);
2064
- }, []);
2065
- const getUrlLengthWithSearchParam = react.useCallback((key, value, prev) => {
2066
- const searchParamsLength = getSearchParamsLengthExcluding(key, prev);
2067
- const urlLength = location.href.length - (location.searchStr.length || 0) + (location.hash.length || 0) + searchParamsLength;
2068
- // 1 === '&' and 1 === '='
2069
- return urlLength + 1 + key.length + 1 + value.length;
2070
- }, [getSearchParamsLengthExcluding, location]);
2071
- return react.useMemo(() => ({ getSearchParamsLengthExcluding, getUrlLengthWithSearchParam }), [getSearchParamsLengthExcluding, getUrlLengthWithSearchParam]);
2072
- };
2073
-
2074
- const MAX_URL_LENGTH = 5000;
2075
- /**
2076
- * Get the persistence key for the filter bar.
2077
- *
2078
- * @param {string} name - The name of the filter bar.
2079
- * @param {string} clientSideUserId - The client side user id.
2080
- * @returns {string} The persistence key.
2081
- */
2082
- const getPersistenceKey = (name, clientSideUserId) => `filter-${name}-${clientSideUserId}`;
2083
2047
  /**
2084
2048
  * Custom hook for managing the persistence of filter bar configurations.
2085
2049
  */
2086
2050
  const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue, loadData: inputLoadData, saveData: inputSaveData, }) => {
2087
2051
  const { clientSideUserId } = reactCoreHooks.useCurrentUser();
2088
- const search = reactRouter.useSearch({ strict: false, shouldThrow: false });
2089
- const location = reactRouter.useLocation();
2090
- const navigate = reactRouter.useNavigate();
2091
2052
  const { encode, decode } = reactComponents.useCustomEncoding();
2092
- const { getUrlLengthWithSearchParam } = useSearchUtils();
2093
- const updateSearch = react.useCallback(async (searchParams) => {
2094
- if (!inputLoadData && !inputSaveData && Boolean(search)) {
2095
- // should check if the state has actually changed from what we last sent to the URL
2096
- if (!searchParams[name] ||
2097
- (typeof searchParams[name] === "string" &&
2098
- !dequal.dequal(decode(searchParams[name]), typeof search[name] === "string" ? decode(search[name]) : search[name]))) {
2099
- return requestAnimationFrame(async () => {
2100
- const replace = search[name] === undefined || search[name] === null;
2101
- await navigate({
2102
- to: ".",
2103
- search: (prev) => {
2104
- if (getUrlLengthWithSearchParam(name, searchParams[name] || "", prev) <= MAX_URL_LENGTH) {
2105
- return { ...prev, ...searchParams };
2106
- }
2107
- else {
2108
- // eslint-disable-next-line no-console
2109
- 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)}`);
2110
- const newSearchParams = { ...prev, [name]: undefined };
2111
- return newSearchParams;
2112
- }
2113
- },
2114
- hash: location.hash,
2115
- replace,
2116
- });
2117
- });
2118
- }
2119
- }
2120
- return Promise.resolve();
2121
- }, [navigate, name, search, inputLoadData, inputSaveData, decode, getUrlLengthWithSearchParam, location.hash]);
2122
- const getFromLocalStorage = react.useCallback((persistenceKey) => {
2123
- return localStorage.getItem(getPersistenceKey(persistenceKey, clientSideUserId));
2124
- }, [clientSideUserId]);
2125
- const lastName = react.useRef(name);
2126
- const [initialStoredFilters] = react.useState(() => (!inputLoadData && getFromLocalStorage(name)) || "{}");
2127
- const { getFilterValuesFromUrl, getFilterValuesToUrl } = useFilterUrlSync();
2128
- const lastSavedStateRef = react.useRef(undefined);
2129
- const lastSearchUpdateRef = react.useRef(undefined);
2130
2053
  const refreshDataRef = react.useRef(refreshData);
2131
2054
  react.useEffect(() => {
2132
2055
  refreshDataRef.current = refreshData;
2133
2056
  }, [refreshData]);
2134
- // Add this useEffect to detect external URL changes
2135
- react.useEffect(() => {
2136
- if (!inputLoadData && !inputSaveData) {
2137
- const currentSearchValue = search?.[name];
2138
- let currentSearchValueParsed;
2139
- let lastSearchUpdateRefParsed;
2140
- try {
2141
- currentSearchValueParsed = currentSearchValue !== undefined && currentSearchValue !== null ? decode(currentSearchValue) : undefined;
2142
- }
2143
- catch (_) {
2144
- // Invalid compressed data, treat as undefined
2145
- currentSearchValueParsed = undefined;
2146
- }
2147
- try {
2148
- lastSearchUpdateRefParsed = lastSearchUpdateRef.current ? decode(lastSearchUpdateRef.current) : undefined;
2149
- }
2150
- catch (_) {
2151
- // Invalid compressed data, treat as undefined
2152
- lastSearchUpdateRefParsed = undefined;
2153
- }
2154
- // H4sIADGRuGgAA3WTy07eQAxGXwVlXUtje27uruqWd0C2x1NaAa2AdoN49xq6-LOgUpQo0onny7H9ctzHsy591q8_73_dxXM8xNPT8fnl-KN3v-P4fHy5vj4-HQ96__5yd3e8fjqeQh_9Nt9v48fVfayr9f3b1YrHBH3f3GDBaj4Zui-HuvsADeO8KeFGrmYrj8hCbzTZrF0coRMRVEYHGcKAsW1Iw8qiF3ox06zbASth1t4GVsqCUvIxqO9R-EJ7w2aDJnjHCbV6JtmlgOSHQzB4DLvQtiwiRGB3aUk3h0lLgdyjFiyD8USPhd55BYzakkbaSbeA2jfmFaV7v9BMErKLguWpUNdQEKwTsqqXNETL_eRkbqojy1K0pCtVkK4LJM2VPtk6lQtdcIpSs2RGno9aQG0LSJrmvgrKHCeDFWWQN0iNNZMYgczFQNtH0VbqKifaq7uVBFdvBIgRIBoClcLXQFlC7VR7E7l2PdHKnH36kN5rJFY96Z0m3mhTKv-hG8cscytgU0vLnAOlTNAWdd_TR11y8m3dsxsGyJp_2XOg5hyZOxaP2Qu6nX137DbnW5L0_S-3p9CPk2hC09sG15SXgrKrHgQlpYe6ZKl9oScNb74IOmMmmbkSYmmHdYWZ7z3baWKpiWq0nA6u2fkEYFoOS-_OtibuIqfdaa0oDytgnB2saSC3YTSI0UKIc9nofWJf_wIRCrRd6AMAAA
2155
- // Check if this is an external URL change (not from our own updates)
2156
- // Only trigger refreshData if there's a meaningful change in the URL value
2157
- if (search?.[name] !== undefined && !dequal.dequal(lastSearchUpdateRefParsed, currentSearchValueParsed)) {
2158
- // This is an external URL change with actual data, trigger refreshData
2159
- void requestAnimationFrame(() => {
2160
- if (refreshDataRef.current) {
2161
- refreshDataRef.current();
2162
- }
2163
- });
2164
- }
2165
- }
2166
- }, [search, name, inputLoadData, inputSaveData, decode]);
2057
+ const { searchValue, updateSearchParam } = reactComponents.useSearchParamSync({
2058
+ key: name,
2059
+ enabled: !inputLoadData && !inputSaveData,
2060
+ onExternalChange: () => {
2061
+ refreshDataRef.current?.();
2062
+ },
2063
+ });
2064
+ const storageKey = reactComponents.useStorageKey(`filter-${name}`, clientSideUserId);
2065
+ const getFromLocalStorage = react.useCallback(() => {
2066
+ return localStorage.getItem(storageKey);
2067
+ }, [storageKey]);
2068
+ const lastName = react.useRef(name);
2069
+ const [initialStoredFilters] = react.useState(() => (!inputLoadData && getFromLocalStorage()) || "{}");
2070
+ const { getFilterValuesFromUrl, getFilterValuesToUrl } = useFilterUrlSync();
2071
+ const lastSavedStateRef = react.useRef(undefined);
2167
2072
  const saveData = react.useCallback((filterBarConfig, filterBarDefinitions) => {
2168
2073
  const newValues = Object.assign({}, lastSavedStateRef.current || {});
2169
2074
  if (filterBarConfig.values) {
@@ -2184,22 +2089,16 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
2184
2089
  inputSaveData(newValues);
2185
2090
  }
2186
2091
  else {
2187
- localStorage.setItem(getPersistenceKey(name, clientSideUserId), JSON.stringify(toPersist));
2092
+ localStorage.setItem(storageKey, reactComponents.storageSerializer.serialize(toPersist));
2188
2093
  const urlObject = getFilterValuesToUrl(newValues, sharedUtils.objectValues(filterBarDefinitions), false, isDefaultValue);
2189
- const result = {};
2190
- if (filterBarConfig.name) {
2191
- result[filterBarConfig.name] = encode(urlObject);
2192
- // Update the ref BEFORE updating the URL to prevent false external change detection
2193
- lastSearchUpdateRef.current = result[filterBarConfig.name];
2194
- }
2195
- updateSearch(result);
2094
+ updateSearchParam(encode(urlObject));
2196
2095
  }
2197
2096
  }
2198
- }, [name, clientSideUserId, inputSaveData, getFilterValuesToUrl, updateSearch, encode, isDefaultValue]);
2097
+ }, [storageKey, inputSaveData, getFilterValuesToUrl, updateSearchParam, encode, isDefaultValue]);
2199
2098
  const loadFromLocalStorage = react.useCallback(() => {
2200
2099
  let storedFilters = null;
2201
2100
  if (lastName.current !== name) {
2202
- storedFilters = localStorage.getItem(getPersistenceKey(name, clientSideUserId)) || "{}";
2101
+ storedFilters = localStorage.getItem(storageKey) || "{}";
2203
2102
  lastName.current = name;
2204
2103
  }
2205
2104
  else {
@@ -2207,29 +2106,34 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
2207
2106
  }
2208
2107
  if (storedFilters && storedFilters !== "undefined") {
2209
2108
  try {
2210
- const loadedFilterBarConfigValues = JSON.parse(storedFilters);
2211
- return loadedFilterBarConfigValues?.values ?? {};
2109
+ const deserialized = reactComponents.storageSerializer.deserialize(storedFilters);
2110
+ if (typeof deserialized === "object" && deserialized !== null && !Array.isArray(deserialized)) {
2111
+ const record = Object.fromEntries(Object.entries(deserialized));
2112
+ return record.values ?? {};
2113
+ }
2114
+ return {};
2212
2115
  }
2213
- catch (error) {
2116
+ catch {
2214
2117
  return {};
2215
2118
  }
2216
2119
  }
2217
2120
  return {};
2218
- }, [clientSideUserId, name, initialStoredFilters, lastName]);
2121
+ }, [storageKey, name, initialStoredFilters, lastName]);
2219
2122
  const loadFromSearchURL = react.useCallback(() => {
2220
- let searchParams = search ?? {};
2221
- const searchParamValue = searchParams[name];
2222
- if (searchParamValue !== undefined && searchParamValue !== null) {
2123
+ if (searchValue !== undefined) {
2223
2124
  try {
2224
- searchParams = decode(searchParamValue);
2125
+ const decoded = decode(searchValue);
2126
+ if (typeof decoded === "object" && !Array.isArray(decoded)) {
2127
+ return Object.fromEntries(Object.entries(decoded));
2128
+ }
2129
+ return {};
2225
2130
  }
2226
- catch (error) {
2227
- searchParams = {};
2131
+ catch {
2132
+ return {};
2228
2133
  }
2229
- return searchParams;
2230
2134
  }
2231
2135
  return null;
2232
- }, [search, name, decode]);
2136
+ }, [searchValue, decode]);
2233
2137
  const loadValues = react.useCallback((filterDefinitions) => {
2234
2138
  if (inputLoadData) {
2235
2139
  return inputLoadData();
@@ -2240,7 +2144,6 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
2240
2144
  if (searchParams !== null && sharedUtils.objectKeys(searchParams).length > 0) {
2241
2145
  const valuesFromUrl = getFilterValuesFromUrl(filterDefinitions, searchParams);
2242
2146
  if (sharedUtils.objectKeys(valuesFromUrl).length > 0) {
2243
- // if there are values from the URL, update ALL filters values based on the searchParams or their defaults
2244
2147
  filterDefinitions.forEach(filter => {
2245
2148
  const key = filter.filterKey;
2246
2149
  if (key in valuesFromUrl) {
@@ -2300,19 +2203,9 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
2300
2203
  initialFilterBarConfig.setters[`set${stringTs.capitalize(key)}`] = (callback) => setValue(key, callback);
2301
2204
  });
2302
2205
  const urlObject = getFilterValuesToUrl(initialFilterBarConfig.values, updatedFilterDefinitionsValues, false, isDefaultValue);
2303
- let needUpdate = false;
2304
- const result = {};
2305
- if (initialFilterBarConfig.name) {
2306
- result[initialFilterBarConfig.name] = encode(urlObject);
2307
- needUpdate = !dequal.dequal(lastSearchUpdateRef.current, result[initialFilterBarConfig.name]);
2308
- // Update the ref BEFORE updating the URL to prevent false external change detection
2309
- lastSearchUpdateRef.current = result[initialFilterBarConfig.name];
2310
- }
2311
- if (needUpdate) {
2312
- void updateSearch(result);
2313
- }
2206
+ updateSearchParam(encode(urlObject));
2314
2207
  return initialFilterBarConfig;
2315
- }, [name, setValue, loadValues, getFilterValuesToUrl, updateSearch, encode, isDefaultValue]);
2208
+ }, [name, setValue, loadValues, getFilterValuesToUrl, updateSearchParam, encode, isDefaultValue]);
2316
2209
  return react.useMemo(() => ({ loadData, saveData, getFilterValuesToUrl }), [loadData, saveData, getFilterValuesToUrl]);
2317
2210
  };
2318
2211
 
package/index.esm.js CHANGED
@@ -2,7 +2,7 @@ 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, useList, List, Text, useTextSearch, Card, CardBody, useViewportBreakpoints, Popover, PopoverTrigger, Tooltip, Badge, PopoverContent, IconButton, MenuList, useCustomEncoding, useWatch } from '@trackunit/react-components';
5
+ import { Button, Icon, useList, List, Text, useTextSearch, Card, CardBody, useViewportBreakpoints, Popover, PopoverTrigger, Tooltip, Badge, PopoverContent, IconButton, MenuList, useCustomEncoding, useSearchParamSync, useStorageKey, storageSerializer, useWatch } from '@trackunit/react-components';
6
6
  import { useAnalytics, useCurrentUser } from '@trackunit/react-core-hooks';
7
7
  import { capitalize } from 'string-ts';
8
8
  import { createEvent } from '@trackunit/iris-app-runtime-core-api';
@@ -15,7 +15,6 @@ import { cvaMerge } from '@trackunit/css-class-variance-utilities';
15
15
  import { dequal } from 'dequal';
16
16
  import { geoJsonPolygonSchema, geoJsonMultiPolygonSchema } from '@trackunit/geo-json-utils';
17
17
  import { z } from 'zod';
18
- import { useLocation, useSearch, useNavigate } from '@tanstack/react-router';
19
18
 
20
19
  var defaultTranslations = {
21
20
  "access.management.filter.operator.role.keyAdmin": "Key Admin",
@@ -2043,125 +2042,31 @@ const useFilterUrlSync = () => {
2043
2042
  }), [getFilterValuesFromUrl, getFilterValuesToUrl]);
2044
2043
  };
2045
2044
 
2046
- /**
2047
- * This hook provides functionality to:
2048
- * - Get the length of the search parameters excluding a specific key
2049
- */
2050
- const useSearchUtils = () => {
2051
- const location = useLocation();
2052
- const getSearchParamsLengthExcluding = useCallback((excludeKey, searchParams) => {
2053
- return objectKeys(searchParams)
2054
- .filter(key => key !== excludeKey)
2055
- .reduce((totalLength, key) => {
2056
- const keyLength = encodeURIComponent(String(key)).length;
2057
- const value = searchParams[key] !== null ? encodeURIComponent(searchParams[key]?.toString() ?? "") : "";
2058
- const valueLength = value.length;
2059
- // Add 1 for '=' and 1 for '&' (except for the first param)
2060
- return totalLength + keyLength + valueLength + (totalLength > 0 ? 2 : 1);
2061
- }, 0);
2062
- }, []);
2063
- const getUrlLengthWithSearchParam = useCallback((key, value, prev) => {
2064
- const searchParamsLength = getSearchParamsLengthExcluding(key, prev);
2065
- const urlLength = location.href.length - (location.searchStr.length || 0) + (location.hash.length || 0) + searchParamsLength;
2066
- // 1 === '&' and 1 === '='
2067
- return urlLength + 1 + key.length + 1 + value.length;
2068
- }, [getSearchParamsLengthExcluding, location]);
2069
- return useMemo(() => ({ getSearchParamsLengthExcluding, getUrlLengthWithSearchParam }), [getSearchParamsLengthExcluding, getUrlLengthWithSearchParam]);
2070
- };
2071
-
2072
- const MAX_URL_LENGTH = 5000;
2073
- /**
2074
- * Get the persistence key for the filter bar.
2075
- *
2076
- * @param {string} name - The name of the filter bar.
2077
- * @param {string} clientSideUserId - The client side user id.
2078
- * @returns {string} The persistence key.
2079
- */
2080
- const getPersistenceKey = (name, clientSideUserId) => `filter-${name}-${clientSideUserId}`;
2081
2045
  /**
2082
2046
  * Custom hook for managing the persistence of filter bar configurations.
2083
2047
  */
2084
2048
  const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue, loadData: inputLoadData, saveData: inputSaveData, }) => {
2085
2049
  const { clientSideUserId } = useCurrentUser();
2086
- const search = useSearch({ strict: false, shouldThrow: false });
2087
- const location = useLocation();
2088
- const navigate = useNavigate();
2089
2050
  const { encode, decode } = useCustomEncoding();
2090
- const { getUrlLengthWithSearchParam } = useSearchUtils();
2091
- const updateSearch = useCallback(async (searchParams) => {
2092
- if (!inputLoadData && !inputSaveData && Boolean(search)) {
2093
- // should check if the state has actually changed from what we last sent to the URL
2094
- if (!searchParams[name] ||
2095
- (typeof searchParams[name] === "string" &&
2096
- !dequal(decode(searchParams[name]), typeof search[name] === "string" ? decode(search[name]) : search[name]))) {
2097
- return requestAnimationFrame(async () => {
2098
- const replace = search[name] === undefined || search[name] === null;
2099
- await navigate({
2100
- to: ".",
2101
- search: (prev) => {
2102
- if (getUrlLengthWithSearchParam(name, searchParams[name] || "", prev) <= MAX_URL_LENGTH) {
2103
- return { ...prev, ...searchParams };
2104
- }
2105
- else {
2106
- // eslint-disable-next-line no-console
2107
- 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)}`);
2108
- const newSearchParams = { ...prev, [name]: undefined };
2109
- return newSearchParams;
2110
- }
2111
- },
2112
- hash: location.hash,
2113
- replace,
2114
- });
2115
- });
2116
- }
2117
- }
2118
- return Promise.resolve();
2119
- }, [navigate, name, search, inputLoadData, inputSaveData, decode, getUrlLengthWithSearchParam, location.hash]);
2120
- const getFromLocalStorage = useCallback((persistenceKey) => {
2121
- return localStorage.getItem(getPersistenceKey(persistenceKey, clientSideUserId));
2122
- }, [clientSideUserId]);
2123
- const lastName = useRef(name);
2124
- const [initialStoredFilters] = useState(() => (!inputLoadData && getFromLocalStorage(name)) || "{}");
2125
- const { getFilterValuesFromUrl, getFilterValuesToUrl } = useFilterUrlSync();
2126
- const lastSavedStateRef = useRef(undefined);
2127
- const lastSearchUpdateRef = useRef(undefined);
2128
2051
  const refreshDataRef = useRef(refreshData);
2129
2052
  useEffect(() => {
2130
2053
  refreshDataRef.current = refreshData;
2131
2054
  }, [refreshData]);
2132
- // Add this useEffect to detect external URL changes
2133
- useEffect(() => {
2134
- if (!inputLoadData && !inputSaveData) {
2135
- const currentSearchValue = search?.[name];
2136
- let currentSearchValueParsed;
2137
- let lastSearchUpdateRefParsed;
2138
- try {
2139
- currentSearchValueParsed = currentSearchValue !== undefined && currentSearchValue !== null ? decode(currentSearchValue) : undefined;
2140
- }
2141
- catch (_) {
2142
- // Invalid compressed data, treat as undefined
2143
- currentSearchValueParsed = undefined;
2144
- }
2145
- try {
2146
- lastSearchUpdateRefParsed = lastSearchUpdateRef.current ? decode(lastSearchUpdateRef.current) : undefined;
2147
- }
2148
- catch (_) {
2149
- // Invalid compressed data, treat as undefined
2150
- lastSearchUpdateRefParsed = undefined;
2151
- }
2152
- // H4sIADGRuGgAA3WTy07eQAxGXwVlXUtje27uruqWd0C2x1NaAa2AdoN49xq6-LOgUpQo0onny7H9ctzHsy591q8_73_dxXM8xNPT8fnl-KN3v-P4fHy5vj4-HQ96__5yd3e8fjqeQh_9Nt9v48fVfayr9f3b1YrHBH3f3GDBaj4Zui-HuvsADeO8KeFGrmYrj8hCbzTZrF0coRMRVEYHGcKAsW1Iw8qiF3ox06zbASth1t4GVsqCUvIxqO9R-EJ7w2aDJnjHCbV6JtmlgOSHQzB4DLvQtiwiRGB3aUk3h0lLgdyjFiyD8USPhd55BYzakkbaSbeA2jfmFaV7v9BMErKLguWpUNdQEKwTsqqXNETL_eRkbqojy1K0pCtVkK4LJM2VPtk6lQtdcIpSs2RGno9aQG0LSJrmvgrKHCeDFWWQN0iNNZMYgczFQNtH0VbqKifaq7uVBFdvBIgRIBoClcLXQFlC7VR7E7l2PdHKnH36kN5rJFY96Z0m3mhTKv-hG8cscytgU0vLnAOlTNAWdd_TR11y8m3dsxsGyJp_2XOg5hyZOxaP2Qu6nX137DbnW5L0_S-3p9CPk2hC09sG15SXgrKrHgQlpYe6ZKl9oScNb74IOmMmmbkSYmmHdYWZ7z3baWKpiWq0nA6u2fkEYFoOS-_OtibuIqfdaa0oDytgnB2saSC3YTSI0UKIc9nofWJf_wIRCrRd6AMAAA
2153
- // Check if this is an external URL change (not from our own updates)
2154
- // Only trigger refreshData if there's a meaningful change in the URL value
2155
- if (search?.[name] !== undefined && !dequal(lastSearchUpdateRefParsed, currentSearchValueParsed)) {
2156
- // This is an external URL change with actual data, trigger refreshData
2157
- void requestAnimationFrame(() => {
2158
- if (refreshDataRef.current) {
2159
- refreshDataRef.current();
2160
- }
2161
- });
2162
- }
2163
- }
2164
- }, [search, name, inputLoadData, inputSaveData, decode]);
2055
+ const { searchValue, updateSearchParam } = useSearchParamSync({
2056
+ key: name,
2057
+ enabled: !inputLoadData && !inputSaveData,
2058
+ onExternalChange: () => {
2059
+ refreshDataRef.current?.();
2060
+ },
2061
+ });
2062
+ const storageKey = useStorageKey(`filter-${name}`, clientSideUserId);
2063
+ const getFromLocalStorage = useCallback(() => {
2064
+ return localStorage.getItem(storageKey);
2065
+ }, [storageKey]);
2066
+ const lastName = useRef(name);
2067
+ const [initialStoredFilters] = useState(() => (!inputLoadData && getFromLocalStorage()) || "{}");
2068
+ const { getFilterValuesFromUrl, getFilterValuesToUrl } = useFilterUrlSync();
2069
+ const lastSavedStateRef = useRef(undefined);
2165
2070
  const saveData = useCallback((filterBarConfig, filterBarDefinitions) => {
2166
2071
  const newValues = Object.assign({}, lastSavedStateRef.current || {});
2167
2072
  if (filterBarConfig.values) {
@@ -2182,22 +2087,16 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
2182
2087
  inputSaveData(newValues);
2183
2088
  }
2184
2089
  else {
2185
- localStorage.setItem(getPersistenceKey(name, clientSideUserId), JSON.stringify(toPersist));
2090
+ localStorage.setItem(storageKey, storageSerializer.serialize(toPersist));
2186
2091
  const urlObject = getFilterValuesToUrl(newValues, objectValues(filterBarDefinitions), false, isDefaultValue);
2187
- const result = {};
2188
- if (filterBarConfig.name) {
2189
- result[filterBarConfig.name] = encode(urlObject);
2190
- // Update the ref BEFORE updating the URL to prevent false external change detection
2191
- lastSearchUpdateRef.current = result[filterBarConfig.name];
2192
- }
2193
- updateSearch(result);
2092
+ updateSearchParam(encode(urlObject));
2194
2093
  }
2195
2094
  }
2196
- }, [name, clientSideUserId, inputSaveData, getFilterValuesToUrl, updateSearch, encode, isDefaultValue]);
2095
+ }, [storageKey, inputSaveData, getFilterValuesToUrl, updateSearchParam, encode, isDefaultValue]);
2197
2096
  const loadFromLocalStorage = useCallback(() => {
2198
2097
  let storedFilters = null;
2199
2098
  if (lastName.current !== name) {
2200
- storedFilters = localStorage.getItem(getPersistenceKey(name, clientSideUserId)) || "{}";
2099
+ storedFilters = localStorage.getItem(storageKey) || "{}";
2201
2100
  lastName.current = name;
2202
2101
  }
2203
2102
  else {
@@ -2205,29 +2104,34 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
2205
2104
  }
2206
2105
  if (storedFilters && storedFilters !== "undefined") {
2207
2106
  try {
2208
- const loadedFilterBarConfigValues = JSON.parse(storedFilters);
2209
- return loadedFilterBarConfigValues?.values ?? {};
2107
+ const deserialized = storageSerializer.deserialize(storedFilters);
2108
+ if (typeof deserialized === "object" && deserialized !== null && !Array.isArray(deserialized)) {
2109
+ const record = Object.fromEntries(Object.entries(deserialized));
2110
+ return record.values ?? {};
2111
+ }
2112
+ return {};
2210
2113
  }
2211
- catch (error) {
2114
+ catch {
2212
2115
  return {};
2213
2116
  }
2214
2117
  }
2215
2118
  return {};
2216
- }, [clientSideUserId, name, initialStoredFilters, lastName]);
2119
+ }, [storageKey, name, initialStoredFilters, lastName]);
2217
2120
  const loadFromSearchURL = useCallback(() => {
2218
- let searchParams = search ?? {};
2219
- const searchParamValue = searchParams[name];
2220
- if (searchParamValue !== undefined && searchParamValue !== null) {
2121
+ if (searchValue !== undefined) {
2221
2122
  try {
2222
- searchParams = decode(searchParamValue);
2123
+ const decoded = decode(searchValue);
2124
+ if (typeof decoded === "object" && !Array.isArray(decoded)) {
2125
+ return Object.fromEntries(Object.entries(decoded));
2126
+ }
2127
+ return {};
2223
2128
  }
2224
- catch (error) {
2225
- searchParams = {};
2129
+ catch {
2130
+ return {};
2226
2131
  }
2227
- return searchParams;
2228
2132
  }
2229
2133
  return null;
2230
- }, [search, name, decode]);
2134
+ }, [searchValue, decode]);
2231
2135
  const loadValues = useCallback((filterDefinitions) => {
2232
2136
  if (inputLoadData) {
2233
2137
  return inputLoadData();
@@ -2238,7 +2142,6 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
2238
2142
  if (searchParams !== null && objectKeys(searchParams).length > 0) {
2239
2143
  const valuesFromUrl = getFilterValuesFromUrl(filterDefinitions, searchParams);
2240
2144
  if (objectKeys(valuesFromUrl).length > 0) {
2241
- // if there are values from the URL, update ALL filters values based on the searchParams or their defaults
2242
2145
  filterDefinitions.forEach(filter => {
2243
2146
  const key = filter.filterKey;
2244
2147
  if (key in valuesFromUrl) {
@@ -2298,19 +2201,9 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
2298
2201
  initialFilterBarConfig.setters[`set${capitalize(key)}`] = (callback) => setValue(key, callback);
2299
2202
  });
2300
2203
  const urlObject = getFilterValuesToUrl(initialFilterBarConfig.values, updatedFilterDefinitionsValues, false, isDefaultValue);
2301
- let needUpdate = false;
2302
- const result = {};
2303
- if (initialFilterBarConfig.name) {
2304
- result[initialFilterBarConfig.name] = encode(urlObject);
2305
- needUpdate = !dequal(lastSearchUpdateRef.current, result[initialFilterBarConfig.name]);
2306
- // Update the ref BEFORE updating the URL to prevent false external change detection
2307
- lastSearchUpdateRef.current = result[initialFilterBarConfig.name];
2308
- }
2309
- if (needUpdate) {
2310
- void updateSearch(result);
2311
- }
2204
+ updateSearchParam(encode(urlObject));
2312
2205
  return initialFilterBarConfig;
2313
- }, [name, setValue, loadValues, getFilterValuesToUrl, updateSearch, encode, isDefaultValue]);
2206
+ }, [name, setValue, loadValues, getFilterValuesToUrl, updateSearchParam, encode, isDefaultValue]);
2314
2207
  return useMemo(() => ({ loadData, saveData, getFilterValuesToUrl }), [loadData, saveData, getFilterValuesToUrl]);
2315
2208
  };
2316
2209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/filters-filter-bar",
3
- "version": "1.21.4",
3
+ "version": "1.21.7",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -10,18 +10,18 @@
10
10
  "dequal": "^2.0.3",
11
11
  "tailwind-merge": "^2.0.0",
12
12
  "string-ts": "^2.0.0",
13
- "zod": "^3.23.8",
13
+ "zod": "^3.25.76",
14
14
  "@trackunit/iris-app-api": "1.17.2",
15
- "@trackunit/react-core-hooks": "1.15.11",
16
- "@trackunit/react-filter-components": "1.20.4",
17
- "@trackunit/react-date-and-time-components": "1.23.4",
15
+ "@trackunit/react-core-hooks": "1.15.12",
16
+ "@trackunit/react-filter-components": "1.20.7",
17
+ "@trackunit/react-date-and-time-components": "1.23.7",
18
18
  "@trackunit/shared-utils": "1.13.97",
19
- "@trackunit/react-form-components": "1.21.4",
20
- "@trackunit/iris-app-runtime-core-api": "1.14.10",
19
+ "@trackunit/react-form-components": "1.21.7",
20
+ "@trackunit/iris-app-runtime-core-api": "1.14.11",
21
21
  "@trackunit/geo-json-utils": "1.12.8",
22
- "@trackunit/i18n-library-translation": "1.18.2",
22
+ "@trackunit/i18n-library-translation": "1.18.3",
23
23
  "@trackunit/css-class-variance-utilities": "1.11.97",
24
- "@trackunit/react-components": "1.21.15"
24
+ "@trackunit/react-components": "1.21.18"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "@apollo/client": "^3.13.8",
@@ -9,13 +9,9 @@ export interface FilterBarPersistenceProps<TFilterBarDefinition extends FilterBa
9
9
  */
10
10
  isDefaultValue: (key: string, value: FilterValueType) => boolean;
11
11
  }
12
- export declare const MAX_URL_LENGTH = 5000;
12
+ export { MAX_URL_LENGTH } from "@trackunit/react-components";
13
13
  /**
14
- * Get the persistence key for the filter bar.
15
- *
16
- * @param {string} name - The name of the filter bar.
17
- * @param {string} clientSideUserId - The client side user id.
18
- * @returns {string} The persistence key.
14
+ * @deprecated Use `useStorageKey` from `@trackunit/react-components` with key `filter-${name}` instead.
19
15
  */
20
16
  export declare const getPersistenceKey: (name: string, clientSideUserId?: string) => string;
21
17
  /**
@@ -72,4 +68,3 @@ type UseFilterBarPersistenceReturn<TFilterBarDefinition extends FilterBarDefinit
72
68
  * Custom hook for managing the persistence of filter bar configurations.
73
69
  */
74
70
  export declare const useFilterBarPersistence: <TFilterBarDefinition extends FilterBarDefinition>({ name, setValue, refreshData, isDefaultValue, loadData: inputLoadData, saveData: inputSaveData, }: FilterBarPersistenceProps<TFilterBarDefinition>) => UseFilterBarPersistenceReturn<TFilterBarDefinition>;
75
- export {};