@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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
+
import {getStorageItem, removeStorageItem, setStorageItem} from '../local-storage';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'plasma';
|
|
5
|
+
|
|
6
|
+
const seed = (value: unknown) => {
|
|
7
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const raw = () => JSON.parse(localStorage.getItem(STORAGE_KEY)!);
|
|
11
|
+
|
|
12
|
+
describe('Plasma versioned localStorage', () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
localStorage.clear();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('getStorageItem', () => {
|
|
18
|
+
it('returns null when storage is empty', () => {
|
|
19
|
+
expect(getStorageItem(['table', 't1', 'columnVisibility'])).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('reads a deeply nested value', () => {
|
|
23
|
+
seed({'storage-version': 1, storage: {table: {t1: {columnVisibility: {col1: true}}}}});
|
|
24
|
+
|
|
25
|
+
expect(getStorageItem(['table', 't1', 'columnVisibility'])).toEqual({col1: true});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns null for a non-existent path', () => {
|
|
29
|
+
seed({'storage-version': 1, storage: {table: {}}});
|
|
30
|
+
|
|
31
|
+
expect(getStorageItem(['table', 'missing', 'columnVisibility'])).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns null when storage version does not match', () => {
|
|
35
|
+
seed({'storage-version': 999, storage: {table: {t1: {columnVisibility: {col1: true}}}}});
|
|
36
|
+
|
|
37
|
+
expect(getStorageItem(['table', 't1', 'columnVisibility'])).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('clears corrupted JSON and returns null', () => {
|
|
41
|
+
localStorage.setItem(STORAGE_KEY, 'not-valid-json{{{');
|
|
42
|
+
|
|
43
|
+
expect(getStorageItem(['any'])).toBeNull();
|
|
44
|
+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('clears storage when root value is not an object', () => {
|
|
48
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify([1, 2, 3]));
|
|
49
|
+
|
|
50
|
+
expect(getStorageItem(['any'])).toBeNull();
|
|
51
|
+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('setStorageItem', () => {
|
|
56
|
+
it('creates the versioned structure when storage is empty', () => {
|
|
57
|
+
setStorageItem(['table', 't1', 'columnVisibility'], {col1: true});
|
|
58
|
+
|
|
59
|
+
expect(raw()).toEqual({
|
|
60
|
+
'storage-version': 1,
|
|
61
|
+
storage: {table: {t1: {columnVisibility: {col1: true}}}},
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('merges into existing storage without overwriting siblings', () => {
|
|
66
|
+
seed({
|
|
67
|
+
'storage-version': 1,
|
|
68
|
+
storage: {table: {t1: {columnVisibility: {col1: true}}}},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
setStorageItem(['table', 't2', 'columnVisibility'], {col2: false});
|
|
72
|
+
|
|
73
|
+
const data = raw();
|
|
74
|
+
expect(data.storage.table.t1).toEqual({columnVisibility: {col1: true}});
|
|
75
|
+
expect(data.storage.table.t2).toEqual({columnVisibility: {col2: false}});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('overwrites an existing value at the same path', () => {
|
|
79
|
+
seed({'storage-version': 1, storage: {table: {t1: {columnVisibility: {col1: true}}}}});
|
|
80
|
+
|
|
81
|
+
setStorageItem(['table', 't1', 'columnVisibility'], {col1: false, col2: true});
|
|
82
|
+
|
|
83
|
+
expect(raw().storage.table.t1.columnVisibility).toEqual({col1: false, col2: true});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('resets storage when version does not match', () => {
|
|
87
|
+
seed({'storage-version': 999, storage: {old: 'data'}});
|
|
88
|
+
|
|
89
|
+
setStorageItem(['table', 't1', 'theme'], 'dark');
|
|
90
|
+
|
|
91
|
+
const data = raw();
|
|
92
|
+
expect(data['storage-version']).toBe(1);
|
|
93
|
+
expect(data.storage.old).toBeUndefined();
|
|
94
|
+
expect(data.storage.table.t1.theme).toBe('dark');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('creates intermediate objects along the path', () => {
|
|
98
|
+
setStorageItem(['a', 'b', 'c', 'd'], 42);
|
|
99
|
+
|
|
100
|
+
expect(raw().storage.a.b.c.d).toBe(42);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('warns when localStorage is full', () => {
|
|
104
|
+
const warnSpy = vi.spyOn(console, 'warn').mockReturnValue(undefined);
|
|
105
|
+
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
106
|
+
throw new DOMException('quota exceeded');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
setStorageItem(['table', 't1', 'col'], {});
|
|
110
|
+
|
|
111
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unable to write'));
|
|
112
|
+
warnSpy.mockRestore();
|
|
113
|
+
vi.restoreAllMocks();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('removeStorageItem', () => {
|
|
118
|
+
it('removes a nested key without affecting siblings', () => {
|
|
119
|
+
seed({
|
|
120
|
+
'storage-version': 1,
|
|
121
|
+
storage: {table: {t1: {columnVisibility: {col1: true}, theme: 'dark'}}},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
removeStorageItem(['table', 't1', 'columnVisibility']);
|
|
125
|
+
|
|
126
|
+
const data = raw();
|
|
127
|
+
expect(data.storage.table.t1.columnVisibility).toBeUndefined();
|
|
128
|
+
expect(data.storage.table.t1.theme).toBe('dark');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does nothing when storage is empty', () => {
|
|
132
|
+
removeStorageItem(['table', 't1', 'columnVisibility']);
|
|
133
|
+
|
|
134
|
+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('does nothing when the path does not exist', () => {
|
|
138
|
+
seed({'storage-version': 1, storage: {table: {}}});
|
|
139
|
+
|
|
140
|
+
removeStorageItem(['table', 'missing', 'columnVisibility']);
|
|
141
|
+
|
|
142
|
+
expect(raw().storage.table).toEqual({});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does nothing when storage version does not match', () => {
|
|
146
|
+
seed({'storage-version': 999, storage: {table: {t1: {columnVisibility: {col1: true}}}}});
|
|
147
|
+
|
|
148
|
+
removeStorageItem(['table', 't1', 'columnVisibility']);
|
|
149
|
+
|
|
150
|
+
expect(raw().storage.table.t1.columnVisibility).toEqual({col1: true});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('prototype pollution protection', () => {
|
|
155
|
+
it.each(['__proto__', 'constructor', 'prototype'])('getStorageItem rejects path containing "%s"', (key) => {
|
|
156
|
+
seed({'storage-version': 1, storage: {[key]: {nested: 'value'}}});
|
|
157
|
+
|
|
158
|
+
expect(getStorageItem([key, 'nested'])).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it.each(['__proto__', 'constructor', 'prototype'])('setStorageItem rejects path containing "%s"', (key) => {
|
|
162
|
+
setStorageItem([key, 'polluted'], true);
|
|
163
|
+
|
|
164
|
+
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
165
|
+
expect(Object.prototype).not.toHaveProperty('polluted');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it.each(['__proto__', 'constructor', 'prototype'])('removeStorageItem rejects path containing "%s"', (key) => {
|
|
169
|
+
seed({'storage-version': 1, storage: {safe: 'data'}});
|
|
170
|
+
|
|
171
|
+
removeStorageItem([key]);
|
|
172
|
+
|
|
173
|
+
expect(raw().storage.safe).toBe('data');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned localStorage utility for Plasma.
|
|
3
|
+
*
|
|
4
|
+
* All Plasma data is stored under a single `"plasma"` key with the shape:
|
|
5
|
+
* ```json
|
|
6
|
+
* {
|
|
7
|
+
* "storage-version": 1,
|
|
8
|
+
* "storage": {
|
|
9
|
+
* "table": {
|
|
10
|
+
* "my-table": { "columnVisibility": { ... } }
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const STORAGE_KEY = 'plasma';
|
|
18
|
+
export const CURRENT_STORAGE_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
type JsonObject = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
const createStorageObject = (): JsonObject => Object.create(null) as JsonObject;
|
|
23
|
+
|
|
24
|
+
const isUnsafeKey = (key: string): boolean => key === '__proto__' || key === 'constructor' || key === 'prototype';
|
|
25
|
+
|
|
26
|
+
const isSafePath = (path: string[]): boolean => path.every((key) => !isUnsafeKey(key));
|
|
27
|
+
|
|
28
|
+
interface PlasmaStorageSchema {
|
|
29
|
+
'storage-version': number;
|
|
30
|
+
storage: JsonObject;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const readStorage = (): PlasmaStorageSchema | null => {
|
|
34
|
+
try {
|
|
35
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
36
|
+
if (raw === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
41
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return parsed as PlasmaStorageSchema;
|
|
45
|
+
} catch {
|
|
46
|
+
try {
|
|
47
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
48
|
+
} catch {
|
|
49
|
+
console.warn(`Unable to clean up corrupted localStorage key "${STORAGE_KEY}".`);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const writeStorage = (data: PlasmaStorageSchema): void => {
|
|
56
|
+
try {
|
|
57
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
58
|
+
} catch {
|
|
59
|
+
console.warn(`Unable to write to localStorage key "${STORAGE_KEY}".`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getNestedValue = (obj: JsonObject, path: string[]): unknown => {
|
|
64
|
+
let current: unknown = obj;
|
|
65
|
+
for (const key of path) {
|
|
66
|
+
if (typeof current !== 'object' || current === null) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
current = (current as JsonObject)[key];
|
|
70
|
+
}
|
|
71
|
+
return current;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const setNestedValue = (obj: JsonObject, path: string[], value: unknown): void => {
|
|
75
|
+
let current = obj;
|
|
76
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
77
|
+
const key = path[i];
|
|
78
|
+
if (isUnsafeKey(key)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (typeof current[key] !== 'object' || current[key] === null) {
|
|
82
|
+
current[key] = createStorageObject();
|
|
83
|
+
}
|
|
84
|
+
current = current[key] as JsonObject;
|
|
85
|
+
}
|
|
86
|
+
const lastKey = path[path.length - 1];
|
|
87
|
+
if (!isUnsafeKey(lastKey)) {
|
|
88
|
+
current[lastKey] = value;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read a value from the versioned Plasma storage at the given path.
|
|
94
|
+
*
|
|
95
|
+
* @param path - Path segments within `storage`, e.g. `['table', 'my-table', 'columnVisibility']`.
|
|
96
|
+
* @returns The stored value, or `null` if it doesn't exist or the storage is corrupted.
|
|
97
|
+
*/
|
|
98
|
+
export const getStorageItem = <T = unknown>(path: string[]): T | null => {
|
|
99
|
+
if (!isSafePath(path)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const data = readStorage();
|
|
103
|
+
if (!data || data['storage-version'] !== CURRENT_STORAGE_VERSION) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const value = getNestedValue(data.storage ?? createStorageObject(), path);
|
|
107
|
+
return value !== undefined ? (value as T) : null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Write a value to the versioned Plasma storage at the given path.
|
|
112
|
+
*
|
|
113
|
+
* @param path - Path segments within `storage`, e.g. `['table', 'my-table', 'columnVisibility']`.
|
|
114
|
+
* @param value - The value to store (must be JSON-serializable).
|
|
115
|
+
*/
|
|
116
|
+
export const setStorageItem = <T = unknown>(path: string[], value: T): void => {
|
|
117
|
+
if (!isSafePath(path)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let data = readStorage();
|
|
121
|
+
if (!data || data['storage-version'] !== CURRENT_STORAGE_VERSION) {
|
|
122
|
+
data = {'storage-version': CURRENT_STORAGE_VERSION, storage: createStorageObject()};
|
|
123
|
+
}
|
|
124
|
+
if (!data.storage) {
|
|
125
|
+
data.storage = createStorageObject();
|
|
126
|
+
}
|
|
127
|
+
setNestedValue(data.storage, path, value);
|
|
128
|
+
writeStorage(data);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Remove a value from the versioned Plasma storage at the given path.
|
|
133
|
+
*
|
|
134
|
+
* @param path - Path segments within `storage`, e.g. `['table', 'my-table', 'columnVisibility']`.
|
|
135
|
+
*/
|
|
136
|
+
export const removeStorageItem = (path: string[]): void => {
|
|
137
|
+
if (!isSafePath(path)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const data = readStorage();
|
|
141
|
+
if (!data || data['storage-version'] !== CURRENT_STORAGE_VERSION) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const parentPath = path.slice(0, -1);
|
|
145
|
+
const key = path[path.length - 1];
|
|
146
|
+
const parent = getNestedValue(data.storage ?? {}, parentPath);
|
|
147
|
+
if (typeof parent === 'object' && parent !== null) {
|
|
148
|
+
delete (parent as JsonObject)[key];
|
|
149
|
+
writeStorage(data);
|
|
150
|
+
}
|
|
151
|
+
};
|