@rozenite/storage-plugin 1.4.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 +28 -0
- package/LICENSE +20 -0
- package/README.md +77 -0
- package/dist/assets/panel-C7usUotG.js +22 -0
- package/dist/assets/panel-DMhXYHH4.css +1 -0
- package/dist/panel.html +31 -0
- package/dist/react-native.cjs +1 -0
- package/dist/react-native.d.ts +4 -0
- package/dist/react-native.js +200 -0
- package/dist/rozenite.config.d.ts +7 -0
- package/dist/rozenite.json +1 -0
- package/dist/src/react-native/adapters/async-storage.d.ts +33 -0
- package/dist/src/react-native/adapters/index.d.ts +6 -0
- package/dist/src/react-native/adapters/mmkv.d.ts +13 -0
- package/dist/src/react-native/adapters/secure-storage.d.ts +18 -0
- package/dist/src/react-native/is-garbled.d.ts +1 -0
- package/dist/src/react-native/storage-view.d.ts +19 -0
- package/dist/src/react-native/useRozeniteStoragePlugin.d.ts +6 -0
- package/dist/src/shared/messaging.d.ts +29 -0
- package/dist/src/shared/types.d.ts +60 -0
- package/dist/src/ui/add-entry-dialog.d.ts +9 -0
- package/dist/src/ui/confirm-dialog.d.ts +11 -0
- package/dist/src/ui/edit-entry-dialog.d.ts +9 -0
- package/dist/src/ui/editable-table.d.ts +10 -0
- package/dist/src/ui/entry-detail-dialog.d.ts +8 -0
- package/dist/src/ui/panel.d.ts +1 -0
- package/dist/types.cjs +1 -0
- package/dist/types.js +11 -0
- package/dist/useRozeniteStoragePlugin.cjs +1 -0
- package/dist/useRozeniteStoragePlugin.js +212 -0
- package/package.json +59 -0
- package/postcss.config.js +6 -0
- package/react-native.ts +40 -0
- package/rozenite.config.ts +8 -0
- package/src/css-modules.d.ts +4 -0
- package/src/react-native/adapters/async-storage.ts +115 -0
- package/src/react-native/adapters/index.ts +19 -0
- package/src/react-native/adapters/mmkv.ts +187 -0
- package/src/react-native/adapters/secure-storage.ts +76 -0
- package/src/react-native/is-garbled.ts +17 -0
- package/src/react-native/storage-view.ts +245 -0
- package/src/react-native/useRozeniteStoragePlugin.ts +162 -0
- package/src/shared/messaging.ts +37 -0
- package/src/shared/types.ts +66 -0
- package/src/ui/add-entry-dialog.tsx +310 -0
- package/src/ui/confirm-dialog.tsx +100 -0
- package/src/ui/edit-entry-dialog.tsx +280 -0
- package/src/ui/editable-table.tsx +298 -0
- package/src/ui/entry-detail-dialog.tsx +220 -0
- package/src/ui/globals.css +123 -0
- package/src/ui/panel.tsx +442 -0
- package/tailwind.config.ts +94 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import type {
|
|
4
|
+
StorageEntry,
|
|
5
|
+
StorageEntryType,
|
|
6
|
+
StorageEntryValue,
|
|
7
|
+
} from '../shared/types';
|
|
8
|
+
import { ConfirmDialog } from './confirm-dialog';
|
|
9
|
+
|
|
10
|
+
const TYPE_OPTIONS: Array<{ value: StorageEntryType; label: string }> = [
|
|
11
|
+
{ value: 'string', label: 'String' },
|
|
12
|
+
{ value: 'number', label: 'Number' },
|
|
13
|
+
{ value: 'boolean', label: 'Boolean' },
|
|
14
|
+
{ value: 'buffer', label: 'Buffer (Array)' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export type AddEntryDialogProps = {
|
|
18
|
+
isOpen: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
onAddEntry: (entry: StorageEntry) => void;
|
|
21
|
+
existingKeys: string[];
|
|
22
|
+
supportedTypes: StorageEntryType[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const AddEntryDialog = ({
|
|
26
|
+
isOpen,
|
|
27
|
+
onClose,
|
|
28
|
+
onAddEntry,
|
|
29
|
+
existingKeys,
|
|
30
|
+
supportedTypes,
|
|
31
|
+
}: AddEntryDialogProps) => {
|
|
32
|
+
const [newEntryKey, setNewEntryKey] = useState('');
|
|
33
|
+
const [newEntryType, setNewEntryType] = useState<StorageEntryType>('string');
|
|
34
|
+
const [newEntryValue, setNewEntryValue] = useState('');
|
|
35
|
+
const [confirmDialog, setConfirmDialog] = useState<{
|
|
36
|
+
isOpen: boolean;
|
|
37
|
+
title: string;
|
|
38
|
+
message: string;
|
|
39
|
+
type: 'confirm' | 'alert';
|
|
40
|
+
onConfirm?: () => void;
|
|
41
|
+
}>({ isOpen: false, title: '', message: '', type: 'alert' });
|
|
42
|
+
|
|
43
|
+
const enabledTypes = useMemo(
|
|
44
|
+
() => TYPE_OPTIONS.filter((option) => supportedTypes.includes(option.value)),
|
|
45
|
+
[supportedTypes]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const firstSupportedType = enabledTypes[0]?.value;
|
|
49
|
+
|
|
50
|
+
const selectedTypeSupported = supportedTypes.includes(newEntryType);
|
|
51
|
+
|
|
52
|
+
const resetForm = () => {
|
|
53
|
+
setNewEntryKey('');
|
|
54
|
+
setNewEntryType(firstSupportedType ?? 'string');
|
|
55
|
+
setNewEntryValue('');
|
|
56
|
+
onClose();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleAddEntry = () => {
|
|
60
|
+
if (!newEntryKey.trim()) return;
|
|
61
|
+
|
|
62
|
+
if (!selectedTypeSupported) {
|
|
63
|
+
setConfirmDialog({
|
|
64
|
+
isOpen: true,
|
|
65
|
+
title: 'Unsupported Type',
|
|
66
|
+
message: 'Selected type is not supported by this storage.',
|
|
67
|
+
type: 'alert',
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (existingKeys.includes(newEntryKey)) {
|
|
73
|
+
setConfirmDialog({
|
|
74
|
+
isOpen: true,
|
|
75
|
+
title: 'Key Already Exists',
|
|
76
|
+
message: 'An entry with this key already exists.',
|
|
77
|
+
type: 'alert',
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let parsedValue: StorageEntryValue;
|
|
83
|
+
try {
|
|
84
|
+
switch (newEntryType) {
|
|
85
|
+
case 'string':
|
|
86
|
+
parsedValue = newEntryValue;
|
|
87
|
+
break;
|
|
88
|
+
case 'number':
|
|
89
|
+
parsedValue = Number(newEntryValue);
|
|
90
|
+
if (Number.isNaN(parsedValue)) {
|
|
91
|
+
throw new Error('Invalid number');
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'boolean':
|
|
95
|
+
if (newEntryValue !== 'true' && newEntryValue !== 'false') {
|
|
96
|
+
throw new Error('Boolean value must be true or false');
|
|
97
|
+
}
|
|
98
|
+
parsedValue = newEntryValue === 'true';
|
|
99
|
+
break;
|
|
100
|
+
case 'buffer':
|
|
101
|
+
parsedValue = JSON.parse(newEntryValue);
|
|
102
|
+
if (
|
|
103
|
+
!Array.isArray(parsedValue) ||
|
|
104
|
+
!parsedValue.every((value) => typeof value === 'number')
|
|
105
|
+
) {
|
|
106
|
+
throw new Error('Buffer must be an array of numbers');
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
throw new Error('Invalid type');
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
setConfirmDialog({
|
|
114
|
+
isOpen: true,
|
|
115
|
+
title: 'Invalid Value',
|
|
116
|
+
message: `Invalid value for ${newEntryType}: ${
|
|
117
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
118
|
+
}`,
|
|
119
|
+
type: 'alert',
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let entry: StorageEntry;
|
|
125
|
+
if (newEntryType === 'string') {
|
|
126
|
+
entry = { key: newEntryKey, type: 'string', value: parsedValue as string };
|
|
127
|
+
} else if (newEntryType === 'number') {
|
|
128
|
+
entry = { key: newEntryKey, type: 'number', value: parsedValue as number };
|
|
129
|
+
} else if (newEntryType === 'boolean') {
|
|
130
|
+
entry = { key: newEntryKey, type: 'boolean', value: parsedValue as boolean };
|
|
131
|
+
} else {
|
|
132
|
+
entry = { key: newEntryKey, type: 'buffer', value: parsedValue as number[] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onAddEntry(entry);
|
|
136
|
+
|
|
137
|
+
resetForm();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
141
|
+
if (event.key === 'Escape') {
|
|
142
|
+
resetForm();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (event.key === 'Enter' && newEntryKey.trim() && newEntryValue.trim()) {
|
|
147
|
+
handleAddEntry();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (!isOpen) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
|
158
|
+
onClick={resetForm}
|
|
159
|
+
>
|
|
160
|
+
<div
|
|
161
|
+
className="bg-gray-800 rounded-lg p-6 w-96 max-w-full mx-4"
|
|
162
|
+
onClick={(event) => event.stopPropagation()}
|
|
163
|
+
onKeyDown={handleKeyDown}
|
|
164
|
+
>
|
|
165
|
+
<div className="flex items-center justify-between mb-4">
|
|
166
|
+
<h2 className="text-lg font-semibold text-gray-100">Add New Entry</h2>
|
|
167
|
+
<button
|
|
168
|
+
onClick={resetForm}
|
|
169
|
+
className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
|
|
170
|
+
title="Close dialog"
|
|
171
|
+
>
|
|
172
|
+
<X className="h-4 w-4" />
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="space-y-4">
|
|
177
|
+
<div>
|
|
178
|
+
<label
|
|
179
|
+
htmlFor="new-entry-key"
|
|
180
|
+
className="block text-sm font-medium text-gray-200 mb-1"
|
|
181
|
+
>
|
|
182
|
+
Key
|
|
183
|
+
</label>
|
|
184
|
+
<input
|
|
185
|
+
id="new-entry-key"
|
|
186
|
+
type="text"
|
|
187
|
+
value={newEntryKey}
|
|
188
|
+
onChange={(event) => setNewEntryKey(event.target.value)}
|
|
189
|
+
placeholder="Enter key name"
|
|
190
|
+
className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
191
|
+
autoFocus
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div>
|
|
196
|
+
<label
|
|
197
|
+
htmlFor="new-entry-type"
|
|
198
|
+
className="block text-sm font-medium text-gray-200 mb-1"
|
|
199
|
+
>
|
|
200
|
+
Type
|
|
201
|
+
</label>
|
|
202
|
+
<select
|
|
203
|
+
id="new-entry-type"
|
|
204
|
+
value={newEntryType}
|
|
205
|
+
onChange={(event) => {
|
|
206
|
+
setNewEntryType(event.target.value as StorageEntryType);
|
|
207
|
+
setNewEntryValue('');
|
|
208
|
+
}}
|
|
209
|
+
className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
210
|
+
>
|
|
211
|
+
{TYPE_OPTIONS.map((option) => {
|
|
212
|
+
const isSupported = supportedTypes.includes(option.value);
|
|
213
|
+
return (
|
|
214
|
+
<option
|
|
215
|
+
key={option.value}
|
|
216
|
+
value={option.value}
|
|
217
|
+
disabled={!isSupported}
|
|
218
|
+
title={
|
|
219
|
+
isSupported ? option.label : 'Not supported by this storage'
|
|
220
|
+
}
|
|
221
|
+
>
|
|
222
|
+
{option.label}
|
|
223
|
+
{!isSupported ? ' (Not supported)' : ''}
|
|
224
|
+
</option>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
</select>
|
|
228
|
+
{!selectedTypeSupported && (
|
|
229
|
+
<p className="text-xs text-amber-400 mt-1">
|
|
230
|
+
Selected type is not supported by this storage.
|
|
231
|
+
</p>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div>
|
|
236
|
+
<label
|
|
237
|
+
htmlFor="new-entry-value"
|
|
238
|
+
className="block text-sm font-medium text-gray-200 mb-1"
|
|
239
|
+
>
|
|
240
|
+
Value
|
|
241
|
+
</label>
|
|
242
|
+
{newEntryType === 'boolean' ? (
|
|
243
|
+
<select
|
|
244
|
+
id="new-entry-value"
|
|
245
|
+
value={newEntryValue}
|
|
246
|
+
onChange={(event) => setNewEntryValue(event.target.value)}
|
|
247
|
+
className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
248
|
+
>
|
|
249
|
+
<option value="">Select value</option>
|
|
250
|
+
<option value="true">true</option>
|
|
251
|
+
<option value="false">false</option>
|
|
252
|
+
</select>
|
|
253
|
+
) : (
|
|
254
|
+
<input
|
|
255
|
+
id="new-entry-value"
|
|
256
|
+
type={newEntryType === 'number' ? 'number' : 'text'}
|
|
257
|
+
value={newEntryValue}
|
|
258
|
+
onChange={(event) => setNewEntryValue(event.target.value)}
|
|
259
|
+
placeholder={
|
|
260
|
+
newEntryType === 'string'
|
|
261
|
+
? 'Enter string value'
|
|
262
|
+
: newEntryType === 'number'
|
|
263
|
+
? 'Enter number value'
|
|
264
|
+
: newEntryType === 'buffer'
|
|
265
|
+
? 'Enter array as JSON, e.g., [1, 2, 3]'
|
|
266
|
+
: 'Enter value'
|
|
267
|
+
}
|
|
268
|
+
className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
271
|
+
{newEntryType === 'buffer' && (
|
|
272
|
+
<p className="text-xs text-gray-400 mt-1">
|
|
273
|
+
Enter as JSON array of numbers, e.g., [1, 2, 3, 255]
|
|
274
|
+
</p>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="flex items-center justify-end gap-2 mt-6">
|
|
280
|
+
<button
|
|
281
|
+
onClick={resetForm}
|
|
282
|
+
className="px-4 py-2 text-sm text-gray-300 hover:text-white hover:bg-gray-700 rounded transition-colors"
|
|
283
|
+
>
|
|
284
|
+
Cancel
|
|
285
|
+
</button>
|
|
286
|
+
<button
|
|
287
|
+
onClick={handleAddEntry}
|
|
288
|
+
disabled={!newEntryKey.trim() || !newEntryValue.trim() || !selectedTypeSupported}
|
|
289
|
+
className="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded transition-colors"
|
|
290
|
+
>
|
|
291
|
+
Add Entry
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<ConfirmDialog
|
|
297
|
+
isOpen={confirmDialog.isOpen}
|
|
298
|
+
onClose={() => setConfirmDialog((previous) => ({ ...previous, isOpen: false }))}
|
|
299
|
+
onConfirm={() => {
|
|
300
|
+
if (confirmDialog.onConfirm) {
|
|
301
|
+
confirmDialog.onConfirm();
|
|
302
|
+
}
|
|
303
|
+
}}
|
|
304
|
+
title={confirmDialog.title}
|
|
305
|
+
message={confirmDialog.message}
|
|
306
|
+
type={confirmDialog.type}
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { X, AlertTriangle, Info } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
export type ConfirmDialogProps = {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
onConfirm: () => void;
|
|
7
|
+
title: string;
|
|
8
|
+
message: string;
|
|
9
|
+
type?: 'confirm' | 'alert';
|
|
10
|
+
confirmText?: string;
|
|
11
|
+
cancelText?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const ConfirmDialog = ({
|
|
15
|
+
isOpen,
|
|
16
|
+
onClose,
|
|
17
|
+
onConfirm,
|
|
18
|
+
title,
|
|
19
|
+
message,
|
|
20
|
+
type = 'confirm',
|
|
21
|
+
confirmText = 'Confirm',
|
|
22
|
+
cancelText = 'Cancel',
|
|
23
|
+
}: ConfirmDialogProps) => {
|
|
24
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
25
|
+
if (e.key === 'Escape') {
|
|
26
|
+
onClose();
|
|
27
|
+
} else if (e.key === 'Enter') {
|
|
28
|
+
onConfirm();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleConfirm = () => {
|
|
33
|
+
onConfirm();
|
|
34
|
+
onClose();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleCancel = () => {
|
|
38
|
+
onClose();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (!isOpen) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
|
46
|
+
onClick={handleCancel}
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
className="bg-gray-800 rounded-lg p-6 w-96 max-w-full mx-4"
|
|
50
|
+
onClick={(e) => e.stopPropagation()}
|
|
51
|
+
onKeyDown={handleKeyDown}
|
|
52
|
+
>
|
|
53
|
+
<div className="flex items-center justify-between mb-4">
|
|
54
|
+
<div className="flex items-center gap-2">
|
|
55
|
+
{type === 'confirm' ? (
|
|
56
|
+
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
|
57
|
+
) : (
|
|
58
|
+
<Info className="h-5 w-5 text-blue-500" />
|
|
59
|
+
)}
|
|
60
|
+
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
|
|
61
|
+
</div>
|
|
62
|
+
<button
|
|
63
|
+
onClick={handleCancel}
|
|
64
|
+
className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
|
|
65
|
+
title="Close dialog"
|
|
66
|
+
>
|
|
67
|
+
<X className="h-4 w-4" />
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="mb-6">
|
|
72
|
+
<p className="text-sm text-gray-300 leading-relaxed">{message}</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Dialog Actions */}
|
|
76
|
+
<div className="flex items-center justify-end gap-2">
|
|
77
|
+
{type === 'confirm' && (
|
|
78
|
+
<button
|
|
79
|
+
onClick={handleCancel}
|
|
80
|
+
className="px-4 py-2 text-sm text-gray-300 hover:text-white hover:bg-gray-700 rounded transition-colors"
|
|
81
|
+
>
|
|
82
|
+
{cancelText}
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
<button
|
|
86
|
+
onClick={handleConfirm}
|
|
87
|
+
className={`px-4 py-2 text-sm text-white rounded transition-colors ${
|
|
88
|
+
type === 'confirm'
|
|
89
|
+
? 'bg-red-600 hover:bg-red-700'
|
|
90
|
+
: 'bg-blue-600 hover:bg-blue-700'
|
|
91
|
+
}`}
|
|
92
|
+
autoFocus
|
|
93
|
+
>
|
|
94
|
+
{type === 'alert' ? 'OK' : confirmText}
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { X, Edit3 } from 'lucide-react';
|
|
3
|
+
import type {
|
|
4
|
+
StorageEntry,
|
|
5
|
+
StorageEntryType,
|
|
6
|
+
StorageEntryValue,
|
|
7
|
+
} from '../shared/types';
|
|
8
|
+
import { ConfirmDialog } from './confirm-dialog';
|
|
9
|
+
|
|
10
|
+
export type EditEntryDialogProps = {
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
onEditEntry: (key: string, newValue: StorageEntryValue) => void;
|
|
14
|
+
entry: StorageEntry | null;
|
|
15
|
+
supportedTypes: StorageEntryType[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const EditEntryDialog = ({
|
|
19
|
+
isOpen,
|
|
20
|
+
onClose,
|
|
21
|
+
onEditEntry,
|
|
22
|
+
entry,
|
|
23
|
+
supportedTypes,
|
|
24
|
+
}: EditEntryDialogProps) => {
|
|
25
|
+
const [editValue, setEditValue] = useState('');
|
|
26
|
+
const [confirmDialog, setConfirmDialog] = useState<{
|
|
27
|
+
isOpen: boolean;
|
|
28
|
+
title: string;
|
|
29
|
+
message: string;
|
|
30
|
+
type: 'confirm' | 'alert';
|
|
31
|
+
onConfirm?: () => void;
|
|
32
|
+
}>({ isOpen: false, title: '', message: '', type: 'alert' });
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (entry && isOpen) {
|
|
36
|
+
setEditValue(Array.isArray(entry.value) ? JSON.stringify(entry.value) : String(entry.value));
|
|
37
|
+
}
|
|
38
|
+
}, [entry, isOpen]);
|
|
39
|
+
|
|
40
|
+
const isTypeSupported = useMemo(
|
|
41
|
+
() => !!entry && supportedTypes.includes(entry.type),
|
|
42
|
+
[entry, supportedTypes]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const resetForm = () => {
|
|
46
|
+
setEditValue('');
|
|
47
|
+
onClose();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleEditEntry = () => {
|
|
51
|
+
if (!entry) return;
|
|
52
|
+
|
|
53
|
+
if (!isTypeSupported) {
|
|
54
|
+
setConfirmDialog({
|
|
55
|
+
isOpen: true,
|
|
56
|
+
title: 'Unsupported Type',
|
|
57
|
+
message: 'This storage does not support the current entry type.',
|
|
58
|
+
type: 'alert',
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let newValue: StorageEntryValue;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
switch (entry.type) {
|
|
67
|
+
case 'string':
|
|
68
|
+
newValue = editValue;
|
|
69
|
+
break;
|
|
70
|
+
case 'number':
|
|
71
|
+
newValue = Number(editValue);
|
|
72
|
+
if (Number.isNaN(newValue)) {
|
|
73
|
+
throw new Error('Invalid number');
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case 'boolean':
|
|
77
|
+
if (editValue !== 'true' && editValue !== 'false') {
|
|
78
|
+
throw new Error('Boolean value must be "true" or "false"');
|
|
79
|
+
}
|
|
80
|
+
newValue = editValue === 'true';
|
|
81
|
+
break;
|
|
82
|
+
case 'buffer':
|
|
83
|
+
newValue = JSON.parse(editValue);
|
|
84
|
+
if (!Array.isArray(newValue) || !newValue.every((v) => typeof v === 'number')) {
|
|
85
|
+
throw new Error('Buffer must be an array of numbers');
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
throw new Error('Invalid type');
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
setConfirmDialog({
|
|
93
|
+
isOpen: true,
|
|
94
|
+
title: 'Invalid Value',
|
|
95
|
+
message: `Invalid value for ${entry.type}: ${
|
|
96
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
97
|
+
}`,
|
|
98
|
+
type: 'alert',
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onEditEntry(entry.key, newValue);
|
|
104
|
+
resetForm();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
108
|
+
if (event.key === 'Escape') {
|
|
109
|
+
resetForm();
|
|
110
|
+
} else if (event.key === 'Enter' && editValue.trim()) {
|
|
111
|
+
handleEditEntry();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const getInputType = (type: StorageEntryType) => {
|
|
116
|
+
if (type === 'number') {
|
|
117
|
+
return 'number';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return 'text';
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const getPlaceholder = (type: StorageEntryType) => {
|
|
124
|
+
if (type === 'string') {
|
|
125
|
+
return 'Enter string value';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (type === 'number') {
|
|
129
|
+
return 'Enter number value';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (type === 'boolean') {
|
|
133
|
+
return 'Enter true or false';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return 'Enter array as JSON, e.g., [1, 2, 3]';
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const getTypeColorClass = (type: StorageEntryType) => {
|
|
140
|
+
if (type === 'string') {
|
|
141
|
+
return 'bg-green-600';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (type === 'number') {
|
|
145
|
+
return 'bg-blue-600';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (type === 'boolean') {
|
|
149
|
+
return 'bg-yellow-600';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return 'bg-purple-600';
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (!isOpen || !entry) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
|
162
|
+
onClick={resetForm}
|
|
163
|
+
>
|
|
164
|
+
<div
|
|
165
|
+
className="bg-gray-800 rounded-lg p-6 w-96 max-w-full mx-4"
|
|
166
|
+
onClick={(event) => event.stopPropagation()}
|
|
167
|
+
onKeyDown={handleKeyDown}
|
|
168
|
+
>
|
|
169
|
+
<div className="flex items-center justify-between mb-4">
|
|
170
|
+
<div className="flex items-center gap-2">
|
|
171
|
+
<Edit3 className="h-5 w-5 text-blue-400" />
|
|
172
|
+
<h2 className="text-lg font-semibold text-gray-100">Edit Entry</h2>
|
|
173
|
+
</div>
|
|
174
|
+
<button
|
|
175
|
+
onClick={resetForm}
|
|
176
|
+
className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
|
|
177
|
+
title="Close dialog"
|
|
178
|
+
>
|
|
179
|
+
<X className="h-4 w-4" />
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="space-y-4">
|
|
184
|
+
<div>
|
|
185
|
+
<label className="block text-sm font-medium text-gray-200 mb-1">Key</label>
|
|
186
|
+
<div className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 font-mono">
|
|
187
|
+
{entry.key}
|
|
188
|
+
</div>
|
|
189
|
+
<p className="text-xs text-gray-400 mt-1">Key cannot be changed during editing</p>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div>
|
|
193
|
+
<label className="block text-sm font-medium text-gray-200 mb-1">Type</label>
|
|
194
|
+
<div className="flex items-center">
|
|
195
|
+
<span
|
|
196
|
+
className={`px-2 py-1 text-xs font-medium rounded text-white ${getTypeColorClass(
|
|
197
|
+
entry.type
|
|
198
|
+
)}`}
|
|
199
|
+
>
|
|
200
|
+
{entry.type}
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
{!isTypeSupported ? (
|
|
204
|
+
<p className="text-xs text-amber-400 mt-1">
|
|
205
|
+
This storage does not support {entry.type} values.
|
|
206
|
+
</p>
|
|
207
|
+
) : (
|
|
208
|
+
<p className="text-xs text-gray-400 mt-1">Type cannot be changed during editing</p>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div>
|
|
213
|
+
<label
|
|
214
|
+
htmlFor="edit-entry-value"
|
|
215
|
+
className="block text-sm font-medium text-gray-200 mb-1"
|
|
216
|
+
>
|
|
217
|
+
Value
|
|
218
|
+
</label>
|
|
219
|
+
{entry.type === 'boolean' ? (
|
|
220
|
+
<select
|
|
221
|
+
id="edit-entry-value"
|
|
222
|
+
value={editValue}
|
|
223
|
+
onChange={(event) => setEditValue(event.target.value)}
|
|
224
|
+
className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
225
|
+
autoFocus
|
|
226
|
+
>
|
|
227
|
+
<option value="true">true</option>
|
|
228
|
+
<option value="false">false</option>
|
|
229
|
+
</select>
|
|
230
|
+
) : (
|
|
231
|
+
<input
|
|
232
|
+
id="edit-entry-value"
|
|
233
|
+
type={getInputType(entry.type)}
|
|
234
|
+
value={editValue}
|
|
235
|
+
onChange={(event) => setEditValue(event.target.value)}
|
|
236
|
+
placeholder={getPlaceholder(entry.type)}
|
|
237
|
+
className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
238
|
+
autoFocus
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
{entry.type === 'buffer' && (
|
|
242
|
+
<p className="text-xs text-gray-400 mt-1">
|
|
243
|
+
Enter as JSON array of numbers, e.g., [1, 2, 3, 255]
|
|
244
|
+
</p>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div className="flex items-center justify-end gap-2 mt-6">
|
|
250
|
+
<button
|
|
251
|
+
onClick={resetForm}
|
|
252
|
+
className="px-4 py-2 text-sm text-gray-300 hover:text-white hover:bg-gray-700 rounded transition-colors"
|
|
253
|
+
>
|
|
254
|
+
Cancel
|
|
255
|
+
</button>
|
|
256
|
+
<button
|
|
257
|
+
onClick={handleEditEntry}
|
|
258
|
+
disabled={!editValue.trim() || !isTypeSupported}
|
|
259
|
+
className="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded transition-colors"
|
|
260
|
+
>
|
|
261
|
+
Save Changes
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<ConfirmDialog
|
|
267
|
+
isOpen={confirmDialog.isOpen}
|
|
268
|
+
onClose={() => setConfirmDialog((previous) => ({ ...previous, isOpen: false }))}
|
|
269
|
+
onConfirm={() => {
|
|
270
|
+
if (confirmDialog.onConfirm) {
|
|
271
|
+
confirmDialog.onConfirm();
|
|
272
|
+
}
|
|
273
|
+
}}
|
|
274
|
+
title={confirmDialog.title}
|
|
275
|
+
message={confirmDialog.message}
|
|
276
|
+
type={confirmDialog.type}
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
};
|