@rozenite/storage-plugin 2.0.0 → 2.2.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/CHANGELOG.md +36 -0
- package/dist/devtools/assets/panel-D5z1BY8U.js +32 -0
- package/dist/devtools/assets/panel-DVlWRg6d.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.js +1 -3
- package/dist/react-native/chunks/secure-storage.require.js +2 -6
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +266 -295
- package/dist/react-native/index.d.ts +14 -2
- package/dist/react-native/index.js +2 -10
- package/dist/rozenite.json +1 -1
- package/package.json +29 -29
- package/react-native.ts +11 -36
- package/rozenite.config.ts +2 -6
- package/sdk.ts +1 -5
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +12 -40
- package/src/react-native/__tests__/export-snapshot.test.ts +2 -6
- package/src/react-native/__tests__/full-entry-request.test.ts +5 -17
- package/src/react-native/__tests__/import.test.ts +9 -8
- package/src/react-native/__tests__/storage-discovery.test.ts +7 -18
- package/src/react-native/__tests__/stress-verification.test.ts +5 -18
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +25 -35
- package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
- package/src/react-native/adapters/async-storage.ts +2 -5
- package/src/react-native/adapters/index.ts +2 -8
- package/src/react-native/adapters/mmkv.ts +7 -24
- package/src/react-native/adapters/secure-storage.ts +2 -6
- package/src/react-native/entry-preview-pagination.ts +11 -27
- package/src/react-native/export-snapshot.ts +3 -9
- package/src/react-native/full-entry-request.ts +3 -8
- package/src/react-native/import.ts +10 -21
- package/src/react-native/storage-discovery.ts +2 -6
- package/src/react-native/storage-view.ts +2 -6
- package/src/react-native/useRozeniteStoragePlugin.ts +107 -119
- package/src/react-native/useStorageAgentTools.ts +8 -24
- package/src/shared/__tests__/entry-preview.test.ts +9 -27
- package/src/shared/__tests__/snapshot.test.ts +10 -32
- package/src/shared/agent-tools.ts +9 -30
- package/src/shared/entry-preview.ts +3 -8
- package/src/shared/messaging.ts +15 -11
- package/src/shared/snapshot.ts +4 -17
- package/src/shared/types.ts +2 -4
- package/src/ui/__tests__/binary-value-editor-state.test.ts +6 -16
- package/src/ui/__tests__/binary.test.ts +13 -33
- package/src/ui/__tests__/large-value-viewer.test.tsx +4 -13
- package/src/ui/__tests__/panel.test.tsx +50 -63
- package/src/ui/__tests__/storage-groups.test.ts +1 -4
- package/src/ui/__tests__/type-conversion.test.ts +8 -24
- package/src/ui/__tests__/use-storage-entries.test.tsx +18 -34
- package/src/ui/add-entry-dialog.tsx +66 -87
- package/src/ui/binary-value-editor-state.ts +6 -24
- package/src/ui/binary-value-editor.tsx +6 -18
- package/src/ui/binary.ts +6 -19
- package/src/ui/edit-entry-dialog.tsx +59 -78
- package/src/ui/editor-switcher.tsx +2 -11
- package/src/ui/entry-detail-dialog.tsx +9 -16
- package/src/ui/format-value.tsx +2 -9
- package/src/ui/import-dialog.tsx +12 -24
- package/src/ui/large-value-viewer-state.ts +2 -9
- package/src/ui/large-value-viewer.tsx +3 -9
- package/src/ui/panel.tsx +129 -235
- package/src/ui/query-client.tsx +4 -15
- package/src/ui/type-conversion.ts +2 -5
- package/src/ui/typed-value-editor.tsx +1 -5
- package/src/ui/use-storage-entries.ts +6 -21
- package/src/ui/utils.ts +1 -4
- package/tsconfig.json +4 -7
- package/dist/devtools/assets/panel-B6Wp0eie.css +0 -1
- package/dist/devtools/assets/panel-D7MFc4HG.js +0 -32
|
@@ -97,11 +97,8 @@ describe('storage entry query hooks', () => {
|
|
|
97
97
|
if (payload.cursor === undefined)
|
|
98
98
|
return Promise.resolve(response(undefined, { next: 'next' }));
|
|
99
99
|
if (payload.cursor === 'next')
|
|
100
|
-
return Promise.resolve(
|
|
101
|
-
|
|
102
|
-
);
|
|
103
|
-
if (payload.cursor === 'last')
|
|
104
|
-
return Promise.resolve(response('last', { previous: 'next' }));
|
|
100
|
+
return Promise.resolve(response('next', { next: 'last', previous: 'first' }));
|
|
101
|
+
if (payload.cursor === 'last') return Promise.resolve(response('last', { previous: 'next' }));
|
|
105
102
|
return Promise.resolve(response('first', { next: 'next' }));
|
|
106
103
|
});
|
|
107
104
|
const hook = await renderPreviewHook({
|
|
@@ -117,9 +114,11 @@ describe('storage entry query hooks', () => {
|
|
|
117
114
|
});
|
|
118
115
|
await act(async () => hook.result.fetchNextPage());
|
|
119
116
|
await act(async () => hook.result.fetchNextPage());
|
|
120
|
-
expect(
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
expect(request.mock.calls.map(([options]) => options.payload.cursor)).toEqual([
|
|
118
|
+
undefined,
|
|
119
|
+
'next',
|
|
120
|
+
'last',
|
|
121
|
+
]);
|
|
123
122
|
const queryKey = storageEntryPreviewQueryKey(target, '', 'ascending');
|
|
124
123
|
const pageKeys = () =>
|
|
125
124
|
hook.queryClient
|
|
@@ -134,9 +133,7 @@ describe('storage entry query hooks', () => {
|
|
|
134
133
|
|
|
135
134
|
it('cancels stale target requests and starts a new sequence at the first page', async () => {
|
|
136
135
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
137
|
-
let resolveFirst:
|
|
138
|
-
| ((value: ReturnType<typeof response>) => void)
|
|
139
|
-
| undefined;
|
|
136
|
+
let resolveFirst: ((value: ReturnType<typeof response>) => void) | undefined;
|
|
140
137
|
const request = vi.fn(({ payload }) => {
|
|
141
138
|
if (payload.target === target) {
|
|
142
139
|
return new Promise((resolve) => {
|
|
@@ -171,8 +168,7 @@ describe('storage entry query hooks', () => {
|
|
|
171
168
|
expect(
|
|
172
169
|
hook.queryClient.getQueryData<{
|
|
173
170
|
pages: Array<ReturnType<typeof response>>;
|
|
174
|
-
}>(storageEntryPreviewQueryKey(otherTarget, 'two', 'descending'))
|
|
175
|
-
?.pages[0].target,
|
|
171
|
+
}>(storageEntryPreviewQueryKey(otherTarget, 'two', 'descending'))?.pages[0].target,
|
|
176
172
|
).toEqual(otherTarget),
|
|
177
173
|
);
|
|
178
174
|
await hook.unmount();
|
|
@@ -243,9 +239,7 @@ describe('storage entry query hooks', () => {
|
|
|
243
239
|
|
|
244
240
|
it('resets every selected-storage preview variant but refetches only the active first page', async () => {
|
|
245
241
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
246
|
-
const request = vi.fn(({ payload }) =>
|
|
247
|
-
Promise.resolve(response(payload.cursor)),
|
|
248
|
-
);
|
|
242
|
+
const request = vi.fn(({ payload }) => Promise.resolve(response(payload.cursor)));
|
|
249
243
|
const hook = await renderPreviewHook({
|
|
250
244
|
client: asClient(request),
|
|
251
245
|
target,
|
|
@@ -255,28 +249,22 @@ describe('storage entry query hooks', () => {
|
|
|
255
249
|
await act(async () => {
|
|
256
250
|
await vi.waitFor(() => expect(hook.result.isSuccess).toBe(true));
|
|
257
251
|
});
|
|
258
|
-
hook.queryClient.setQueryData(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
);
|
|
252
|
+
hook.queryClient.setQueryData(storageEntryPreviewQueryKey(target, 'inactive', 'ascending'), {
|
|
253
|
+
pages: [response(undefined)],
|
|
254
|
+
pageParams: [undefined],
|
|
255
|
+
});
|
|
262
256
|
|
|
263
|
-
await act(async () =>
|
|
264
|
-
resetSelectedStorageQueries(hook.queryClient, target),
|
|
265
|
-
);
|
|
257
|
+
await act(async () => resetSelectedStorageQueries(hook.queryClient, target));
|
|
266
258
|
expect(request).toHaveBeenCalledTimes(2);
|
|
267
259
|
expect(
|
|
268
|
-
hook.queryClient.getQueryData(
|
|
269
|
-
storageEntryPreviewQueryKey(target, 'inactive', 'ascending'),
|
|
270
|
-
),
|
|
260
|
+
hook.queryClient.getQueryData(storageEntryPreviewQueryKey(target, 'inactive', 'ascending')),
|
|
271
261
|
).toBeUndefined();
|
|
272
262
|
await hook.unmount();
|
|
273
263
|
});
|
|
274
264
|
|
|
275
265
|
it('invalidates only the changed storage preview prefix and drops its stale full value', async () => {
|
|
276
266
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
277
|
-
const request = vi.fn(({ payload }) =>
|
|
278
|
-
Promise.resolve(response(payload.cursor)),
|
|
279
|
-
);
|
|
267
|
+
const request = vi.fn(({ payload }) => Promise.resolve(response(payload.cursor)));
|
|
280
268
|
const hook = await renderPreviewHook({
|
|
281
269
|
client: asClient(request),
|
|
282
270
|
target,
|
|
@@ -286,11 +274,7 @@ describe('storage entry query hooks', () => {
|
|
|
286
274
|
await act(async () => {
|
|
287
275
|
await vi.waitFor(() => expect(hook.result.isSuccess).toBe(true));
|
|
288
276
|
});
|
|
289
|
-
const otherQueryKey = storageEntryPreviewQueryKey(
|
|
290
|
-
otherTarget,
|
|
291
|
-
'other',
|
|
292
|
-
'ascending',
|
|
293
|
-
);
|
|
277
|
+
const otherQueryKey = storageEntryPreviewQueryKey(otherTarget, 'other', 'ascending');
|
|
294
278
|
hook.queryClient.setQueryDefaults(otherQueryKey, { gcTime: Infinity });
|
|
295
279
|
hook.queryClient.setQueryData(otherQueryKey, {
|
|
296
280
|
pages: [response(undefined)],
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { Button,
|
|
1
|
+
import { Button, Dialog, Field, Input, useConfirmDialog } from '@rozenite/ui';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import type {
|
|
4
|
-
StorageEntry,
|
|
5
|
-
StorageEntryType,
|
|
6
|
-
StorageEntryValue,
|
|
7
|
-
} from '../shared/types';
|
|
3
|
+
import type { StorageEntry, StorageEntryType, StorageEntryValue } from '../shared/types';
|
|
8
4
|
import { TypedValueEditor } from './typed-value-editor';
|
|
9
5
|
import { defaultValueForType } from './type-conversion';
|
|
10
6
|
|
|
@@ -49,9 +45,7 @@ export const AddEntryDialog = ({
|
|
|
49
45
|
const [currentValue, setCurrentValue] = useState<StorageEntryValue | null>(
|
|
50
46
|
defaultValueForType(initialType),
|
|
51
47
|
);
|
|
52
|
-
const
|
|
53
|
-
null,
|
|
54
|
-
);
|
|
48
|
+
const confirm = useConfirmDialog();
|
|
55
49
|
|
|
56
50
|
// Reset state every time the dialog opens, so a previous session's
|
|
57
51
|
// type / value doesn't bleed in.
|
|
@@ -72,21 +66,23 @@ export const AddEntryDialog = ({
|
|
|
72
66
|
onClose();
|
|
73
67
|
};
|
|
74
68
|
|
|
75
|
-
const handleAdd = () => {
|
|
69
|
+
const handleAdd = async () => {
|
|
76
70
|
if (!newEntryKey.trim() || currentValue === null) return;
|
|
77
71
|
|
|
78
72
|
if (!isCurrentTypeSupported) {
|
|
79
|
-
|
|
73
|
+
await confirm({
|
|
74
|
+
variant: 'alert',
|
|
80
75
|
title: 'Unsupported Type',
|
|
81
|
-
|
|
76
|
+
description: 'Selected type is not supported by this storage.',
|
|
82
77
|
});
|
|
83
78
|
return;
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
if (existingKeys.includes(newEntryKey)) {
|
|
87
|
-
|
|
82
|
+
await confirm({
|
|
83
|
+
variant: 'alert',
|
|
88
84
|
title: 'Key Already Exists',
|
|
89
|
-
|
|
85
|
+
description: 'An entry with this key already exists.',
|
|
90
86
|
});
|
|
91
87
|
return;
|
|
92
88
|
}
|
|
@@ -96,85 +92,68 @@ export const AddEntryDialog = ({
|
|
|
96
92
|
};
|
|
97
93
|
|
|
98
94
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
newEntryKey.trim() &&
|
|
102
|
-
currentType !== 'buffer'
|
|
103
|
-
) {
|
|
104
|
-
handleAdd();
|
|
95
|
+
if (event.key === 'Enter' && newEntryKey.trim() && currentType !== 'buffer') {
|
|
96
|
+
void handleAdd();
|
|
105
97
|
}
|
|
106
98
|
};
|
|
107
99
|
|
|
108
100
|
// Unsavable when no key, no supported type, or when the value is
|
|
109
101
|
// null — the hex editor signals invalid / empty hex via null.
|
|
110
|
-
const isAddDisabled =
|
|
111
|
-
!newEntryKey.trim() || !isCurrentTypeSupported || currentValue === null;
|
|
102
|
+
const isAddDisabled = !newEntryKey.trim() || !isCurrentTypeSupported || currentValue === null;
|
|
112
103
|
|
|
113
104
|
return (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
>
|
|
121
|
-
<Dialog.
|
|
122
|
-
<Dialog.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
<Field>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<Field>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
</Dialog>
|
|
168
|
-
|
|
169
|
-
<ConfirmDialog
|
|
170
|
-
open={alert !== null}
|
|
171
|
-
onOpenChange={(open) => {
|
|
172
|
-
if (!open) setAlert(null);
|
|
173
|
-
}}
|
|
174
|
-
variant="alert"
|
|
175
|
-
title={alert?.title ?? ''}
|
|
176
|
-
description={alert?.message}
|
|
177
|
-
/>
|
|
178
|
-
</>
|
|
105
|
+
<Dialog
|
|
106
|
+
open={isOpen}
|
|
107
|
+
onOpenChange={(open) => {
|
|
108
|
+
if (!open) resetAndClose();
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<Dialog.Content onKeyDown={handleKeyDown}>
|
|
112
|
+
<Dialog.Header>
|
|
113
|
+
<Dialog.Title>Add New Entry</Dialog.Title>
|
|
114
|
+
</Dialog.Header>
|
|
115
|
+
|
|
116
|
+
<div className="flex flex-col gap-4">
|
|
117
|
+
<Field>
|
|
118
|
+
<Field.Label htmlFor="new-entry-key">Key</Field.Label>
|
|
119
|
+
<Input
|
|
120
|
+
id="new-entry-key"
|
|
121
|
+
value={newEntryKey}
|
|
122
|
+
onChange={(event) => setNewEntryKey(event.target.value)}
|
|
123
|
+
placeholder="Enter key name"
|
|
124
|
+
autoFocus
|
|
125
|
+
/>
|
|
126
|
+
</Field>
|
|
127
|
+
|
|
128
|
+
<Field>
|
|
129
|
+
<Field.Label htmlFor="new-entry-value">Value</Field.Label>
|
|
130
|
+
<TypedValueEditor
|
|
131
|
+
supportedTypes={supportedTypes}
|
|
132
|
+
type={currentType}
|
|
133
|
+
value={currentValue}
|
|
134
|
+
onChange={(nextType, nextValue) => {
|
|
135
|
+
setCurrentType(nextType);
|
|
136
|
+
setCurrentValue(nextValue);
|
|
137
|
+
}}
|
|
138
|
+
inputId="new-entry-value"
|
|
139
|
+
/>
|
|
140
|
+
{!isCurrentTypeSupported && (
|
|
141
|
+
<Field.Description className="text-danger">
|
|
142
|
+
Selected type is not supported by this storage.
|
|
143
|
+
</Field.Description>
|
|
144
|
+
)}
|
|
145
|
+
</Field>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<Dialog.Footer>
|
|
149
|
+
<Button tone="neutral" variant="outline" onClick={resetAndClose}>
|
|
150
|
+
Cancel
|
|
151
|
+
</Button>
|
|
152
|
+
<Button onClick={() => void handleAdd()} disabled={isAddDisabled}>
|
|
153
|
+
Add Entry
|
|
154
|
+
</Button>
|
|
155
|
+
</Dialog.Footer>
|
|
156
|
+
</Dialog.Content>
|
|
157
|
+
</Dialog>
|
|
179
158
|
);
|
|
180
159
|
};
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
base64ToBytes,
|
|
3
|
-
bytesToBase64,
|
|
4
|
-
bytesToGroupedHex,
|
|
5
|
-
hexInputToBytes,
|
|
6
|
-
} from './binary';
|
|
1
|
+
import { base64ToBytes, bytesToBase64, bytesToGroupedHex, hexInputToBytes } from './binary';
|
|
7
2
|
|
|
8
3
|
export type EditorMode = 'hex' | 'base64';
|
|
9
4
|
|
|
@@ -20,9 +15,7 @@ export type EditorAction =
|
|
|
20
15
|
| { type: 'switch-mode'; mode: EditorMode }
|
|
21
16
|
| { type: 'replace-bytes'; bytes: number[] };
|
|
22
17
|
|
|
23
|
-
export type Validation =
|
|
24
|
-
| { ok: true; bytes: number[] }
|
|
25
|
-
| { ok: false; reason: string };
|
|
18
|
+
export type Validation = { ok: true; bytes: number[] } | { ok: false; reason: string };
|
|
26
19
|
|
|
27
20
|
const encode = (bytes: readonly number[], mode: EditorMode): string =>
|
|
28
21
|
mode === 'hex' ? bytesToGroupedHex(bytes) : bytesToBase64(bytes);
|
|
@@ -30,20 +23,12 @@ const encode = (bytes: readonly number[], mode: EditorMode): string =>
|
|
|
30
23
|
const parse = (text: string, mode: EditorMode) =>
|
|
31
24
|
mode === 'hex' ? hexInputToBytes(text) : base64ToBytes(text);
|
|
32
25
|
|
|
33
|
-
const parsedToState = (
|
|
34
|
-
text: string,
|
|
35
|
-
mode: EditorMode,
|
|
36
|
-
): Pick<EditorState, 'bytes' | 'error'> => {
|
|
26
|
+
const parsedToState = (text: string, mode: EditorMode): Pick<EditorState, 'bytes' | 'error'> => {
|
|
37
27
|
const result = parse(text, mode);
|
|
38
|
-
return result.ok
|
|
39
|
-
? { bytes: result.value, error: null }
|
|
40
|
-
: { bytes: null, error: result.error };
|
|
28
|
+
return result.ok ? { bytes: result.value, error: null } : { bytes: null, error: result.error };
|
|
41
29
|
};
|
|
42
30
|
|
|
43
|
-
export const initialState = (args: {
|
|
44
|
-
initialBytes?: number[];
|
|
45
|
-
mode?: EditorMode;
|
|
46
|
-
}): EditorState => {
|
|
31
|
+
export const initialState = (args: { initialBytes?: number[]; mode?: EditorMode }): EditorState => {
|
|
47
32
|
const mode = args.mode ?? 'hex';
|
|
48
33
|
if (args.initialBytes && args.initialBytes.length > 0) {
|
|
49
34
|
return {
|
|
@@ -61,10 +46,7 @@ export const initialState = (args: {
|
|
|
61
46
|
};
|
|
62
47
|
};
|
|
63
48
|
|
|
64
|
-
export const reduce = (
|
|
65
|
-
state: EditorState,
|
|
66
|
-
action: EditorAction,
|
|
67
|
-
): EditorState => {
|
|
49
|
+
export const reduce = (state: EditorState, action: EditorAction): EditorState => {
|
|
68
50
|
switch (action.type) {
|
|
69
51
|
case 'set-text': {
|
|
70
52
|
return {
|
|
@@ -4,11 +4,7 @@ import { EditorView, keymap } from '@codemirror/view';
|
|
|
4
4
|
import { cn } from '@rozenite/ui';
|
|
5
5
|
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
6
6
|
import { compactAsciiPreview } from './binary';
|
|
7
|
-
import {
|
|
8
|
-
initialState,
|
|
9
|
-
reduce,
|
|
10
|
-
type EditorMode,
|
|
11
|
-
} from './binary-value-editor-state';
|
|
7
|
+
import { initialState, reduce, type EditorMode } from './binary-value-editor-state';
|
|
12
8
|
|
|
13
9
|
export type BinaryValueEditorProps = {
|
|
14
10
|
initialBytes?: number[];
|
|
@@ -57,13 +53,8 @@ const ModeButton = ({
|
|
|
57
53
|
</button>
|
|
58
54
|
);
|
|
59
55
|
|
|
60
|
-
export const BinaryValueEditor = ({
|
|
61
|
-
|
|
62
|
-
onChange,
|
|
63
|
-
}: BinaryValueEditorProps) => {
|
|
64
|
-
const [state, dispatch] = useReducer(reduce, undefined, () =>
|
|
65
|
-
initialState({}),
|
|
66
|
-
);
|
|
56
|
+
export const BinaryValueEditor = ({ initialBytes, onChange }: BinaryValueEditorProps) => {
|
|
57
|
+
const [state, dispatch] = useReducer(reduce, undefined, () => initialState({}));
|
|
67
58
|
const initialBytesRef = useRef(initialBytes);
|
|
68
59
|
const [isPreparing, setIsPreparing] = useState(
|
|
69
60
|
Boolean(initialBytesRef.current && initialBytesRef.current.length > 0),
|
|
@@ -108,9 +99,7 @@ export const BinaryValueEditor = ({
|
|
|
108
99
|
if (!update.docChanged) return;
|
|
109
100
|
const newText = update.state.doc.toString();
|
|
110
101
|
if (newText === stateRef.current.text) return;
|
|
111
|
-
const isPaste = update.transactions.some((tr) =>
|
|
112
|
-
tr.isUserEvent('input.paste'),
|
|
113
|
-
);
|
|
102
|
+
const isPaste = update.transactions.some((tr) => tr.isUserEvent('input.paste'));
|
|
114
103
|
dispatch({
|
|
115
104
|
type: isPaste ? 'normalize-paste' : 'set-text',
|
|
116
105
|
text: newText,
|
|
@@ -181,11 +170,10 @@ export const BinaryValueEditor = ({
|
|
|
181
170
|
<div className="text-muted-foreground">{byteCount} bytes</div>
|
|
182
171
|
{asciiPreview && (
|
|
183
172
|
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
|
|
184
|
-
ASCII:{
|
|
185
|
-
<span className="font-mono text-foreground">{asciiPreview}</span>
|
|
173
|
+
ASCII: <span className="font-mono text-foreground">{asciiPreview}</span>
|
|
186
174
|
</div>
|
|
187
175
|
)}
|
|
188
|
-
{state.error && <div className="text-
|
|
176
|
+
{state.error && <div className="text-danger">{state.error}</div>}
|
|
189
177
|
</div>
|
|
190
178
|
</div>
|
|
191
179
|
);
|
package/src/ui/binary.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export type ParseResult<T> =
|
|
2
|
-
| { ok: true; value: T }
|
|
3
|
-
| { ok: false; error: string };
|
|
1
|
+
export type ParseResult<T> = { ok: true; value: T } | { ok: false; error: string };
|
|
4
2
|
|
|
5
3
|
const BYTES_PER_LINE = 16;
|
|
6
4
|
const BYTES_PER_GROUP = 8;
|
|
@@ -11,13 +9,10 @@ const OFFSET_WIDTH = 8;
|
|
|
11
9
|
const ASCII_PRINTABLE_MIN = 0x20;
|
|
12
10
|
const ASCII_PRINTABLE_MAX = 0x7e;
|
|
13
11
|
|
|
14
|
-
const toHexPair = (byte: number): string =>
|
|
15
|
-
byte.toString(16).toUpperCase().padStart(2, '0');
|
|
12
|
+
const toHexPair = (byte: number): string => byte.toString(16).toUpperCase().padStart(2, '0');
|
|
16
13
|
|
|
17
14
|
const toAsciiChar = (byte: number): string =>
|
|
18
|
-
byte >= ASCII_PRINTABLE_MIN && byte <= ASCII_PRINTABLE_MAX
|
|
19
|
-
? String.fromCharCode(byte)
|
|
20
|
-
: '.';
|
|
15
|
+
byte >= ASCII_PRINTABLE_MIN && byte <= ASCII_PRINTABLE_MAX ? String.fromCharCode(byte) : '.';
|
|
21
16
|
|
|
22
17
|
const formatHexLine = (slice: readonly number[]): string => {
|
|
23
18
|
const left = slice.slice(0, BYTES_PER_GROUP).map(toHexPair).join(' ');
|
|
@@ -33,10 +28,7 @@ export type HexdumpRow = {
|
|
|
33
28
|
|
|
34
29
|
// Produces one visible hexdump row. Unlike bytesToHexdump this never builds a
|
|
35
30
|
// representation for bytes outside the requested row.
|
|
36
|
-
export const formatHexdumpRow = (
|
|
37
|
-
bytes: readonly number[],
|
|
38
|
-
rowStart: number,
|
|
39
|
-
): HexdumpRow => {
|
|
31
|
+
export const formatHexdumpRow = (bytes: readonly number[], rowStart: number): HexdumpRow => {
|
|
40
32
|
let left = '';
|
|
41
33
|
let right = '';
|
|
42
34
|
let ascii = '';
|
|
@@ -82,10 +74,7 @@ export const bytesToHexdump = (bytes: readonly number[]): string => {
|
|
|
82
74
|
export const bytesToAsciiPreview = (bytes: readonly number[]): string =>
|
|
83
75
|
bytes.map(toAsciiChar).join('');
|
|
84
76
|
|
|
85
|
-
export const compactAsciiPreview = (
|
|
86
|
-
bytes: readonly number[],
|
|
87
|
-
maxBytes = 64,
|
|
88
|
-
): string => {
|
|
77
|
+
export const compactAsciiPreview = (bytes: readonly number[], maxBytes = 64): string => {
|
|
89
78
|
let preview = '';
|
|
90
79
|
const limit = Math.min(bytes.length, maxBytes);
|
|
91
80
|
for (let index = 0; index < limit; index++) {
|
|
@@ -133,9 +122,7 @@ const TRAILING_ASCII_COLUMN_RE = /\|[^|]*\|\s*$/;
|
|
|
133
122
|
export const hexInputToBytes = (input: string): ParseResult<number[]> => {
|
|
134
123
|
const cleaned = input
|
|
135
124
|
.split(/\r?\n/)
|
|
136
|
-
.map((line) =>
|
|
137
|
-
line.replace(TRAILING_ASCII_COLUMN_RE, '').replace(HEXDUMP_OFFSET_RE, ''),
|
|
138
|
-
)
|
|
125
|
+
.map((line) => line.replace(TRAILING_ASCII_COLUMN_RE, '').replace(HEXDUMP_OFFSET_RE, ''))
|
|
139
126
|
.join('')
|
|
140
127
|
.replace(/0x/gi, '')
|
|
141
128
|
.replace(/\s+/g, '');
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { Button,
|
|
1
|
+
import { Button, Dialog, Field, useConfirmDialog } from '@rozenite/ui';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import type {
|
|
4
|
-
StorageEntry,
|
|
5
|
-
StorageEntryType,
|
|
6
|
-
StorageEntryValue,
|
|
7
|
-
} from '../shared/types';
|
|
3
|
+
import type { StorageEntry, StorageEntryType, StorageEntryValue } from '../shared/types';
|
|
8
4
|
import { TypedValueEditor } from './typed-value-editor';
|
|
9
5
|
import { defaultValueForType } from './type-conversion';
|
|
10
6
|
|
|
@@ -32,9 +28,7 @@ export const EditEntryDialog = ({
|
|
|
32
28
|
const [currentValue, setCurrentValue] = useState<StorageEntryValue | null>(
|
|
33
29
|
defaultValueForType('string'),
|
|
34
30
|
);
|
|
35
|
-
const
|
|
36
|
-
null,
|
|
37
|
-
);
|
|
31
|
+
const confirm = useConfirmDialog();
|
|
38
32
|
|
|
39
33
|
useEffect(() => {
|
|
40
34
|
if (entry && isOpen) {
|
|
@@ -54,13 +48,14 @@ export const EditEntryDialog = ({
|
|
|
54
48
|
onClose();
|
|
55
49
|
};
|
|
56
50
|
|
|
57
|
-
const handleSave = () => {
|
|
51
|
+
const handleSave = async () => {
|
|
58
52
|
if (!entry || currentValue === null) return;
|
|
59
53
|
|
|
60
54
|
if (!isCurrentTypeSupported) {
|
|
61
|
-
|
|
55
|
+
await confirm({
|
|
56
|
+
variant: 'alert',
|
|
62
57
|
title: 'Unsupported Type',
|
|
63
|
-
|
|
58
|
+
description: 'This storage does not support the selected type.',
|
|
64
59
|
});
|
|
65
60
|
return;
|
|
66
61
|
}
|
|
@@ -71,7 +66,7 @@ export const EditEntryDialog = ({
|
|
|
71
66
|
|
|
72
67
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
73
68
|
if (event.key === 'Enter' && currentType !== 'buffer') {
|
|
74
|
-
handleSave();
|
|
69
|
+
void handleSave();
|
|
75
70
|
}
|
|
76
71
|
};
|
|
77
72
|
|
|
@@ -85,71 +80,57 @@ export const EditEntryDialog = ({
|
|
|
85
80
|
const isSaveDisabled = !isCurrentTypeSupported || currentValue === null;
|
|
86
81
|
|
|
87
82
|
return (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
>
|
|
95
|
-
<Dialog.
|
|
96
|
-
<Dialog.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
<Field>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
83
|
+
<Dialog
|
|
84
|
+
open={isOpen}
|
|
85
|
+
onOpenChange={(open) => {
|
|
86
|
+
if (!open) resetAndClose();
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<Dialog.Content onKeyDown={handleKeyDown}>
|
|
90
|
+
<Dialog.Header>
|
|
91
|
+
<Dialog.Title>Edit Entry</Dialog.Title>
|
|
92
|
+
</Dialog.Header>
|
|
93
|
+
|
|
94
|
+
<div className="flex flex-col gap-4">
|
|
95
|
+
<Field>
|
|
96
|
+
<Field.Label>Key</Field.Label>
|
|
97
|
+
<div className="h-8 w-full truncate rounded-md border border-input bg-muted px-3 py-1.5 font-mono text-sm text-foreground">
|
|
98
|
+
{entryForDisplay.key}
|
|
99
|
+
</div>
|
|
100
|
+
<Field.Description>Key cannot be changed during editing</Field.Description>
|
|
101
|
+
</Field>
|
|
102
|
+
|
|
103
|
+
<Field>
|
|
104
|
+
<Field.Label htmlFor="edit-entry-value">Value</Field.Label>
|
|
105
|
+
<TypedValueEditor
|
|
106
|
+
key={entryForDisplay.key}
|
|
107
|
+
supportedTypes={supportedTypes}
|
|
108
|
+
type={currentType}
|
|
109
|
+
value={currentValue}
|
|
110
|
+
onChange={(nextType, nextValue) => {
|
|
111
|
+
setCurrentType(nextType);
|
|
112
|
+
setCurrentValue(nextValue);
|
|
113
|
+
}}
|
|
114
|
+
inputId="edit-entry-value"
|
|
115
|
+
autoFocus
|
|
116
|
+
/>
|
|
117
|
+
{!isCurrentTypeSupported && (
|
|
118
|
+
<Field.Description className="text-danger">
|
|
119
|
+
This storage does not support {currentType} values.
|
|
108
120
|
</Field.Description>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
autoFocus
|
|
124
|
-
/>
|
|
125
|
-
{!isCurrentTypeSupported && (
|
|
126
|
-
<Field.Description className="text-destructive">
|
|
127
|
-
This storage does not support {currentType} values.
|
|
128
|
-
</Field.Description>
|
|
129
|
-
)}
|
|
130
|
-
</Field>
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
<Dialog.Footer>
|
|
134
|
-
<Button variant="outline" onClick={resetAndClose}>
|
|
135
|
-
Cancel
|
|
136
|
-
</Button>
|
|
137
|
-
<Button onClick={handleSave} disabled={isSaveDisabled}>
|
|
138
|
-
Save Changes
|
|
139
|
-
</Button>
|
|
140
|
-
</Dialog.Footer>
|
|
141
|
-
</Dialog.Content>
|
|
142
|
-
</Dialog>
|
|
143
|
-
|
|
144
|
-
<ConfirmDialog
|
|
145
|
-
open={alert !== null}
|
|
146
|
-
onOpenChange={(open) => {
|
|
147
|
-
if (!open) setAlert(null);
|
|
148
|
-
}}
|
|
149
|
-
variant="alert"
|
|
150
|
-
title={alert?.title ?? ''}
|
|
151
|
-
description={alert?.message}
|
|
152
|
-
/>
|
|
153
|
-
</>
|
|
121
|
+
)}
|
|
122
|
+
</Field>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<Dialog.Footer>
|
|
126
|
+
<Button tone="neutral" variant="outline" onClick={resetAndClose}>
|
|
127
|
+
Cancel
|
|
128
|
+
</Button>
|
|
129
|
+
<Button onClick={() => void handleSave()} disabled={isSaveDisabled}>
|
|
130
|
+
Save Changes
|
|
131
|
+
</Button>
|
|
132
|
+
</Dialog.Footer>
|
|
133
|
+
</Dialog.Content>
|
|
134
|
+
</Dialog>
|
|
154
135
|
);
|
|
155
136
|
};
|
|
@@ -10,12 +10,7 @@ const TYPE_LABELS: Record<StorageEntryType, string> = {
|
|
|
10
10
|
|
|
11
11
|
// Visual ordering of the pills. Keep `string` first because it's the
|
|
12
12
|
// most common landing type after a fresh read on an MMKV key.
|
|
13
|
-
const TYPE_ORDER: StorageEntryType[] = [
|
|
14
|
-
'string',
|
|
15
|
-
'number',
|
|
16
|
-
'boolean',
|
|
17
|
-
'buffer',
|
|
18
|
-
];
|
|
13
|
+
const TYPE_ORDER: StorageEntryType[] = ['string', 'number', 'boolean', 'buffer'];
|
|
19
14
|
|
|
20
15
|
export type EditorSwitcherProps = {
|
|
21
16
|
supportedTypes: StorageEntryType[];
|
|
@@ -23,11 +18,7 @@ export type EditorSwitcherProps = {
|
|
|
23
18
|
onChange: (type: StorageEntryType) => void;
|
|
24
19
|
};
|
|
25
20
|
|
|
26
|
-
export const EditorSwitcher = ({
|
|
27
|
-
supportedTypes,
|
|
28
|
-
value,
|
|
29
|
-
onChange,
|
|
30
|
-
}: EditorSwitcherProps) => {
|
|
21
|
+
export const EditorSwitcher = ({ supportedTypes, value, onChange }: EditorSwitcherProps) => {
|
|
31
22
|
const available = TYPE_ORDER.filter((type) => supportedTypes.includes(type));
|
|
32
23
|
|
|
33
24
|
// Adaptive hide: the switcher is meaningless when the backend only
|