From 852df20902425078902143a5ebc648856bf2e141 Mon Sep 17 00:00:00 2001 From: Radoslaw Szwajkowski Date: Wed, 4 Sep 2024 23:42:31 +0200 Subject: [PATCH] Support custom serializers in table hooks Signed-off-by: Radoslaw Szwajkowski --- .../active-item/useActiveItemState.ts | 9 ++++ .../expansion/useExpansionState.ts | 9 ++++ .../filtering/useFilterState.ts | 9 ++++ .../pagination/usePaginationState.ts | 9 ++++ .../table-controls/sorting/useSortState.ts | 9 ++++ client/src/app/hooks/table-controls/types.ts | 12 ++++- .../table-controls/useTableControlState.ts | 54 +++++++++++++------ client/src/app/hooks/usePersistentState.ts | 39 ++++++++++++++ 8 files changed, 134 insertions(+), 16 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts index f0e3f3626f..3e38c3146f 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts @@ -76,6 +76,15 @@ export const useActiveItemState = < persistTo, key: "activeItem", } + : persistTo === "custom" + ? { + persistTo, + key: "filters", + serialize: (val) => + args.customPersistance?.write(JSON.stringify(val)), + deserialize: () => + JSON.parse(args.customPersistance?.read() ?? "{}") ?? {}, + } : { persistTo }), }); return { activeItemId, setActiveItemId }; diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index ac3a873bbf..c01c1b7813 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -111,6 +111,15 @@ export const useExpansionState = < persistTo, key: "expandedCells", } + : persistTo === "custom" + ? { + persistTo, + key: "filters", + serialize: (val) => + args.customPersistance?.write(JSON.stringify(val)), + deserialize: () => + JSON.parse(args.customPersistance?.read() ?? "{}") ?? {}, + } : { persistTo }), }); return { expandedCells, setExpandedCells }; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 2de43e1f7c..9a4f6bc86d 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -104,6 +104,15 @@ export const useFilterState = < } : persistTo === "localStorage" || persistTo === "sessionStorage" ? { persistTo, key: "filters" } + : persistTo === "custom" + ? { + persistTo, + key: "filters", + serialize: (val) => + args.customPersistance?.write(JSON.stringify(val)), + deserialize: () => + JSON.parse(args.customPersistance?.read() ?? "{}") ?? {}, + } : { persistTo }), }); return { filterValues, setFilterValues }; diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 6fd87c84b1..26ca38547b 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -116,6 +116,15 @@ export const usePaginationState = < persistTo, key: "pagination", } + : persistTo === "custom" + ? { + persistTo, + key: "filters", + serialize: (val) => + args.customPersistance?.write(JSON.stringify(val)), + deserialize: () => + JSON.parse(args.customPersistance?.read() ?? "{}") ?? {}, + } : { persistTo }), }); const { pageNumber, itemsPerPage } = paginationState || defaultValue; diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index fc583142e9..7bc3a5f358 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -113,6 +113,15 @@ export const useSortState = < persistTo, key: "sort", } + : persistTo === "custom" + ? { + persistTo, + key: "sort", + serialize: (val) => + args.customPersistance?.write(JSON.stringify(val)), + deserialize: () => + JSON.parse(args.customPersistance?.read() ?? "{}") ?? {}, + } : { persistTo }), }); return { activeSort, setActiveSort }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index d2293c2348..88cb1dd007 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -70,12 +70,14 @@ export type TableFeature = * - "urlParams" (recommended) - URL query parameters. Persists on page reload, browser history buttons (back/forward) or loading a bookmark. Resets on page navigation. * - "localStorage" - Browser localStorage API. Persists semi-permanently and is shared across all tabs/windows. Resets only when the user clears their browsing data. * - "sessionStorage" - Browser sessionStorage API. Persists on page/history navigation/reload. Resets when the tab/window is closed. + * - callback - custom solutions */ export type PersistTarget = | "state" | "urlParams" | "localStorage" - | "sessionStorage"; + | "sessionStorage" + | "custom"; /** * Common persistence-specific args @@ -107,6 +109,10 @@ export type IFeaturePersistenceArgs< * Where to persist state for this feature. */ persistTo?: PersistTarget; + customPersistance?: { + write: (params: string) => void; + read: () => string; + }; }; export interface ColumnSetting { @@ -132,6 +138,10 @@ export type ITablePersistenceArgs< persistTo?: | PersistTarget | Partial>; + customPersistance?: { + write: (value: string, key: TableFeature) => void; + read: (key: TableFeature) => string; + }; }; /** diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 7e9d1ca651..da0e855861 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -1,7 +1,8 @@ import { + IFeaturePersistenceArgs, ITableControlState, + ITablePersistenceArgs, IUseTableControlStateArgs, - PersistTarget, TableFeature, } from "./types"; import { useFilterState } from "./filtering"; @@ -11,6 +12,30 @@ import { useActiveItemState } from "./active-item"; import { useExpansionState } from "./expansion"; import { useColumnState } from "./column/useColumnState"; +const getPersistTo = ({ + feature, + persistTo, + customPersistance, +}: { + feature: TableFeature; + persistTo: ITablePersistenceArgs["persistTo"]; + customPersistance: ITablePersistenceArgs["customPersistance"]; +}): { + persistTo: IFeaturePersistenceArgs["persistTo"]; + customPersistance?: IFeaturePersistenceArgs["customPersistance"]; +} => ({ + persistTo: + !persistTo || typeof persistTo === "string" + ? persistTo + : persistTo[feature] || persistTo.default, + customPersistance: customPersistance + ? { + read: () => customPersistance?.read(feature) ?? "", + write: (val: string) => customPersistance?.write(val, feature), + } + : undefined, +}); + /** * Provides the "source of truth" state for all table features. * - State can be persisted in one or more configurable storage targets, either the same for the entire table or different targets per feature. @@ -41,31 +66,30 @@ export const useTableControlState = < TFilterCategoryKey, TPersistenceKeyPrefix > => { - const getPersistTo = (feature: TableFeature): PersistTarget | undefined => - !args.persistTo || typeof args.persistTo === "string" - ? args.persistTo - : args.persistTo[feature] || args.persistTo.default; - + const { customPersistance, persistTo, ...rest } = args; const filterState = useFilterState< TItem, TFilterCategoryKey, TPersistenceKeyPrefix - >({ ...args, persistTo: getPersistTo("filter") }); + >({ + ...rest, + ...getPersistTo({ feature: "filter", customPersistance, persistTo }), + }); const sortState = useSortState({ - ...args, - persistTo: getPersistTo("sort"), + ...rest, + ...getPersistTo({ feature: "sort", customPersistance, persistTo }), }); const paginationState = usePaginationState({ - ...args, - persistTo: getPersistTo("pagination"), + ...rest, + ...getPersistTo({ customPersistance, persistTo, feature: "pagination" }), }); const expansionState = useExpansionState({ - ...args, - persistTo: getPersistTo("expansion"), + ...rest, + ...getPersistTo({ customPersistance, persistTo, feature: "expansion" }), }); const activeItemState = useActiveItemState({ - ...args, - persistTo: getPersistTo("activeItem"), + ...rest, + ...getPersistTo({ customPersistance, persistTo, feature: "activeItem" }), }); const { columnNames, tableName, initialColumns } = args; diff --git a/client/src/app/hooks/usePersistentState.ts b/client/src/app/hooks/usePersistentState.ts index ccd50c452a..bc8c5a3f5b 100644 --- a/client/src/app/hooks/usePersistentState.ts +++ b/client/src/app/hooks/usePersistentState.ts @@ -9,6 +9,15 @@ import { DisallowCharacters } from "@app/utils/type-utils"; type PersistToStateOptions = { persistTo?: "state" }; +type PersistToCustomHookOptions = { + persistTo: "custom"; + key: string; + defaultValue: TValue; + isEnabled?: boolean; + serialize: (params: TValue) => void; + deserialize: () => TValue; +}; + type PersistToUrlParamsOptions< TValue, TPersistenceKeyPrefix extends string, @@ -33,6 +42,7 @@ export type UsePersistentStateOptions< | PersistToStateOptions | PersistToUrlParamsOptions | PersistToStorageOptions + | PersistToCustomHookOptions ); export const usePersistentState = < @@ -92,7 +102,36 @@ export const usePersistentState = < ? { ...options, key: prefixKey(options.key) } : { ...options, isEnabled: false, key: "" } ), + custom: useCustomHook( + isCustomHookOptions(options) + ? options + : { + key: "", + serialize: () => {}, + deserialize: () => defaultValue, + defaultValue, + isEnabled: false, + persistTo: "custom", + } + ), }; const [value, setValue] = persistence[persistTo || "state"]; return isEnabled ? [value, setValue] : [defaultValue, () => {}]; }; + +const useCustomHook = ({ + serialize, + deserialize, +}: PersistToCustomHookOptions): [TValue, (val: TValue) => void] => { + return [deserialize(), serialize]; +}; + +export const isCustomHookOptions = < + TValue, + TPersistenceKeyPrefix extends string, + TURLParamKey extends string, +>( + o: Partial< + UsePersistentStateOptions + > +): o is PersistToCustomHookOptions => o.persistTo === "custom";