@applica-software-guru/react-admin 1.5.352 → 1.5.353
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/dist/components/ra-lists/List.d.ts.map +1 -1
- package/dist/components/ra-lists/ListTabsToolbar.d.ts.map +1 -1
- package/dist/components/ra-lists/ListToolbar.d.ts +2 -0
- package/dist/components/ra-lists/ListToolbar.d.ts.map +1 -1
- package/dist/components/ra-lists/ListView.d.ts +17 -0
- package/dist/components/ra-lists/ListView.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useLocalStorage.d.ts +26 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -1
- package/dist/hooks/useSessionStorage.d.ts +29 -0
- package/dist/hooks/useSessionStorage.d.ts.map +1 -0
- package/dist/react-admin.cjs.js +61 -61
- package/dist/react-admin.cjs.js.gz +0 -0
- package/dist/react-admin.cjs.js.map +1 -1
- package/dist/react-admin.es.js +10437 -10337
- package/dist/react-admin.es.js.gz +0 -0
- package/dist/react-admin.es.js.map +1 -1
- package/dist/react-admin.umd.js +59 -59
- package/dist/react-admin.umd.js.gz +0 -0
- package/dist/react-admin.umd.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/sessionStorage.d.ts +16 -0
- package/dist/utils/sessionStorage.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/ra-lists/List.tsx +0 -1
- package/src/components/ra-lists/ListTabsToolbar.tsx +42 -9
- package/src/components/ra-lists/ListToolbar.tsx +74 -36
- package/src/components/ra-lists/ListView.tsx +20 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useLocalStorage.tsx +26 -0
- package/src/hooks/useSessionStorage.tsx +74 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/sessionStorage.ts +73 -0
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,QAAQ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type IListener = (value: unknown) => void;
|
|
2
|
+
interface IStorage {
|
|
3
|
+
get: (key: string) => unknown | null;
|
|
4
|
+
set: (key: string, value: unknown) => unknown | null;
|
|
5
|
+
watch: (key: string, callback: IListener) => void;
|
|
6
|
+
unwatch: (key: string, callback?: IListener) => void;
|
|
7
|
+
}
|
|
8
|
+
declare class SessionStorage implements IStorage {
|
|
9
|
+
#private;
|
|
10
|
+
get(key: string): unknown | null;
|
|
11
|
+
set(key: string, value: unknown): unknown | null;
|
|
12
|
+
watch(key: string, callback: IListener): void;
|
|
13
|
+
unwatch(key: string, callback?: IListener | undefined): void;
|
|
14
|
+
}
|
|
15
|
+
export { SessionStorage };
|
|
16
|
+
//# sourceMappingURL=sessionStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionStorage.d.ts","sourceRoot":"","sources":["../../../src/utils/sessionStorage.ts"],"names":[],"mappings":"AAOA,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;AAC1C,UAAU,QAAQ;IAChB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IACrC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,IAAI,CAAC;IACrD,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC;IAClD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC;CACtD;AAED,cAAM,cAAe,YAAW,QAAQ;;IAOtC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IAUhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI;IAUhD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,IAAI;IAa7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI;CAe7D;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -125,7 +125,6 @@ const StyledList = styled(RaList, { slot: 'root' })(({ theme }) => ({
|
|
|
125
125
|
'& .RaList-actions': {
|
|
126
126
|
minHeight: 80,
|
|
127
127
|
alignItems: 'center',
|
|
128
|
-
padding: theme.spacing(2.5),
|
|
129
128
|
borderTopLeftRadius: `${theme.shape.borderRadius}px`,
|
|
130
129
|
borderTopRightRadius: `${theme.shape.borderRadius}px`,
|
|
131
130
|
borderBottomLeftRadius: 0,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Tab, Tabs } from '@mui/material';
|
|
2
|
-
import { useListContext } from 'react-admin';
|
|
3
|
-
import { ReactElement, useCallback, useEffect, useMemo, useRef
|
|
2
|
+
import { useListContext, useResourceContext } from 'react-admin';
|
|
3
|
+
import { ReactElement, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
4
|
import { useListViewContext } from './ListViewProvider';
|
|
5
|
+
import { useSessionStorage } from '@/hooks';
|
|
6
|
+
import { useSearchParams } from 'react-router-dom';
|
|
5
7
|
|
|
6
8
|
type ListTabToolbarConfig = {
|
|
7
9
|
label: string;
|
|
@@ -19,14 +21,20 @@ function ListTabsToolbar(props: ListTabsToolbarProps) {
|
|
|
19
21
|
const { tabs = [] } = props;
|
|
20
22
|
const { setFilters, filterValues } = useListContext();
|
|
21
23
|
const defaultTabIndex = useMemo(() => tabs.findIndex((tab) => tab.default) ?? 0, [tabs]);
|
|
22
|
-
const [currentTab, setCurrentTab] = useState(defaultTabIndex);
|
|
23
|
-
const initialized = useRef(false);
|
|
24
24
|
const { setCurrentTabKey } = useListViewContext();
|
|
25
|
+
const resource = useResourceContext();
|
|
26
|
+
|
|
27
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
25
28
|
|
|
26
29
|
if (tabs.length > 0 && tabs.some((tab) => !tab.key)) {
|
|
27
30
|
throw new Error('ListTabsToolbar: Each tab must have a unique key.');
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
const tabGroupKey = useMemo(() => `${resource.replace(/^entities\//, '')}-tab-group`, [resource]);
|
|
34
|
+
|
|
35
|
+
const [currentTab, setCurrentTab] = useSessionStorage(tabGroupKey, defaultTabIndex);
|
|
36
|
+
const initialized = useRef(false);
|
|
37
|
+
|
|
30
38
|
const buildFilters = useCallback(
|
|
31
39
|
(index: number, debounce: boolean) => {
|
|
32
40
|
if (setCurrentTabKey) {
|
|
@@ -41,25 +49,50 @@ function ListTabsToolbar(props: ListTabsToolbarProps) {
|
|
|
41
49
|
[filterValues, setFilters, setCurrentTabKey, tabs]
|
|
42
50
|
);
|
|
43
51
|
|
|
52
|
+
const updateUrlParam = useCallback(
|
|
53
|
+
(index: number) => {
|
|
54
|
+
const newParams = new URLSearchParams(searchParams.toString());
|
|
55
|
+
newParams.set(tabGroupKey, String(index));
|
|
56
|
+
setSearchParams(newParams, { replace: true });
|
|
57
|
+
},
|
|
58
|
+
[searchParams, setSearchParams, tabGroupKey]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const param = searchParams.get(tabGroupKey);
|
|
63
|
+
if (String(param) !== String(currentTab)) {
|
|
64
|
+
updateUrlParam(Number(currentTab));
|
|
65
|
+
}
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
}, [filterValues]);
|
|
68
|
+
|
|
44
69
|
useEffect(() => {
|
|
45
70
|
if (!initialized.current) {
|
|
46
|
-
|
|
71
|
+
const param = searchParams.get(tabGroupKey);
|
|
72
|
+
const initialIndex = param != null ? Number(param) : Number(currentTab);
|
|
73
|
+
|
|
74
|
+
if (param != null && String(initialIndex) !== String(currentTab)) {
|
|
75
|
+
setCurrentTab(initialIndex);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
buildFilters(initialIndex, false);
|
|
47
79
|
initialized.current = true;
|
|
48
80
|
}
|
|
49
|
-
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, [searchParams, setSearchParams, tabGroupKey, currentTab, setCurrentTab, buildFilters]);
|
|
50
83
|
|
|
51
84
|
const handleChange = useCallback(
|
|
52
85
|
(_: any, newIndex: number) => {
|
|
53
86
|
setCurrentTab(newIndex);
|
|
54
87
|
buildFilters(newIndex, true);
|
|
55
88
|
},
|
|
56
|
-
[buildFilters]
|
|
89
|
+
[buildFilters, setCurrentTab]
|
|
57
90
|
);
|
|
58
91
|
|
|
59
92
|
return tabs.length > 0 ? (
|
|
60
|
-
<Tabs value={currentTab} onChange={handleChange}>
|
|
93
|
+
<Tabs value={Number(currentTab)} onChange={handleChange}>
|
|
61
94
|
{tabs.map((tab) => (
|
|
62
|
-
<Tab icon={tab.icon} key={tab.
|
|
95
|
+
<Tab icon={tab.icon} key={tab.key} label={tab.label} />
|
|
63
96
|
))}
|
|
64
97
|
</Tabs>
|
|
65
98
|
) : null;
|
|
@@ -6,62 +6,99 @@ import { Exporter, useListContext } from 'ra-core';
|
|
|
6
6
|
import { FilterContext, FilterForm } from 'react-admin';
|
|
7
7
|
import { ActiveFiltersChips, FilterSidebarContext } from './FilterSidebar';
|
|
8
8
|
import { isEmpty } from 'lodash';
|
|
9
|
+
import { ListTabToolbarConfig, ListTabsToolbar } from './ListTabsToolbar';
|
|
9
10
|
|
|
10
11
|
interface ListToolbarPropsExtended extends ListToolbarProps {
|
|
11
12
|
hasFilterSidebar?: boolean;
|
|
13
|
+
tabs?: ListTabToolbarConfig[];
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
function ListToolbarComp(props: ListToolbarPropsExtended): ReactElement | null {
|
|
15
|
-
const { filters, actions, className, hasFilterSidebar, ...rest } = props;
|
|
17
|
+
const { filters, actions, className, hasFilterSidebar, tabs, ...rest } = props;
|
|
16
18
|
const theme = useTheme() as any;
|
|
17
19
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
|
18
20
|
const Actions = actions ? React.cloneElement(actions, { ...rest, ...actions.props }) : null;
|
|
19
21
|
const { filterValues, data, isLoading } = useListContext();
|
|
20
22
|
const shouldRenderEmptyList = !isLoading && data && data.length === 0;
|
|
21
23
|
|
|
24
|
+
const hasTabs = tabs && tabs.length > 0;
|
|
25
|
+
|
|
22
26
|
return Array.isArray(filters) ? (
|
|
23
27
|
<FilterSidebarContext.Provider value={{ hasFilterSidebar }}>
|
|
24
28
|
<FilterContext.Provider value={filters}>
|
|
25
|
-
{
|
|
26
|
-
<Box
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
<Root className={className}>
|
|
30
|
+
<Box sx={{ display: 'flex', flex: 1, flexDirection: 'column', gap: 2 }}>
|
|
31
|
+
{tabs ? <ListTabsToolbar tabs={tabs} /> : null}
|
|
32
|
+
<Box
|
|
33
|
+
sx={{
|
|
34
|
+
display: 'flex',
|
|
35
|
+
flexDirection: 'row',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
gap: 2,
|
|
38
|
+
p: hasTabs ? 0 : theme.spacing(2.5),
|
|
39
|
+
px: theme.spacing(2.5),
|
|
40
|
+
pb: theme.spacing(2.5)
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{isMobile && hasFilterSidebar ? (
|
|
44
|
+
<Box
|
|
45
|
+
sx={{
|
|
46
|
+
border: shouldRenderEmptyList ? '1px solid' : 0,
|
|
47
|
+
borderColor: theme.palette.mode === 'dark' ? theme.palette.divider : theme.palette.grey.A800,
|
|
48
|
+
width: '100%'
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{Actions}
|
|
52
|
+
{!isEmpty(filterValues) && (
|
|
53
|
+
<>
|
|
54
|
+
<Divider sx={{ my: 1 }} />
|
|
55
|
+
<ActiveFiltersChips />
|
|
56
|
+
</>
|
|
57
|
+
)}
|
|
58
|
+
</Box>
|
|
59
|
+
) : (
|
|
60
|
+
<>
|
|
61
|
+
{hasFilterSidebar ? <ActiveFiltersChips /> : <FilterForm />}
|
|
62
|
+
<Box sx={{ flex: 1 }} />
|
|
63
|
+
{Actions}
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
39
67
|
</Box>
|
|
40
|
-
|
|
41
|
-
<Root className={className}>
|
|
42
|
-
{hasFilterSidebar ? <ActiveFiltersChips /> : <FilterForm />}
|
|
43
|
-
<span />
|
|
44
|
-
{Actions}
|
|
45
|
-
</Root>
|
|
46
|
-
)}
|
|
68
|
+
</Root>
|
|
47
69
|
</FilterContext.Provider>
|
|
48
70
|
</FilterSidebarContext.Provider>
|
|
49
71
|
) : (
|
|
50
72
|
<Root className={className}>
|
|
51
|
-
{
|
|
52
|
-
?
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
<Box sx={{ display: 'flex', flex: 1, flexDirection: 'column', gap: 2 }}>
|
|
74
|
+
{tabs ? <ListTabsToolbar tabs={tabs} /> : null}
|
|
75
|
+
<Box
|
|
76
|
+
sx={{
|
|
77
|
+
display: 'flex',
|
|
78
|
+
flexDirection: 'row',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
gap: 2,
|
|
81
|
+
p: hasTabs ? 0 : theme.spacing(2.5),
|
|
82
|
+
px: theme.spacing(2.5),
|
|
83
|
+
pb: theme.spacing(2.5)
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{filters
|
|
87
|
+
? React.cloneElement(filters, {
|
|
88
|
+
...rest,
|
|
89
|
+
context: 'form'
|
|
90
|
+
})
|
|
91
|
+
: null}
|
|
92
|
+
<span />
|
|
93
|
+
{actions
|
|
94
|
+
? React.cloneElement(actions, {
|
|
95
|
+
...rest,
|
|
96
|
+
filters,
|
|
97
|
+
...actions.props
|
|
98
|
+
})
|
|
99
|
+
: null}
|
|
100
|
+
</Box>
|
|
101
|
+
</Box>
|
|
65
102
|
</Root>
|
|
66
103
|
);
|
|
67
104
|
}
|
|
@@ -92,6 +129,7 @@ const Root = styled('div', {
|
|
|
92
129
|
justifyContent: 'space-between',
|
|
93
130
|
alignItems: 'flex-end',
|
|
94
131
|
width: '100%',
|
|
132
|
+
padding: 0,
|
|
95
133
|
// [theme.breakpoints.down('md')]: {
|
|
96
134
|
// flexWrap: 'wrap'
|
|
97
135
|
// },
|
|
@@ -17,7 +17,7 @@ import { ListToolbar } from './ListToolbar';
|
|
|
17
17
|
import { FilterSidebar } from './FilterSidebar';
|
|
18
18
|
import { MainCard } from '@/components/MainCard';
|
|
19
19
|
import { Empty } from '@/components/ra-lists/Empty';
|
|
20
|
-
import { type ListTabToolbarConfig
|
|
20
|
+
import { type ListTabToolbarConfig } from './ListTabsToolbar';
|
|
21
21
|
|
|
22
22
|
const defaultActions = <DefaultActions />;
|
|
23
23
|
const defaultPagination = <DefaultPagination />;
|
|
@@ -65,7 +65,6 @@ function ListView<RecordType extends RaRecord = any>(props: ListViewProps): Reac
|
|
|
65
65
|
backgroundColor: hasAsideSidebar ? theme.palette.background.paper : 'transparent'
|
|
66
66
|
}}
|
|
67
67
|
>
|
|
68
|
-
{tabs ? <ListTabsToolbar tabs={tabs} /> : null}
|
|
69
68
|
{filters || actions ? (
|
|
70
69
|
<ListToolbar
|
|
71
70
|
className={ListClasses.actions}
|
|
@@ -73,6 +72,7 @@ function ListView<RecordType extends RaRecord = any>(props: ListViewProps): Reac
|
|
|
73
72
|
actions={actions}
|
|
74
73
|
hasCreate={hasCreate}
|
|
75
74
|
hasFilterSidebar={hasFilterSidebar}
|
|
75
|
+
tabs={tabs}
|
|
76
76
|
/>
|
|
77
77
|
) : null}
|
|
78
78
|
{!error && (
|
|
@@ -219,6 +219,24 @@ interface ListViewProps {
|
|
|
219
219
|
*/
|
|
220
220
|
tabs?: ListTabToolbarConfig[];
|
|
221
221
|
|
|
222
|
+
/**
|
|
223
|
+
* A unique key to identify the tab group. Used to persist the selected tab in session storage.
|
|
224
|
+
* Must be stable across renders and unique among different List components.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* import { List, ListTabToolbarConfig } from 'react-admin';
|
|
228
|
+
* const tabs: ListTabToolbarConfig[] = [
|
|
229
|
+
* { label: 'Published', key: 'published', icon: <IconPublished />, filter: { status: 'published' }, default: true },
|
|
230
|
+
* { label: 'Drafts', key: 'drafts', icon: <IconDrafts />, filter: { status: 'draft' } },
|
|
231
|
+
* ];
|
|
232
|
+
* const PostList = () => (
|
|
233
|
+
* <List tabs={tabs} tabGroupKey="post-list-tabs">
|
|
234
|
+
* ...
|
|
235
|
+
* </List>
|
|
236
|
+
* );
|
|
237
|
+
*/
|
|
238
|
+
tabGroupKey?: string;
|
|
239
|
+
|
|
222
240
|
/**
|
|
223
241
|
* The actions to display in the toolbar. defaults to Filter + Create + Export.
|
|
224
242
|
*
|
package/src/hooks/index.ts
CHANGED
|
@@ -16,6 +16,32 @@ function buildSetter(key: string, currentValue: unknown, callback: ISetter): ISe
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* React hook for synchronizing a state value with browser LocalStorage.
|
|
21
|
+
*
|
|
22
|
+
* This hook provides a stateful value and a setter function, similar to `useState`,
|
|
23
|
+
* but persists the value in LocalStorage under the specified key. The value is
|
|
24
|
+
* automatically updated when LocalStorage changes (including from other tabs/windows).
|
|
25
|
+
*
|
|
26
|
+
* Use this hook only when you need to persist state across sessions or tabs.
|
|
27
|
+
* If you only need to share state within a single session, consider using `useSessionStorage`
|
|
28
|
+
*
|
|
29
|
+
* Example:
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const [value, setValue] = useLocalStorage('myKey', 'defaultValue');
|
|
32
|
+
*
|
|
33
|
+
* // Read the value
|
|
34
|
+
* console.log(value);
|
|
35
|
+
*
|
|
36
|
+
* // Update the value (persists to LocalStorage)
|
|
37
|
+
* setValue('newValue');
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @param key - The LocalStorage key to store the value under. Must be a non-empty string.
|
|
41
|
+
* @param defaultValue - The default value to use if the key does not exist in LocalStorage.
|
|
42
|
+
* @returns A tuple containing the current value and a setter function to update it.
|
|
43
|
+
*
|
|
44
|
+
*/
|
|
19
45
|
function useLocalStorage(key: string, defaultValue?: unknown): [unknown, (newValue: unknown) => unknown] {
|
|
20
46
|
yup.string().required().validateSync(key);
|
|
21
47
|
const [state, setState] = useState(localStorageInstance.get(key) ?? defaultValue);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { SessionStorage } from '@/utils';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import * as yup from 'yup';
|
|
5
|
+
|
|
6
|
+
const sessionStorageInstance = new SessionStorage();
|
|
7
|
+
|
|
8
|
+
type ISetter = (newValue: unknown) => unknown;
|
|
9
|
+
|
|
10
|
+
function buildSetter(key: string, currentValue: unknown, callback: ISetter): ISetter {
|
|
11
|
+
return function (newValue: unknown) {
|
|
12
|
+
const updatedValue = _.isFunction(newValue) ? newValue(currentValue) : newValue;
|
|
13
|
+
sessionStorageInstance.set(key, updatedValue);
|
|
14
|
+
callback(updatedValue);
|
|
15
|
+
return updatedValue;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* React hook for synchronizing a state value with browser SessionStorage.
|
|
21
|
+
*
|
|
22
|
+
* This hook provides a stateful value and a setter function, similar to `useState`,
|
|
23
|
+
* but persists the value in SessionStorage under the specified key across the same session.
|
|
24
|
+
*
|
|
25
|
+
* Use this hook only when you need to persist state across tabs within the same session.
|
|
26
|
+
* If you need to persist state across sessions, consider using `useLocalStorage`
|
|
27
|
+
* instead.
|
|
28
|
+
*
|
|
29
|
+
* Example:
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const [value, setValue] = useSessionStorage('myKey', 'defaultValue');
|
|
32
|
+
*
|
|
33
|
+
* // Read the value
|
|
34
|
+
* console.log(value);
|
|
35
|
+
*
|
|
36
|
+
* // Update the value (persists to SessionStorage)
|
|
37
|
+
* setValue('newValue');
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @param key - The SessionStorage key to store the value under. Must be a non-empty string.
|
|
41
|
+
* @param defaultValue - The default value to use if the key does not exist in SessionStorage.
|
|
42
|
+
* @returns A tuple containing the current value and a setter function to update it.
|
|
43
|
+
*
|
|
44
|
+
*/
|
|
45
|
+
function useSessionStorage(key: string, defaultValue?: unknown): [unknown, (newValue: unknown) => unknown] {
|
|
46
|
+
yup.string().required().validateSync(key);
|
|
47
|
+
const [state, setState] = useState(sessionStorageInstance.get(key) ?? defaultValue);
|
|
48
|
+
const defaultValueRef = useRef<unknown | undefined>(defaultValue);
|
|
49
|
+
const storedDefaultValue = _.isEqual(defaultValueRef.current, defaultValue) ? defaultValueRef.current : defaultValue;
|
|
50
|
+
const setterRef = useRef<{ key: string; value: unknown; setter: ISetter } | undefined>();
|
|
51
|
+
const storedSetter = setterRef.current;
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setState(sessionStorageInstance.get(key) ?? storedDefaultValue);
|
|
55
|
+
}, [key, storedDefaultValue, setState]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
function listener(value: unknown) {
|
|
59
|
+
setState(value);
|
|
60
|
+
}
|
|
61
|
+
sessionStorageInstance.watch(key, listener);
|
|
62
|
+
return () => {
|
|
63
|
+
sessionStorageInstance.unwatch(key, listener);
|
|
64
|
+
};
|
|
65
|
+
}, [key, setState]);
|
|
66
|
+
|
|
67
|
+
if (key !== storedSetter?.key || !_.isEqual(state, storedSetter?.value)) {
|
|
68
|
+
setterRef.current = { key, value: state, setter: buildSetter(key, state, setState) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [state, setterRef.current?.setter ?? _.noop];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { useSessionStorage };
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import * as yup from 'yup';
|
|
3
|
+
|
|
4
|
+
const listenerSchema = yup.mixed().test('function', 'Invalid function', (value) => {
|
|
5
|
+
return _.isFunction(value);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
type IListener = (value: unknown) => void;
|
|
9
|
+
interface IStorage {
|
|
10
|
+
get: (key: string) => unknown | null;
|
|
11
|
+
set: (key: string, value: unknown) => unknown | null;
|
|
12
|
+
watch: (key: string, callback: IListener) => void;
|
|
13
|
+
unwatch: (key: string, callback?: IListener) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class SessionStorage implements IStorage {
|
|
17
|
+
#listeners: Array<{ key: string; callback: IListener }> = [];
|
|
18
|
+
|
|
19
|
+
#canAccessSessionStorage(): boolean {
|
|
20
|
+
return typeof window !== 'undefined';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(key: string): unknown | null {
|
|
24
|
+
yup.string().required().validateSync(key);
|
|
25
|
+
if (!this.#canAccessSessionStorage()) {
|
|
26
|
+
return null;
|
|
27
|
+
} else {
|
|
28
|
+
const value = sessionStorage.getItem(key);
|
|
29
|
+
return _.isNil(value) ? null : JSON.parse(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set(key: string, value: unknown): unknown | null {
|
|
34
|
+
yup.string().required().validateSync(key);
|
|
35
|
+
if (this.#canAccessSessionStorage()) {
|
|
36
|
+
sessionStorage.setItem(key, JSON.stringify(value));
|
|
37
|
+
return value;
|
|
38
|
+
} else {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
watch(key: string, callback: IListener): void {
|
|
44
|
+
yup.string().required().validateSync(key);
|
|
45
|
+
listenerSchema.validateSync(callback);
|
|
46
|
+
if (this.#canAccessSessionStorage()) {
|
|
47
|
+
this.#listeners.push({ key: key, callback: callback });
|
|
48
|
+
window.addEventListener('storage', (e) => {
|
|
49
|
+
if (e.storageArea === sessionStorage && e.key === key) {
|
|
50
|
+
callback(this.get(key));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
unwatch(key: string, callback?: IListener | undefined): void {
|
|
57
|
+
yup.string().required().validateSync(key);
|
|
58
|
+
listenerSchema.optional().default(_.identity).validateSync(callback);
|
|
59
|
+
if (this.#canAccessSessionStorage()) {
|
|
60
|
+
if (callback !== undefined) {
|
|
61
|
+
this.#listeners = _.reject(this.#listeners, (listener) => {
|
|
62
|
+
return listener.key === key && listener.callback === callback;
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
this.#listeners = _.reject(this.#listeners, (listener) => {
|
|
66
|
+
return listener.key === key;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { SessionStorage };
|