@trackunit/filters-filter-bar 2.6.2 → 2.6.3

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.
Files changed (3) hide show
  1. package/index.cjs.js +24 -27
  2. package/index.esm.js +26 -29
  3. package/package.json +9 -9
package/index.cjs.js CHANGED
@@ -1911,6 +1911,12 @@ const useFilterUrlSync = () => {
1911
1911
  }), [getFilterValuesFromUrl, getFilterValuesToUrl]);
1912
1912
  };
1913
1913
 
1914
+ const filterBarPersistedSchema = zod.z.object({
1915
+ values: zod.z.record(zod.z.string(), zod.z.unknown()),
1916
+ });
1917
+ // A fresh object per call — callers (e.g. loadValues) mutate the returned `values` object
1918
+ // directly, so this must never be a shared reference reused across calls.
1919
+ const createDefaultFilterBarConfigPersisted = () => ({ values: {} });
1914
1920
  /**
1915
1921
  * Custom hook for managing the persistence of filter bar configurations.
1916
1922
  */
@@ -1929,13 +1935,17 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
1929
1935
  },
1930
1936
  });
1931
1937
  const storageKey = reactComponents.useStorageKey(`filter-${name}`, clientSideUserId);
1932
- const getFromLocalStorage = react.useCallback(() => {
1933
- // TODO: migrate to useLocalStorage hook this file was exempted when the no-direct-storage-access rule was introduced
1934
- // eslint-disable-next-line no-restricted-globals
1935
- return localStorage.getItem(storageKey);
1936
- }, [storageKey]);
1938
+ // Custom mode (inputLoadData/inputSaveData provided) must never touch localStorage at all —
1939
+ // useLocalStorage's write-on-mount behavior would create an entry even if its setter is never
1940
+ // called, so this reads/writes explicitly via the non-hook accessors instead, preserving the
1941
+ // exact current imperative control flow (including custom mode's complete storage bypass).
1942
+ const getFromLocalStorage = react.useCallback(() => sharedUtils.readLocalStorageValue({
1943
+ key: storageKey,
1944
+ defaultState: createDefaultFilterBarConfigPersisted(),
1945
+ schema: filterBarPersistedSchema,
1946
+ }), [storageKey]);
1937
1947
  const lastName = react.useRef(name);
1938
- const [initialStoredFilters] = react.useState(() => (!inputLoadData && getFromLocalStorage()) || "{}");
1948
+ const [initialStoredFilters] = react.useState(() => !inputLoadData ? getFromLocalStorage() : null);
1939
1949
  const { getFilterValuesFromUrl, getFilterValuesToUrl } = useFilterUrlSync();
1940
1950
  const lastSavedStateRef = react.useRef(undefined);
1941
1951
  const saveData = react.useCallback((filterBarConfig, filterBarDefinitions) => {
@@ -1958,38 +1968,25 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
1958
1968
  inputSaveData(newValues);
1959
1969
  }
1960
1970
  else {
1961
- // eslint-disable-next-line no-restricted-globals
1962
- localStorage.setItem(storageKey, reactComponents.storageSerializer.serialize(toPersist));
1971
+ sharedUtils.writeLocalStorageValue({ key: storageKey, value: toPersist, schema: filterBarPersistedSchema });
1963
1972
  const urlObject = getFilterValuesToUrl(newValues, sharedUtils.objectValues(filterBarDefinitions), false, isDefaultValue);
1964
1973
  updateSearchParam(encode(urlObject));
1965
1974
  }
1966
1975
  }
1967
1976
  }, [storageKey, inputSaveData, getFilterValuesToUrl, updateSearchParam, encode, isDefaultValue]);
