@rozenite/storage-plugin 1.8.1 → 1.10.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 +34 -0
- package/README.md +23 -0
- package/dist/devtools/assets/panel-Bm-SWF7d.js +33 -0
- package/dist/devtools/assets/panel-DIqI4WSp.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.cjs +1 -1
- package/dist/react-native/chunks/index.require.js +43 -57
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +253 -191
- package/dist/react-native/index.d.ts +29 -6
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.d.ts +1 -0
- package/package.json +9 -6
- package/src/react-native/__tests__/import.test.ts +182 -0
- package/src/react-native/adapters/__tests__/mmkv.test.ts +436 -0
- package/src/react-native/adapters/mmkv.ts +65 -39
- package/src/react-native/import.ts +67 -0
- package/src/react-native/storage-view.ts +16 -8
- package/src/react-native/useRozeniteStoragePlugin.ts +61 -36
- package/src/shared/__tests__/snapshot.test.ts +361 -0
- package/src/shared/messaging.ts +25 -1
- package/src/shared/snapshot.ts +276 -0
- package/src/shared/types.ts +1 -0
- package/src/ui/__tests__/binary-value-editor-state.test.ts +199 -0
- package/src/ui/__tests__/binary.test.ts +251 -0
- package/src/ui/__tests__/type-conversion.test.ts +123 -0
- package/src/ui/add-entry-dialog.tsx +83 -173
- package/src/ui/binary-value-editor-state.ts +125 -0
- package/src/ui/binary-value-editor.tsx +168 -0
- package/src/ui/binary.ts +123 -0
- package/src/ui/edit-entry-dialog.tsx +64 -161
- package/src/ui/editable-table.tsx +12 -10
- package/src/ui/editor-switcher.tsx +62 -0
- package/src/ui/entry-detail-dialog.tsx +14 -6
- package/src/ui/import-dialog.tsx +261 -0
- package/src/ui/panel.tsx +257 -57
- package/src/ui/type-conversion.ts +105 -0
- package/src/ui/typed-value-editor.tsx +96 -0
- package/src/ui/utils.ts +30 -0
- package/dist/devtools/assets/panel-DMhXYHH4.css +0 -1
- package/dist/devtools/assets/panel-eGOuOVos.js +0 -22
- package/src/react-native/is-garbled.ts +0 -17
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { AlertTriangle, CheckCircle2, Loader2, X, XCircle } from 'lucide-react';
|
|
2
|
+
import type { StorageEntry, StorageTarget } from '../shared/types';
|
|
3
|
+
import type { ImportPreview, StorageSnapshotV1 } from '../shared/snapshot';
|
|
4
|
+
|
|
5
|
+
export type ImportFlightState =
|
|
6
|
+
| {
|
|
7
|
+
phase: 'preview';
|
|
8
|
+
target: StorageTarget;
|
|
9
|
+
targetLabel: string;
|
|
10
|
+
snapshot: StorageSnapshotV1;
|
|
11
|
+
preview: ImportPreview;
|
|
12
|
+
entriesToWrite: StorageEntry[];
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
phase: 'importing';
|
|
16
|
+
target: StorageTarget;
|
|
17
|
+
total: number;
|
|
18
|
+
written: number;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
phase: 'result';
|
|
22
|
+
ok: true;
|
|
23
|
+
written: number;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
phase: 'result';
|
|
27
|
+
ok: false;
|
|
28
|
+
written: number;
|
|
29
|
+
total: number;
|
|
30
|
+
failedKey?: string;
|
|
31
|
+
error: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ImportDialogProps = {
|
|
35
|
+
state: ImportFlightState | null;
|
|
36
|
+
onApply: () => void;
|
|
37
|
+
onCancel: () => void;
|
|
38
|
+
onClose: () => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const KeyList = ({ title, items }: { title: string; items: string[] }) => {
|
|
42
|
+
if (items.length === 0) return null;
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<div className="text-xs font-medium text-gray-300">
|
|
46
|
+
{title} ({items.length})
|
|
47
|
+
</div>
|
|
48
|
+
<div className="mt-1 max-h-24 overflow-auto rounded bg-gray-900 px-2 py-1 font-mono text-xs text-gray-200">
|
|
49
|
+
{items.slice(0, 50).join(', ')}
|
|
50
|
+
{items.length > 50 ? `, +${items.length - 50} more` : ''}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const PreviewBody = ({
|
|
57
|
+
state,
|
|
58
|
+
onApply,
|
|
59
|
+
onCancel,
|
|
60
|
+
}: {
|
|
61
|
+
state: Extract<ImportFlightState, { phase: 'preview' }>;
|
|
62
|
+
onApply: () => void;
|
|
63
|
+
onCancel: () => void;
|
|
64
|
+
}) => {
|
|
65
|
+
const { snapshot, preview, entriesToWrite, targetLabel } = state;
|
|
66
|
+
const hasUnsupported = preview.unsupportedTypes.length > 0;
|
|
67
|
+
const sourceLabel = `${snapshot.storage.adapterName} / ${snapshot.storage.storageName}`;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<div className="space-y-3">
|
|
72
|
+
{preview.metadataMismatch && (
|
|
73
|
+
<div className="flex items-start gap-2 rounded border border-yellow-700 bg-yellow-900/30 p-2 text-xs text-yellow-200">
|
|
74
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
|
75
|
+
<div>
|
|
76
|
+
This file was exported from <strong>{sourceLabel}</strong>. You
|
|
77
|
+
are importing into <strong>{targetLabel}</strong>.
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{hasUnsupported && (
|
|
83
|
+
<div className="flex items-start gap-2 rounded border border-red-700 bg-red-900/30 p-2 text-xs text-red-200">
|
|
84
|
+
<XCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
|
85
|
+
<div>
|
|
86
|
+
{preview.unsupportedTypes.length}{' '}
|
|
87
|
+
{preview.unsupportedTypes.length === 1
|
|
88
|
+
? 'entry has a type'
|
|
89
|
+
: 'entries have types'}{' '}
|
|
90
|
+
not supported by this storage. Remove them from the file and try
|
|
91
|
+
again.
|
|
92
|
+
<div className="mt-1 max-h-20 overflow-auto rounded bg-gray-900 px-2 py-1 font-mono text-xs text-red-100">
|
|
93
|
+
{preview.unsupportedTypes
|
|
94
|
+
.map((u) => `${u.key} (${u.type})`)
|
|
95
|
+
.join(', ')}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
102
|
+
<div className="rounded bg-gray-900 p-2">
|
|
103
|
+
<div className="text-gray-400">New</div>
|
|
104
|
+
<div className="text-base font-semibold text-green-400">
|
|
105
|
+
{preview.newKeys.length}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="rounded bg-gray-900 p-2">
|
|
109
|
+
<div className="text-gray-400">Overwrite</div>
|
|
110
|
+
<div className="text-base font-semibold text-yellow-400">
|
|
111
|
+
{preview.overwriteKeys.length}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="rounded bg-gray-900 p-2">
|
|
115
|
+
<div className="text-gray-400">Skipped</div>
|
|
116
|
+
<div className="text-base font-semibold text-gray-400">
|
|
117
|
+
{preview.skippedKeys.length}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<KeyList title="New keys" items={preview.newKeys} />
|
|
123
|
+
<KeyList title="Overwrite keys" items={preview.overwriteKeys} />
|
|
124
|
+
<KeyList
|
|
125
|
+
title="Skipped (filtered by storage)"
|
|
126
|
+
items={preview.skippedKeys.map((s) => s.key)}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="mt-6 flex items-center justify-end gap-2">
|
|
131
|
+
<button
|
|
132
|
+
onClick={onCancel}
|
|
133
|
+
className="rounded px-4 py-2 text-sm text-gray-300 transition-colors hover:bg-gray-700 hover:text-white"
|
|
134
|
+
>
|
|
135
|
+
Cancel
|
|
136
|
+
</button>
|
|
137
|
+
<button
|
|
138
|
+
onClick={onApply}
|
|
139
|
+
disabled={hasUnsupported}
|
|
140
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-600"
|
|
141
|
+
>
|
|
142
|
+
Apply ({entriesToWrite.length})
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const ImportingBody = ({
|
|
150
|
+
state,
|
|
151
|
+
}: {
|
|
152
|
+
state: Extract<ImportFlightState, { phase: 'importing' }>;
|
|
153
|
+
}) => (
|
|
154
|
+
<div className="flex flex-col items-center gap-3 py-6">
|
|
155
|
+
<Loader2 className="h-6 w-6 animate-spin text-blue-400" />
|
|
156
|
+
<div className="text-sm text-gray-200">
|
|
157
|
+
Importing… {state.written} / {state.total}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const ResultBody = ({
|
|
163
|
+
state,
|
|
164
|
+
onClose,
|
|
165
|
+
}: {
|
|
166
|
+
state: Extract<ImportFlightState, { phase: 'result' }>;
|
|
167
|
+
onClose: () => void;
|
|
168
|
+
}) => (
|
|
169
|
+
<>
|
|
170
|
+
{state.ok ? (
|
|
171
|
+
<div className="flex flex-col items-center gap-3 py-4">
|
|
172
|
+
<CheckCircle2 className="h-8 w-8 text-green-400" />
|
|
173
|
+
<div className="text-sm text-gray-200">
|
|
174
|
+
Imported {state.written} {state.written === 1 ? 'entry' : 'entries'}.
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
<div className="space-y-2 py-2">
|
|
179
|
+
<div className="flex items-center gap-2 text-red-300">
|
|
180
|
+
<XCircle className="h-5 w-5" />
|
|
181
|
+
<div className="text-sm font-medium">
|
|
182
|
+
Import failed after {state.written} of {state.total}.
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
{state.failedKey && (
|
|
186
|
+
<div className="text-xs text-gray-300">
|
|
187
|
+
Failed at key:{' '}
|
|
188
|
+
<span className="font-mono text-gray-100">{state.failedKey}</span>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
<div className="rounded bg-gray-900 px-2 py-1 font-mono text-xs text-red-200">
|
|
192
|
+
{state.error}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
<div className="mt-4 flex items-center justify-end">
|
|
198
|
+
<button
|
|
199
|
+
onClick={onClose}
|
|
200
|
+
autoFocus
|
|
201
|
+
className="rounded bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
|
|
202
|
+
>
|
|
203
|
+
Close
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
export const ImportDialog = ({
|
|
210
|
+
state,
|
|
211
|
+
onApply,
|
|
212
|
+
onCancel,
|
|
213
|
+
onClose,
|
|
214
|
+
}: ImportDialogProps) => {
|
|
215
|
+
if (!state) return null;
|
|
216
|
+
|
|
217
|
+
const title =
|
|
218
|
+
state.phase === 'preview'
|
|
219
|
+
? 'Import snapshot'
|
|
220
|
+
: state.phase === 'importing'
|
|
221
|
+
? 'Importing…'
|
|
222
|
+
: state.ok
|
|
223
|
+
? 'Import complete'
|
|
224
|
+
: 'Import failed';
|
|
225
|
+
|
|
226
|
+
// Importing phase: dialog is locked; only the success/failure transition closes it.
|
|
227
|
+
const isDismissible = state.phase !== 'importing';
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
|
232
|
+
onClick={isDismissible ? onClose : undefined}
|
|
233
|
+
>
|
|
234
|
+
<div
|
|
235
|
+
className="mx-4 w-[28rem] max-w-full rounded-lg bg-gray-800 p-6"
|
|
236
|
+
onClick={(event) => event.stopPropagation()}
|
|
237
|
+
>
|
|
238
|
+
<div className="mb-4 flex items-center justify-between">
|
|
239
|
+
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
|
|
240
|
+
{isDismissible && (
|
|
241
|
+
<button
|
|
242
|
+
onClick={onClose}
|
|
243
|
+
className="rounded p-1 text-gray-400 transition-colors hover:bg-gray-700 hover:text-gray-200"
|
|
244
|
+
title="Close dialog"
|
|
245
|
+
>
|
|
246
|
+
<X className="h-4 w-4" />
|
|
247
|
+
</button>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{state.phase === 'preview' && (
|
|
252
|
+
<PreviewBody state={state} onApply={onApply} onCancel={onCancel} />
|
|
253
|
+
)}
|
|
254
|
+
{state.phase === 'importing' && <ImportingBody state={state} />}
|
|
255
|
+
{state.phase === 'result' && (
|
|
256
|
+
<ResultBody state={state} onClose={onClose} />
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
};
|