@coveord/plasma-mantine 58.0.1 → 59.0.0
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/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +100 -103
- package/dist/.tsbuildinfo +1 -1
- package/dist/cjs/components/Badge/Badge.d.ts +2 -2
- package/dist/cjs/components/Badge/Badge.d.ts.map +1 -1
- package/dist/cjs/components/Badge/Badge.js.map +1 -1
- package/dist/cjs/components/Header/HeaderRight/HeaderRight.js +1 -1
- package/dist/cjs/components/Header/HeaderRight/HeaderRight.js.map +1 -1
- package/dist/cjs/components/LastUpdated/LastUpdated.d.ts.map +1 -1
- package/dist/cjs/components/LastUpdated/LastUpdated.js +0 -1
- package/dist/cjs/components/LastUpdated/LastUpdated.js.map +1 -1
- package/dist/cjs/components/Modal/ModalFooter.module.css +1 -0
- package/dist/cjs/components/Prompt/Prompt.d.ts +3 -3
- package/dist/cjs/components/Prompt/Prompt.d.ts.map +1 -1
- package/dist/cjs/components/Prompt/Prompt.js +6 -6
- package/dist/cjs/components/Prompt/Prompt.js.map +1 -1
- package/dist/cjs/components/Table/use-persisted-column-visibility.d.ts +16 -0
- package/dist/cjs/components/Table/use-persisted-column-visibility.d.ts.map +1 -0
- package/dist/cjs/components/Table/use-persisted-column-visibility.js +123 -0
- package/dist/cjs/components/Table/use-persisted-column-visibility.js.map +1 -0
- package/dist/cjs/components/Table/use-table.d.ts +12 -0
- package/dist/cjs/components/Table/use-table.d.ts.map +1 -1
- package/dist/cjs/components/Table/use-table.js +22 -7
- package/dist/cjs/components/Table/use-table.js.map +1 -1
- package/dist/cjs/styles/Modal.module.css +13 -0
- package/dist/cjs/utils/local-storage.d.ts +38 -0
- package/dist/cjs/utils/local-storage.d.ts.map +1 -0
- package/dist/cjs/utils/local-storage.js +175 -0
- package/dist/cjs/utils/local-storage.js.map +1 -0
- package/dist/esm/components/Badge/Badge.d.ts +2 -2
- package/dist/esm/components/Badge/Badge.d.ts.map +1 -1
- package/dist/esm/components/Badge/Badge.js.map +1 -1
- package/dist/esm/components/Header/HeaderRight/HeaderRight.js +1 -1
- package/dist/esm/components/Header/HeaderRight/HeaderRight.js.map +1 -1
- package/dist/esm/components/LastUpdated/LastUpdated.d.ts.map +1 -1
- package/dist/esm/components/LastUpdated/LastUpdated.js +0 -1
- package/dist/esm/components/LastUpdated/LastUpdated.js.map +1 -1
- package/dist/esm/components/Modal/ModalFooter.module.css +1 -0
- package/dist/esm/components/Prompt/Prompt.d.ts +3 -3
- package/dist/esm/components/Prompt/Prompt.d.ts.map +1 -1
- package/dist/esm/components/Prompt/Prompt.js +7 -8
- package/dist/esm/components/Prompt/Prompt.js.map +1 -1
- package/dist/esm/components/Table/use-persisted-column-visibility.d.ts +16 -0
- package/dist/esm/components/Table/use-persisted-column-visibility.d.ts.map +1 -0
- package/dist/esm/components/Table/use-persisted-column-visibility.js +85 -0
- package/dist/esm/components/Table/use-persisted-column-visibility.js.map +1 -0
- package/dist/esm/components/Table/use-table.d.ts +12 -0
- package/dist/esm/components/Table/use-table.d.ts.map +1 -1
- package/dist/esm/components/Table/use-table.js +15 -3
- package/dist/esm/components/Table/use-table.js.map +1 -1
- package/dist/esm/styles/Modal.module.css +13 -0
- package/dist/esm/utils/local-storage.d.ts +38 -0
- package/dist/esm/utils/local-storage.d.ts.map +1 -0
- package/dist/esm/utils/local-storage.js +134 -0
- package/dist/esm/utils/local-storage.js.map +1 -0
- package/package.json +5 -5
- package/src/components/Badge/Badge.tsx +30 -27
- package/src/components/Header/HeaderRight/HeaderRight.tsx +1 -1
- package/src/components/LastUpdated/LastUpdated.tsx +34 -36
- package/src/components/Modal/ModalFooter.module.css +1 -0
- package/src/components/Prompt/Prompt.tsx +14 -6
- package/src/components/Table/__tests__/use-persisted-column-visibility.spec.ts +203 -0
- package/src/components/Table/use-persisted-column-visibility.ts +79 -0
- package/src/components/Table/use-table.ts +36 -3
- package/src/styles/Modal.module.css +13 -0
- package/src/utils/__tests__/local-storage.spec.ts +176 -0
- package/src/utils/local-storage.ts +151 -0
|
@@ -4,41 +4,44 @@ import {
|
|
|
4
4
|
BadgeProps,
|
|
5
5
|
BadgeStylesNames,
|
|
6
6
|
BadgeVariant,
|
|
7
|
+
ElementProps,
|
|
7
8
|
Badge as MantineBadge,
|
|
9
|
+
PolymorphicComponentProps,
|
|
8
10
|
polymorphicFactory,
|
|
9
11
|
PolymorphicFactory,
|
|
10
|
-
PolymorphicComponentProps,
|
|
11
12
|
useComputedColorScheme,
|
|
12
13
|
} from '@mantine/core';
|
|
13
14
|
import {forwardRef, ForwardRefExoticComponent, ReactElement, ReactNode, RefAttributes} from 'react';
|
|
14
15
|
|
|
15
16
|
export interface SemanticBadgeProps
|
|
16
|
-
extends
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
17
|
+
extends
|
|
18
|
+
Pick<
|
|
19
|
+
BadgeProps,
|
|
20
|
+
| 'm'
|
|
21
|
+
| 'mt'
|
|
22
|
+
| 'mb'
|
|
23
|
+
| 'ml'
|
|
24
|
+
| 'mr'
|
|
25
|
+
| 'ms'
|
|
26
|
+
| 'me'
|
|
27
|
+
| 'mx'
|
|
28
|
+
| 'my'
|
|
29
|
+
| 'miw'
|
|
30
|
+
| 'maw'
|
|
31
|
+
| 'pos'
|
|
32
|
+
| 'top'
|
|
33
|
+
| 'left'
|
|
34
|
+
| 'right'
|
|
35
|
+
| 'bottom'
|
|
36
|
+
| 'inset'
|
|
37
|
+
| 'display'
|
|
38
|
+
| 'flex'
|
|
39
|
+
| 'leftSection'
|
|
40
|
+
| 'rightSection'
|
|
41
|
+
| 'fullWidth'
|
|
42
|
+
| 'circle'
|
|
43
|
+
>,
|
|
44
|
+
ElementProps<'div'> {
|
|
42
45
|
/**
|
|
43
46
|
* The size of the badge.
|
|
44
47
|
* @default 'small'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {BoxProps, factory, Factory, Group, GroupProps, StylesApiProps, Text, useProps, useStyles} from '@mantine/core';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
|
-
import
|
|
3
|
+
import {ForwardedRef} from 'react';
|
|
4
4
|
|
|
5
5
|
export type LastUpdatedStylesNames = 'root' | 'label';
|
|
6
6
|
|
|
@@ -35,43 +35,41 @@ const defaultProps: Partial<LastUpdatedProps> = {
|
|
|
35
35
|
formatter: (time) => dayjs(time).format('h:mm:ss A'),
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
export const LastUpdated = factory<LastUpdatedFactory>(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
38
|
+
export const LastUpdated = factory<LastUpdatedFactory>((props: LastUpdatedProps, ref: ForwardedRef<HTMLDivElement>) => {
|
|
39
|
+
const {formatter, label, time, classNames, className, styles, style, vars, unstyled, ...others} = useProps(
|
|
40
|
+
'PlasmaLastUpdated',
|
|
41
|
+
defaultProps as Partial<LastUpdatedProps>,
|
|
42
|
+
props,
|
|
43
|
+
);
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
const resolvedTime = time ?? dayjs().valueOf();
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
const getStyles = useStyles<LastUpdatedFactory>({
|
|
48
|
+
name: 'LastUpdated',
|
|
49
|
+
classes: {},
|
|
50
|
+
props,
|
|
51
|
+
className,
|
|
52
|
+
style,
|
|
53
|
+
classNames,
|
|
54
|
+
styles,
|
|
55
|
+
unstyled,
|
|
56
|
+
vars,
|
|
57
|
+
});
|
|
58
|
+
const stylesApiProps = {classNames, styles};
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
);
|
|
60
|
+
return (
|
|
61
|
+
<Group
|
|
62
|
+
justify={props.justify}
|
|
63
|
+
ref={ref}
|
|
64
|
+
{...getStyles('root', {className, style, ...stylesApiProps})}
|
|
65
|
+
{...others}
|
|
66
|
+
>
|
|
67
|
+
<Text size="xs" {...getStyles('label', {className, style, ...stylesApiProps})}>
|
|
68
|
+
{label}
|
|
69
|
+
<span role="timer">{formatter(resolvedTime)}</span>
|
|
70
|
+
</Text>
|
|
71
|
+
</Group>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
76
74
|
|
|
77
75
|
LastUpdated.displayName = 'LastUpdated';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Box,
|
|
3
2
|
factory,
|
|
4
3
|
Factory,
|
|
5
4
|
ModalRootProps,
|
|
@@ -9,7 +8,16 @@ import {
|
|
|
9
8
|
useProps,
|
|
10
9
|
useStyles,
|
|
11
10
|
} from '@mantine/core';
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Children,
|
|
13
|
+
ComponentProps,
|
|
14
|
+
ComponentType,
|
|
15
|
+
forwardRef,
|
|
16
|
+
ForwardRefExoticComponent,
|
|
17
|
+
ReactElement,
|
|
18
|
+
ReactNode,
|
|
19
|
+
RefAttributes,
|
|
20
|
+
} from 'react';
|
|
13
21
|
import {InfoToken} from '../InfoToken/InfoToken.js';
|
|
14
22
|
import {Modal} from '../Modal/Modal.js';
|
|
15
23
|
import {PromptContextProvider} from './Prompt.context.js';
|
|
@@ -114,9 +122,9 @@ const _Prompt = factory<PromptFactory>((_props, ref) => {
|
|
|
114
122
|
<Modal.CloseButton {...getStyles('close', stylesApiProps)} />
|
|
115
123
|
</Modal.Header>
|
|
116
124
|
<Modal.Body {...getStyles('body', stylesApiProps)}>
|
|
117
|
-
|
|
125
|
+
{otherChildren}
|
|
126
|
+
{footers}
|
|
118
127
|
</Modal.Body>
|
|
119
|
-
{footers}
|
|
120
128
|
</Modal.Content>
|
|
121
129
|
</Modal.Root>
|
|
122
130
|
</PromptContextProvider>
|
|
@@ -124,12 +132,12 @@ const _Prompt = factory<PromptFactory>((_props, ref) => {
|
|
|
124
132
|
});
|
|
125
133
|
_Prompt.displayName = 'Prompt';
|
|
126
134
|
|
|
127
|
-
type PromptCompoundComponent =
|
|
135
|
+
type PromptCompoundComponent = ForwardRefExoticComponent<PromptProps & RefAttributes<HTMLDivElement>>;
|
|
128
136
|
|
|
129
137
|
const PromptFooter: ComponentType<ComponentProps<typeof Modal.Footer>> = (props) => <Modal.Footer {...props} />;
|
|
130
138
|
PromptFooter.displayName = 'Prompt.Footer';
|
|
131
139
|
|
|
132
|
-
const createPromptCompound = (variant: PromptVariant, displayName: string)
|
|
140
|
+
const createPromptCompound = (variant: PromptVariant, displayName: string) => {
|
|
133
141
|
const Component = forwardRef<HTMLDivElement, PromptProps>((props, ref) => (
|
|
134
142
|
<_Prompt ref={ref} {...props} variant={variant} />
|
|
135
143
|
));
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import {act, renderHook} from '@test-utils';
|
|
2
|
+
import {CURRENT_STORAGE_VERSION, STORAGE_KEY} from '../../../utils/local-storage.js';
|
|
3
|
+
import {usePersistedColumnVisibility} from '../use-persisted-column-visibility.js';
|
|
4
|
+
import {useTable} from '../use-table.js';
|
|
5
|
+
|
|
6
|
+
const setStoredVisibility = (tableId: string, value: unknown) => {
|
|
7
|
+
const existing = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null') ?? {
|
|
8
|
+
'storage-version': CURRENT_STORAGE_VERSION,
|
|
9
|
+
storage: {},
|
|
10
|
+
};
|
|
11
|
+
if (!existing.storage.table) {
|
|
12
|
+
existing.storage.table = {};
|
|
13
|
+
}
|
|
14
|
+
if (!existing.storage.table[tableId]) {
|
|
15
|
+
existing.storage.table[tableId] = {};
|
|
16
|
+
}
|
|
17
|
+
existing.storage.table[tableId].columnVisibility = value;
|
|
18
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(existing));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getStoredVisibility = (tableId: string): unknown => {
|
|
22
|
+
const data = JSON.parse(localStorage.getItem(STORAGE_KEY)!);
|
|
23
|
+
return data?.storage?.table?.[tableId]?.columnVisibility ?? null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('usePersistedColumnVisibility', () => {
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
localStorage.clear();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('without tableId', () => {
|
|
32
|
+
it('returns the default visibility and does not persist', () => {
|
|
33
|
+
const defaults = {col1: true, col2: false};
|
|
34
|
+
const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity));
|
|
35
|
+
|
|
36
|
+
expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false});
|
|
37
|
+
|
|
38
|
+
act(() => {
|
|
39
|
+
result.current.persistColumnVisibility({col1: false});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(localStorage.length).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('with tableId', () => {
|
|
47
|
+
const tableId = 'test-table';
|
|
48
|
+
|
|
49
|
+
it('returns the default visibility when nothing is stored', () => {
|
|
50
|
+
const defaults = {col1: true, col2: true, col3: false};
|
|
51
|
+
const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
|
|
52
|
+
|
|
53
|
+
expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: true, col3: false});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('merges stored visibility with defaults', () => {
|
|
57
|
+
setStoredVisibility(tableId, {col2: false});
|
|
58
|
+
const defaults = {col1: true, col2: true, col3: false};
|
|
59
|
+
|
|
60
|
+
const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
|
|
61
|
+
|
|
62
|
+
expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false, col3: false});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('ignores stored keys not present in defaults and non-boolean values', () => {
|
|
66
|
+
setStoredVisibility(tableId, {unknown: true, col1: 'yes', col2: false});
|
|
67
|
+
const defaults = {col1: true, col2: true};
|
|
68
|
+
|
|
69
|
+
const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
|
|
70
|
+
|
|
71
|
+
expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('caps visible columns to maxSelectableColumns from both defaults and storage', () => {
|
|
75
|
+
setStoredVisibility(tableId, {col1: true, col2: true, col3: true});
|
|
76
|
+
const defaults = {col1: false, col2: false, col3: false};
|
|
77
|
+
|
|
78
|
+
const {result} = renderHook(() => usePersistedColumnVisibility(defaults, 2, tableId));
|
|
79
|
+
|
|
80
|
+
const visibleCount = Object.values(result.current.initialColumnVisibility).filter(Boolean).length;
|
|
81
|
+
expect(visibleCount).toBe(2);
|
|
82
|
+
expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: true, col3: false});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('persists visibility to localStorage', () => {
|
|
86
|
+
const defaults = {col1: true, col2: true};
|
|
87
|
+
const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
|
|
88
|
+
|
|
89
|
+
act(() => {
|
|
90
|
+
result.current.persistColumnVisibility({col1: false, col2: true});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(getStoredVisibility(tableId)).toEqual({col1: false, col2: true});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('clears invalid stored data and falls back to defaults', () => {
|
|
97
|
+
localStorage.setItem(STORAGE_KEY, 'not-valid-json{{{');
|
|
98
|
+
const defaults = {col1: true, col2: false};
|
|
99
|
+
|
|
100
|
+
const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
|
|
101
|
+
|
|
102
|
+
expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false});
|
|
103
|
+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it.each([
|
|
107
|
+
['an array', [true, false]],
|
|
108
|
+
['null', null],
|
|
109
|
+
['an empty object', {}],
|
|
110
|
+
])('falls back to defaults when stored value is %s', (_, storedValue) => {
|
|
111
|
+
setStoredVisibility(tableId, storedValue);
|
|
112
|
+
const defaults = {col1: true, col2: false};
|
|
113
|
+
|
|
114
|
+
const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
|
|
115
|
+
|
|
116
|
+
expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('skips localStorage when defaultVisibleColumns is empty', () => {
|
|
120
|
+
setStoredVisibility(tableId, {col1: true});
|
|
121
|
+
|
|
122
|
+
const {result} = renderHook(() => usePersistedColumnVisibility({}, Infinity, tableId));
|
|
123
|
+
|
|
124
|
+
expect(result.current.initialColumnVisibility).toEqual({});
|
|
125
|
+
|
|
126
|
+
act(() => {
|
|
127
|
+
result.current.persistColumnVisibility({col1: false});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(getStoredVisibility(tableId)).toEqual({col1: true});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('useTable column visibility persistence', () => {
|
|
136
|
+
const tableId = 'integration-table';
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
localStorage.clear();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('initializes column visibility from localStorage when tableId is provided', () => {
|
|
143
|
+
setStoredVisibility(tableId, {col1: false, col2: true});
|
|
144
|
+
|
|
145
|
+
const {result} = renderHook(() =>
|
|
146
|
+
useTable({
|
|
147
|
+
tableId,
|
|
148
|
+
initialState: {columnVisibility: {col1: true, col2: false}},
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(result.current.state.columnVisibility).toEqual({col1: false, col2: true});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('does not read from or write to localStorage when no tableId is provided', () => {
|
|
156
|
+
const {result} = renderHook(() =>
|
|
157
|
+
useTable({
|
|
158
|
+
initialState: {columnVisibility: {col1: true, col2: true}},
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(result.current.state.columnVisibility).toEqual({col1: true, col2: true});
|
|
163
|
+
|
|
164
|
+
act(() => {
|
|
165
|
+
result.current.setColumnVisibility({col1: false});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result.current.state.columnVisibility).toEqual({col1: false});
|
|
169
|
+
expect(localStorage.length).toBe(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('persists to localStorage when setColumnVisibility is called with a value', () => {
|
|
173
|
+
const {result} = renderHook(() =>
|
|
174
|
+
useTable({
|
|
175
|
+
tableId,
|
|
176
|
+
initialState: {columnVisibility: {col1: true, col2: true}},
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
act(() => {
|
|
181
|
+
result.current.setColumnVisibility({col1: false, col2: true});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(result.current.state.columnVisibility).toEqual({col1: false, col2: true});
|
|
185
|
+
expect(getStoredVisibility(tableId)).toEqual({col1: false, col2: true});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('persists to localStorage when setColumnVisibility is called with an updater function', () => {
|
|
189
|
+
const {result} = renderHook(() =>
|
|
190
|
+
useTable({
|
|
191
|
+
tableId,
|
|
192
|
+
initialState: {columnVisibility: {col1: true, col2: false}},
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
act(() => {
|
|
197
|
+
result.current.setColumnVisibility((prev) => ({...prev, col2: true}));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(result.current.state.columnVisibility).toEqual({col1: true, col2: true});
|
|
201
|
+
expect(getStoredVisibility(tableId)).toEqual({col1: true, col2: true});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {useCallback, useMemo} from 'react';
|
|
2
|
+
import {getStorageItem, setStorageItem} from '../../utils/local-storage.js';
|
|
3
|
+
import type {TableState} from './use-table.js';
|
|
4
|
+
|
|
5
|
+
type ColumnVisibility = TableState['columnVisibility'];
|
|
6
|
+
|
|
7
|
+
const storagePath = (tableId: string) => ['table', tableId, 'columnVisibility'] as const;
|
|
8
|
+
|
|
9
|
+
const capVisibleColumns = (visibility: ColumnVisibility, max: number): ColumnVisibility => {
|
|
10
|
+
let visibleCount = 0;
|
|
11
|
+
const result: ColumnVisibility = {};
|
|
12
|
+
for (const [key, isVisible] of Object.entries(visibility)) {
|
|
13
|
+
const shouldShow = isVisible && visibleCount < max;
|
|
14
|
+
result[key] = shouldShow;
|
|
15
|
+
if (shouldShow) {
|
|
16
|
+
visibleCount++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const sanitizeFromStorage = (raw: unknown, validColumnIds: Set<string>): ColumnVisibility => {
|
|
23
|
+
const result: ColumnVisibility = {};
|
|
24
|
+
|
|
25
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
30
|
+
if (validColumnIds.has(key) && typeof value === 'boolean') {
|
|
31
|
+
result[key] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hook that persists column visibility preferences to localStorage.
|
|
39
|
+
*
|
|
40
|
+
* @param defaultVisibleColumns - The default visibility map. Its keys define the set of valid column IDs.
|
|
41
|
+
* Must be a stable reference (e.g. via `useRef`) to avoid re-reading localStorage on every render.
|
|
42
|
+
* @param maxSelectableColumns - Maximum number of columns that can be visible at the same time.
|
|
43
|
+
* @param tableId - Unique identifier for the table. When omitted, no persistence occurs.
|
|
44
|
+
*/
|
|
45
|
+
export const usePersistedColumnVisibility = (
|
|
46
|
+
defaultVisibleColumns: ColumnVisibility,
|
|
47
|
+
maxSelectableColumns: number,
|
|
48
|
+
tableId?: string,
|
|
49
|
+
) => {
|
|
50
|
+
const path = tableId ? storagePath(tableId) : null;
|
|
51
|
+
const validIds = useMemo(() => new Set(Object.keys(defaultVisibleColumns)), [defaultVisibleColumns]);
|
|
52
|
+
const hasValidIds = validIds.size > 0;
|
|
53
|
+
|
|
54
|
+
const initialColumnVisibility = useMemo((): ColumnVisibility => {
|
|
55
|
+
if (!path || !hasValidIds) {
|
|
56
|
+
return defaultVisibleColumns;
|
|
57
|
+
}
|
|
58
|
+
const stored = getStorageItem<unknown>([...path]);
|
|
59
|
+
if (stored !== null) {
|
|
60
|
+
const sanitized = sanitizeFromStorage(stored, validIds);
|
|
61
|
+
if (Object.keys(sanitized).length > 0) {
|
|
62
|
+
return capVisibleColumns({...defaultVisibleColumns, ...sanitized}, maxSelectableColumns);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return capVisibleColumns(defaultVisibleColumns, maxSelectableColumns);
|
|
66
|
+
}, [path, validIds, defaultVisibleColumns, maxSelectableColumns]);
|
|
67
|
+
|
|
68
|
+
const persistColumnVisibility = useCallback(
|
|
69
|
+
(visibility: ColumnVisibility) => {
|
|
70
|
+
if (!path || !hasValidIds) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setStorageItem([...path], visibility);
|
|
74
|
+
},
|
|
75
|
+
[path, hasValidIds],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return {initialColumnVisibility, persistColumnVisibility};
|
|
79
|
+
};
|
|
@@ -4,6 +4,7 @@ import {type ExpandedState, type PaginationState, type SortingState} from '@tans
|
|
|
4
4
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
5
5
|
import {Dispatch, SetStateAction, useCallback, useMemo, useState} from 'react';
|
|
6
6
|
import {useUrlSyncedState, UseUrlSyncedStateOptions} from '../../hooks/use-url-synced-state.js';
|
|
7
|
+
import {usePersistedColumnVisibility} from './use-persisted-column-visibility.js';
|
|
7
8
|
|
|
8
9
|
// Create a deeply optional version of another type
|
|
9
10
|
type DeepPartial<T> = {
|
|
@@ -191,6 +192,18 @@ export interface UseTableOptions<TData = unknown> {
|
|
|
191
192
|
* @default false
|
|
192
193
|
*/
|
|
193
194
|
syncWithUrl?: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* Unique identifier for the table. When provided, column visibility preferences are persisted to localStorage.
|
|
197
|
+
*/
|
|
198
|
+
tableId?: string;
|
|
199
|
+
/**
|
|
200
|
+
* Maximum number of columns that can be visible when restoring persisted visibility from localStorage.
|
|
201
|
+
* This only affects the initial column visibility resolved on mount when `tableId` is set.
|
|
202
|
+
* It does not enforce a runtime limit on `setColumnVisibility` — use `TableColumnsSelector` for UI enforcement.
|
|
203
|
+
*
|
|
204
|
+
* @default Infinity
|
|
205
|
+
*/
|
|
206
|
+
maxSelectableColumns?: number;
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
const defaultOptions: UseTableOptions = {
|
|
@@ -317,7 +330,9 @@ const COLUMN_VISIBILITY_SERIALIZATION = serialization<'columnVisibility'>({
|
|
|
317
330
|
|
|
318
331
|
export const useTable = <TData>(userOptions: UseTableOptions<TData> = {}): TableStore<TData> => {
|
|
319
332
|
const options = defaultsDeep({}, userOptions, defaultOptions) as UseTableOptions<TData>;
|
|
320
|
-
const initialState =
|
|
333
|
+
const [initialState] = useState(
|
|
334
|
+
() => defaultsDeep({}, userOptions.initialState, defaultState) as TableState<TData>,
|
|
335
|
+
);
|
|
321
336
|
/**
|
|
322
337
|
* The `useUrlSyncedState` hook defaults to synchronize, but the table wants to default to not synchronize,
|
|
323
338
|
* so always pass the sync option as a resolved boolean value.
|
|
@@ -355,12 +370,30 @@ export const useTable = <TData>(userOptions: UseTableOptions<TData> = {}): Table
|
|
|
355
370
|
initialState: initialState.dateRange,
|
|
356
371
|
sync,
|
|
357
372
|
});
|
|
358
|
-
|
|
373
|
+
|
|
374
|
+
const {initialColumnVisibility, persistColumnVisibility} = usePersistedColumnVisibility(
|
|
375
|
+
initialState.columnVisibility,
|
|
376
|
+
options.maxSelectableColumns ?? Infinity,
|
|
377
|
+
options.tableId,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const [columnVisibility, _setColumnVisibility] = useUrlSyncedState<TableState<TData>['columnVisibility']>({
|
|
359
381
|
...COLUMN_VISIBILITY_SERIALIZATION,
|
|
360
|
-
initialState:
|
|
382
|
+
initialState: initialColumnVisibility,
|
|
361
383
|
sync,
|
|
362
384
|
});
|
|
363
385
|
|
|
386
|
+
const setColumnVisibility: typeof _setColumnVisibility = useCallback(
|
|
387
|
+
(updater) => {
|
|
388
|
+
_setColumnVisibility((old) => {
|
|
389
|
+
const newVis = updater instanceof Function ? updater(old) : updater;
|
|
390
|
+
persistColumnVisibility(newVis);
|
|
391
|
+
return newVis;
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
[_setColumnVisibility, persistColumnVisibility],
|
|
395
|
+
);
|
|
396
|
+
|
|
364
397
|
// unsynced
|
|
365
398
|
const [totalEntries, _setTotalEntries] = useState<TableState<TData>['totalEntries']>(initialState.totalEntries);
|
|
366
399
|
const [unfilteredTotalEntries, setUnfilteredTotalEntries] = useState<TableState<TData>['totalEntries']>(
|
|
@@ -4,8 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
.header {
|
|
6
6
|
gap: var(--mantine-spacing-sm);
|
|
7
|
+
min-height: unset;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
.close {
|
|
10
11
|
align-self: flex-start;
|
|
11
12
|
}
|
|
13
|
+
|
|
14
|
+
.content {
|
|
15
|
+
overflow-y: initial;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.body {
|
|
21
|
+
overflow-y: scroll;
|
|
22
|
+
border-end-start-radius: var(--modal-radius, var(--mantine-radius-default));
|
|
23
|
+
border-end-end-radius: var(--modal-radius, var(--mantine-radius-default));
|
|
24
|
+
}
|