1968
1977
  const loadFromLocalStorage = react.useCallback(() => {
1969
- let storedFilters = null;
1978
+ let stored;
1970
1979
  if (lastName.current !== name) {
1971
- // eslint-disable-next-line no-restricted-globals
1972
- storedFilters = localStorage.getItem(storageKey) || "{}";
1980
+ stored = getFromLocalStorage();
1973
1981
  lastName.current = name;
1974
1982
  }
1975
1983
  else {
1976
- storedFilters = initialStoredFilters;
1977
- }
1978
- if (storedFilters && storedFilters !== "undefined") {
1979
- try {
1980
- const deserialized = reactComponents.storageSerializer.deserialize(storedFilters);
1981
- if (typeof deserialized === "object" && deserialized !== null && !Array.isArray(deserialized)) {
1982
- const record = Object.fromEntries(Object.entries(deserialized));
1983
- return record.values ?? {};
1984
- }
1985
- return {};
1986
- }
1987
- catch {
1988
- return {};
1989
- }
1984
+ stored = initialStoredFilters ?? createDefaultFilterBarConfigPersisted();
1990
1985
  }
1991
- return {};
1992
- }, [storageKey, name, initialStoredFilters, lastName]);
1986
+ // Callers (loadValues) mutate the returned object directly — return a fresh shallow copy
1987
+ // each time so a mutation doesn't leak into the cached initialStoredFilters for next time.
1988
+ return { ...stored.values };
1989
+ }, [name, initialStoredFilters, getFromLocalStorage]);
1993
1990
  const loadFromSearchURL = react.useCallback(() => {
1994
1991
  if (searchValue !== undefined) {
1995
1992
  try {
package/index.esm.js CHANGED
@@ -3,13 +3,13 @@ import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-l
3
3
  import { useMemo, useState, useEffect, useCallback, Fragment as Fragment$1, useRef } from 'react';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import { Filter, FilterBody, RadioFilterItem, CheckBoxFilterItem, FilterHeader as FilterHeader$1, FilterFooter } from '@trackunit/react-filter-components';
6
- import { Button, Icon, useList, List, Text, useTextSearch, cvaMenuList, cvaMenu, ZStack, IconButton, Badge, useViewportBreakpoints, MenuTree, Popover, PopoverTrigger, Tooltip, PopoverContent, useCustomEncoding, useSearchParamSync, useStorageKey, storageSerializer, useWatch } from '@trackunit/react-components';
6
+ import { Button, Icon, useList, List, Text, useTextSearch, cvaMenuList, cvaMenu, ZStack, IconButton, Badge, useViewportBreakpoints, MenuTree, Popover, PopoverTrigger, Tooltip, PopoverContent, useCustomEncoding, useSearchParamSync, useStorageKey, useWatch } from '@trackunit/react-components';
7
7
  import { useAnalytics, useCurrentUser } from '@trackunit/react-core-hooks';
8
8
  import { capitalize } from 'string-ts';
9
9
  import { createEvent } from '@trackunit/iris-app-runtime-core-api';
10
10
  import { Search, NumberField, RadioGroup } from '@trackunit/react-form-components';
11
11
  import { DayRangePicker } from '@trackunit/react-date-and-time-components';
12
- import { nonNullable, capitalize as capitalize$1, objectValues, truthy, objectKeys } from '@trackunit/shared-utils';
12
+ import { nonNullable, capitalize as capitalize$1, objectValues, truthy, objectKeys, readLocalStorageValue, writeLocalStorageValue } from '@trackunit/shared-utils';
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';
@@ -1909,6 +1909,12 @@ const useFilterUrlSync = () => {
1909
1909
  }), [getFilterValuesFromUrl, getFilterValuesToUrl]);
1910
1910
  };
1911
1911
 
1912
+ const filterBarPersistedSchema = z.object({
1913
+ values: z.record(z.string(), z.unknown()),
1914
+ });
1915
+ // A fresh object per call — callers (e.g. loadValues) mutate the returned `values` object
1916
+ // directly, so this must never be a shared reference reused across calls.
1917
+ const createDefaultFilterBarConfigPersisted = () => ({ values: {} });
1912
1918
  /**
1913
1919
  * Custom hook for managing the persistence of filter bar configurations.
1914
1920
  */
@@ -1927,13 +1933,17 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
1927
1933
  },
1928
1934
  });
1929
1935
  const storageKey = useStorageKey(`filter-${name}`, clientSideUserId);
1930
- const getFromLocalStorage = useCallback(() => {
1931
- // TODO: migrate to useLocalStorage hook this file was exempted when the no-direct-storage-access rule was introduced
1932
- // eslint-disable-next-line no-restricted-globals
1933
- return localStorage.getItem(storageKey);
1934
- }, [storageKey]);
1936
+ // Custom mode (inputLoadData/inputSaveData provided) must never touch localStorage at all —
1937
+ // useLocalStorage's write-on-mount behavior would create an entry even if its setter is never
1938
+ // called, so this reads/writes explicitly via the non-hook accessors instead, preserving the
1939
+ // exact current imperative control flow (including custom mode's complete storage bypass).
1940
+ const getFromLocalStorage = useCallback(() => readLocalStorageValue({
1941
+ key: storageKey,
1942
+ defaultState: createDefaultFilterBarConfigPersisted(),
1943
+ schema: filterBarPersistedSchema,
1944
+ }), [storageKey]);
1935
1945
  const lastName = useRef(name);
1936
- const [initialStoredFilters] = useState(() => (!inputLoadData && getFromLocalStorage()) || "{}");
1946
+ const [initialStoredFilters] = useState(() => !inputLoadData ? getFromLocalStorage() : null);
1937
1947
  const { getFilterValuesFromUrl, getFilterValuesToUrl } = useFilterUrlSync();
1938
1948
  const lastSavedStateRef = useRef(undefined);
1939
1949
  const saveData = useCallback((filterBarConfig, filterBarDefinitions) => {
@@ -1956,38 +1966,25 @@ const useFilterBarPersistence = ({ name, setValue, refreshData, isDefaultValue,
1956
1966
  inputSaveData(newValues);
1957
1967
  }
1958
1968
  else {
1959
- // eslint-disable-next-line no-restricted-globals
1960
- localStorage.setItem(storageKey, storageSerializer.serialize(toPersist));
1969
+ writeLocalStorageValue({ key: storageKey, value: toPersist, schema: filterBarPersistedSchema });
1961
1970
  const urlObject = getFilterValuesToUrl(newValues, objectValues(filterBarDefinitions), false, isDefaultValue);
1962
1971
  updateSearchParam(encode(urlObject));
1963
1972
  }
1964
1973
  }
1965
1974
  }, [storageKey, inputSaveData, getFilterValuesToUrl, updateSearchParam, encode, isDefaultValue]);
1966
1975
  const loadFromLocalStorage = useCallback(() => {
1967
- let storedFilters = null;
1976
+ let stored;
1968
1977
  if (lastName.current !== name) {
1969
- // eslint-disable-next-line no-restricted-globals
1970
- storedFilters = localStorage.getItem(storageKey) || "{}";
1978
+ stored = getFromLocalStorage();
1971
1979
  lastName.current = name;
1972
1980
  }
1973
1981
  else {
1974
- storedFilters = initialStoredFilters;
1975
- }
1976
- if (storedFilters && storedFilters !== "undefined") {
1977
- try {
1978
- const deserialized = storageSerializer.deserialize(storedFilters);
1979
- if (typeof deserialized === "object" && deserialized !== null && !Array.isArray(deserialized)) {
1980
- const record = Object.fromEntries(Object.entries(deserialized));
1981
- return record.values ?? {};
1982
- }
1983
- return {};
1984
- }
1985
- catch {
1986
- return {};
1987
- }
1982
+ stored = initialStoredFilters ?? createDefaultFilterBarConfigPersisted();
1988
1983
  }
1989
- return {};
1990
- }, [storageKey, name, initialStoredFilters, lastName]);
1984
+ // Callers (loadValues) mutate the returned object directly — return a fresh shallow copy
1985
+ // each time so a mutation doesn't leak into the cached initialStoredFilters for next time.
1986
+ return { ...stored.values };
1987
+ }, [name, initialStoredFilters, getFromLocalStorage]);
1991
1988
  const loadFromSearchURL = useCallback(() => {
1992
1989
  if (searchValue !== undefined) {
1993
1990
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/filters-filter-bar",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -12,16 +12,16 @@
12
12
  "string-ts": "^2.0.0",
13
13
  "zod": "^3.25.76",
14
14
  "@trackunit/iris-app-api": "2.4.5",
15
- "@trackunit/react-core-hooks": "1.21.8",
16
- "@trackunit/react-filter-components": "2.6.2",
17
- "@trackunit/react-date-and-time-components": "2.6.2",
18
- "@trackunit/shared-utils": "1.16.15",
19
- "@trackunit/react-form-components": "2.6.2",
20
- "@trackunit/iris-app-runtime-core-api": "1.17.15",
15
+ "@trackunit/react-core-hooks": "1.21.9",
16
+ "@trackunit/react-filter-components": "2.6.3",
17
+ "@trackunit/react-date-and-time-components": "2.6.3",
18
+ "@trackunit/shared-utils": "1.16.16",
19
+ "@trackunit/react-form-components": "2.6.3",
20
+ "@trackunit/iris-app-runtime-core-api": "1.17.16",
21
21
  "@trackunit/geo-json-utils": "1.15.13",
22
- "@trackunit/i18n-library-translation": "2.4.8",
22
+ "@trackunit/i18n-library-translation": "2.4.9",
23
23
  "@trackunit/css-class-variance-utilities": "1.14.13",
24
- "@trackunit/react-components": "2.9.2"
24
+ "@trackunit/react-components": "2.9.3"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "@apollo/client": "^3.13.8",