@rozenite/sqlite-plugin 1.7.0-rc.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 +8 -0
- package/LICENSE +20 -0
- package/README.md +102 -0
- package/dist/devtools/assets/panel-B3paLkwG.js +82 -0
- package/dist/devtools/assets/panel-CIU0JBOs.css +1 -0
- package/dist/devtools/panel.html +31 -0
- package/dist/react-native/chunks/bridge-values.cjs +5 -0
- package/dist/react-native/chunks/bridge-values.js +258 -0
- package/dist/react-native/chunks/index.require.cjs +1 -0
- package/dist/react-native/chunks/index.require.js +118 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.cjs +1 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +189 -0
- package/dist/react-native/index.cjs +1 -0
- package/dist/react-native/index.d.ts +178 -0
- package/dist/react-native/index.js +16 -0
- package/dist/rozenite.json +1 -0
- package/package.json +83 -0
- package/postcss.config.js +6 -0
- package/react-native.ts +55 -0
- package/rozenite.config.ts +8 -0
- package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +94 -0
- package/src/react-native/adapters/expo-sqlite.ts +230 -0
- package/src/react-native/adapters/generic.ts +88 -0
- package/src/react-native/adapters/index.ts +9 -0
- package/src/react-native/sqlite-view.ts +24 -0
- package/src/react-native/useRozeniteSqlitePlugin.ts +262 -0
- package/src/shared/__tests__/bridge-values.test.ts +34 -0
- package/src/shared/__tests__/sql.test.ts +55 -0
- package/src/shared/bridge-values.ts +170 -0
- package/src/shared/protocol.ts +41 -0
- package/src/shared/sql.ts +420 -0
- package/src/shared/types.ts +81 -0
- package/src/ui/__tests__/sql-editor-utils.test.ts +135 -0
- package/src/ui/__tests__/sqlite-row-edit-value.test.ts +22 -0
- package/src/ui/__tests__/sqlite-row-mutations.test.ts +310 -0
- package/src/ui/__tests__/sqlite-table-column-order.test.ts +83 -0
- package/src/ui/__tests__/value-utils.test.tsx +12 -0
- package/src/ui/cell-detail-drawer.tsx +65 -0
- package/src/ui/globals.css +1415 -0
- package/src/ui/panel.tsx +2815 -0
- package/src/ui/query-result-table.tsx +199 -0
- package/src/ui/sql-editor-utils.ts +352 -0
- package/src/ui/sql-editor.tsx +509 -0
- package/src/ui/sqlite-data-table.tsx +296 -0
- package/src/ui/sqlite-introspection.ts +189 -0
- package/src/ui/sqlite-modal-controls.tsx +32 -0
- package/src/ui/sqlite-row-delete-modal.tsx +130 -0
- package/src/ui/sqlite-row-edit-modal.tsx +487 -0
- package/src/ui/sqlite-row-edit-value.ts +53 -0
- package/src/ui/sqlite-row-mutations.ts +246 -0
- package/src/ui/sqlite-table-column-order.ts +154 -0
- package/src/ui/use-sqlite-requests.ts +205 -0
- package/src/ui/utils.ts +107 -0
- package/src/ui/value-utils.tsx +162 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { Modal, useOverlayState } from '@heroui/react';
|
|
2
|
+
import { Pencil, Save } from 'lucide-react';
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import type { SqliteColumnInfo } from './sqlite-introspection';
|
|
5
|
+
import {
|
|
6
|
+
SqliteModalCloseButton,
|
|
7
|
+
sqlitePrimaryButtonClassName,
|
|
8
|
+
sqliteSecondaryButtonClassName,
|
|
9
|
+
} from './sqlite-modal-controls';
|
|
10
|
+
import {
|
|
11
|
+
canColumnBeNull,
|
|
12
|
+
getCompatibleValueKinds,
|
|
13
|
+
getEditableColumns,
|
|
14
|
+
getPrimaryKeyColumns,
|
|
15
|
+
} from './sqlite-row-mutations';
|
|
16
|
+
import {
|
|
17
|
+
parseEditableFieldValue,
|
|
18
|
+
type EditableFieldDraft,
|
|
19
|
+
type EditableValueKind,
|
|
20
|
+
} from './sqlite-row-edit-value';
|
|
21
|
+
import { getValueKind, getValuePreview } from './value-utils';
|
|
22
|
+
|
|
23
|
+
type SqliteRowEditModalProps = {
|
|
24
|
+
isOpen: boolean;
|
|
25
|
+
rowNumber: number;
|
|
26
|
+
entityName: string;
|
|
27
|
+
row: Record<string, unknown> | null;
|
|
28
|
+
columns: SqliteColumnInfo[];
|
|
29
|
+
onClose: () => void;
|
|
30
|
+
onSave: (nextValues: Record<string, unknown>) => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const stringifyDraftValue = (value: unknown, kind: EditableValueKind) => {
|
|
34
|
+
if (value === null) {
|
|
35
|
+
if (kind === 'boolean') {
|
|
36
|
+
return 'false';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (kind === 'blob-ish') {
|
|
40
|
+
return '[]';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (kind === 'json') {
|
|
44
|
+
return '{}';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
if (kind === 'blob-ish') {
|
|
52
|
+
return '[]';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (kind === 'json') {
|
|
56
|
+
return '{}';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return kind === 'boolean' ? 'false' : '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (kind === 'number' && typeof value === 'boolean') {
|
|
63
|
+
return value ? '1' : '0';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (kind === 'boolean' && typeof value === 'number') {
|
|
67
|
+
return value === 0 ? 'false' : 'true';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (kind === 'blob-ish' || kind === 'json') {
|
|
71
|
+
return JSON.stringify(value, null, 2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return String(value);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const createFieldDraft = (
|
|
78
|
+
column: SqliteColumnInfo,
|
|
79
|
+
value: unknown,
|
|
80
|
+
): EditableFieldDraft => {
|
|
81
|
+
const compatibleKinds = getCompatibleValueKinds(column, value);
|
|
82
|
+
const kind = compatibleKinds[0] ?? 'text';
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
kind,
|
|
86
|
+
rawValue: stringifyDraftValue(value, kind),
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const SqliteRowEditModal = ({
|
|
91
|
+
isOpen,
|
|
92
|
+
rowNumber,
|
|
93
|
+
entityName,
|
|
94
|
+
row,
|
|
95
|
+
columns,
|
|
96
|
+
onClose,
|
|
97
|
+
onSave,
|
|
98
|
+
}: SqliteRowEditModalProps) => {
|
|
99
|
+
const overlay = useOverlayState({
|
|
100
|
+
isOpen,
|
|
101
|
+
onOpenChange: (open: boolean) => {
|
|
102
|
+
if (!open) {
|
|
103
|
+
onClose();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const primaryKeyColumns = useMemo(
|
|
108
|
+
() => getPrimaryKeyColumns(columns),
|
|
109
|
+
[columns],
|
|
110
|
+
);
|
|
111
|
+
const editableColumns = useMemo(() => getEditableColumns(columns), [columns]);
|
|
112
|
+
const [drafts, setDrafts] = useState<Record<string, EditableFieldDraft>>({});
|
|
113
|
+
const [submitting, setSubmitting] = useState(false);
|
|
114
|
+
const [error, setError] = useState<string | null>(null);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!isOpen || !row) {
|
|
118
|
+
setDrafts({});
|
|
119
|
+
setSubmitting(false);
|
|
120
|
+
setError(null);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
setDrafts(
|
|
125
|
+
Object.fromEntries(
|
|
126
|
+
editableColumns.map((column) => [
|
|
127
|
+
column.name,
|
|
128
|
+
createFieldDraft(column, row[column.name]),
|
|
129
|
+
]),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
setSubmitting(false);
|
|
133
|
+
setError(null);
|
|
134
|
+
}, [editableColumns, isOpen, row]);
|
|
135
|
+
|
|
136
|
+
const handleKindChange = (columnName: string, kind: EditableValueKind) => {
|
|
137
|
+
const column = editableColumns.find(
|
|
138
|
+
(candidate) => candidate.name === columnName,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
setDrafts((current) => ({
|
|
142
|
+
...current,
|
|
143
|
+
[columnName]: {
|
|
144
|
+
kind,
|
|
145
|
+
rawValue:
|
|
146
|
+
kind === 'boolean'
|
|
147
|
+
? 'false'
|
|
148
|
+
: kind === 'blob-ish'
|
|
149
|
+
? '[]'
|
|
150
|
+
: kind === 'json'
|
|
151
|
+
? '{}'
|
|
152
|
+
: kind === 'null'
|
|
153
|
+
? ''
|
|
154
|
+
: kind === 'number' &&
|
|
155
|
+
current[columnName]?.rawValue === 'true'
|
|
156
|
+
? '1'
|
|
157
|
+
: kind === 'number' &&
|
|
158
|
+
current[columnName]?.rawValue === 'false'
|
|
159
|
+
? '0'
|
|
160
|
+
: (current[columnName]?.rawValue ??
|
|
161
|
+
(column
|
|
162
|
+
? createFieldDraft(column, null).rawValue
|
|
163
|
+
: '')),
|
|
164
|
+
},
|
|
165
|
+
}));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleSave = async () => {
|
|
169
|
+
if (!row) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
setSubmitting(true);
|
|
175
|
+
setError(null);
|
|
176
|
+
|
|
177
|
+
const nextValues = Object.fromEntries(
|
|
178
|
+
editableColumns.map((column) => [
|
|
179
|
+
column.name,
|
|
180
|
+
parseEditableFieldValue(
|
|
181
|
+
drafts[column.name] ?? createFieldDraft(column, row[column.name]),
|
|
182
|
+
),
|
|
183
|
+
]),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await onSave(nextValues);
|
|
187
|
+
onClose();
|
|
188
|
+
} catch (nextError) {
|
|
189
|
+
setError(
|
|
190
|
+
nextError instanceof Error ? nextError.message : String(nextError),
|
|
191
|
+
);
|
|
192
|
+
} finally {
|
|
193
|
+
setSubmitting(false);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Modal state={overlay}>
|
|
199
|
+
<Modal.Backdrop
|
|
200
|
+
variant="blur"
|
|
201
|
+
isDismissable={!submitting}
|
|
202
|
+
className="bg-[rgba(5,10,16,0.24)] backdrop-blur-[2px]"
|
|
203
|
+
>
|
|
204
|
+
<Modal.Container placement="center" size="lg" scroll="inside">
|
|
205
|
+
<Modal.Dialog
|
|
206
|
+
aria-label={`Edit row ${rowNumber} in ${entityName}`}
|
|
207
|
+
className="w-full max-w-4xl overflow-hidden border border-white/10 bg-[#0a121b] p-0 text-white shadow-[0_30px_90px_rgba(0,0,0,0.42)]"
|
|
208
|
+
>
|
|
209
|
+
<div className="flex items-center justify-between gap-4 border-b border-white/8 px-5 py-5">
|
|
210
|
+
<div>
|
|
211
|
+
<div className="flex items-center gap-2">
|
|
212
|
+
<Pencil aria-hidden="true" className="h-4 w-4 text-sky-300" />
|
|
213
|
+
<h2 className="text-lg font-semibold text-white">
|
|
214
|
+
Edit Row {rowNumber}
|
|
215
|
+
</h2>
|
|
216
|
+
</div>
|
|
217
|
+
<p className="mt-1 text-sm text-slate-400">{entityName}</p>
|
|
218
|
+
</div>
|
|
219
|
+
<SqliteModalCloseButton onClose={onClose} disabled={submitting} />
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<Modal.Body className="space-y-0 p-0">
|
|
223
|
+
<div className="space-y-5 px-5 py-5">
|
|
224
|
+
{primaryKeyColumns.length > 0 ? (
|
|
225
|
+
<section className="space-y-3">
|
|
226
|
+
<div>
|
|
227
|
+
<h3 className="text-sm font-medium text-slate-200">
|
|
228
|
+
Row Identifier
|
|
229
|
+
</h3>
|
|
230
|
+
<p className="mt-1 text-xs text-slate-400">
|
|
231
|
+
Primary-key fields are shown for reference and cannot be
|
|
232
|
+
edited.
|
|
233
|
+
</p>
|
|
234
|
+
</div>
|
|
235
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
236
|
+
{primaryKeyColumns.map((column) => (
|
|
237
|
+
<div
|
|
238
|
+
key={column.name}
|
|
239
|
+
className="rounded-2xl border border-white/8 bg-white/[0.03] p-3"
|
|
240
|
+
>
|
|
241
|
+
<div className="flex items-center justify-between gap-3">
|
|
242
|
+
<div>
|
|
243
|
+
<p className="text-sm font-medium text-white">
|
|
244
|
+
{column.name}
|
|
245
|
+
</p>
|
|
246
|
+
<p className="text-xs uppercase tracking-[0.24em] text-slate-500">
|
|
247
|
+
{column.type || 'value'}
|
|
248
|
+
</p>
|
|
249
|
+
</div>
|
|
250
|
+
<span className="sqlite-chip sqlite-chip-static">
|
|
251
|
+
PK
|
|
252
|
+
</span>
|
|
253
|
+
</div>
|
|
254
|
+
<p className="mt-3 break-all font-mono text-sm text-slate-200">
|
|
255
|
+
{getValuePreview(row?.[column.name])}
|
|
256
|
+
</p>
|
|
257
|
+
<p className="mt-1 text-xs text-slate-500">
|
|
258
|
+
{getValueKind(row?.[column.name])}
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
</section>
|
|
264
|
+
) : null}
|
|
265
|
+
|
|
266
|
+
<section className="space-y-3">
|
|
267
|
+
<div>
|
|
268
|
+
<h3 className="text-sm font-medium text-slate-200">
|
|
269
|
+
Editable Values
|
|
270
|
+
</h3>
|
|
271
|
+
<p className="mt-1 text-xs text-slate-400">
|
|
272
|
+
Update any non-primary-key column and save to write the
|
|
273
|
+
row back to SQLite.
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{editableColumns.length === 0 ? (
|
|
278
|
+
<div className="rounded-2xl border border-white/8 bg-white/[0.03] p-4 text-sm text-slate-300">
|
|
279
|
+
This row does not expose editable, non-primary-key
|
|
280
|
+
columns.
|
|
281
|
+
</div>
|
|
282
|
+
) : (
|
|
283
|
+
<div className="space-y-4">
|
|
284
|
+
{editableColumns.map((column) => {
|
|
285
|
+
const draft =
|
|
286
|
+
drafts[column.name] ??
|
|
287
|
+
createFieldDraft(column, row?.[column.name]);
|
|
288
|
+
const compatibleKinds = getCompatibleValueKinds(
|
|
289
|
+
column,
|
|
290
|
+
row?.[column.name],
|
|
291
|
+
);
|
|
292
|
+
const allowNull = canColumnBeNull(column);
|
|
293
|
+
const shouldUseTextArea =
|
|
294
|
+
draft.kind === 'blob-ish' ||
|
|
295
|
+
draft.kind === 'json' ||
|
|
296
|
+
(draft.kind === 'text' &&
|
|
297
|
+
draft.rawValue.includes('\n'));
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div
|
|
301
|
+
key={column.name}
|
|
302
|
+
className="rounded-2xl border border-white/8 bg-white/[0.03] p-4"
|
|
303
|
+
>
|
|
304
|
+
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
305
|
+
<div className="min-w-0">
|
|
306
|
+
<label
|
|
307
|
+
htmlFor={`sqlite-row-edit-${column.name}`}
|
|
308
|
+
className="text-sm font-medium text-white"
|
|
309
|
+
>
|
|
310
|
+
{column.name}
|
|
311
|
+
</label>
|
|
312
|
+
<p className="mt-1 text-xs text-slate-500">
|
|
313
|
+
{column.type || 'value'}
|
|
314
|
+
</p>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{compatibleKinds.length > 1 || allowNull ? (
|
|
318
|
+
<div className="w-full max-w-[12rem]">
|
|
319
|
+
<label
|
|
320
|
+
htmlFor={`sqlite-row-edit-type-${column.name}`}
|
|
321
|
+
className="sr-only"
|
|
322
|
+
>
|
|
323
|
+
Value type
|
|
324
|
+
</label>
|
|
325
|
+
<select
|
|
326
|
+
id={`sqlite-row-edit-type-${column.name}`}
|
|
327
|
+
className="sqlite-select w-full"
|
|
328
|
+
value={draft.kind}
|
|
329
|
+
disabled={submitting}
|
|
330
|
+
onChange={(event) =>
|
|
331
|
+
handleKindChange(
|
|
332
|
+
column.name,
|
|
333
|
+
event.target.value as EditableValueKind,
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
>
|
|
337
|
+
{compatibleKinds.map((kind) => (
|
|
338
|
+
<option key={kind} value={kind}>
|
|
339
|
+
{kind === 'blob-ish'
|
|
340
|
+
? 'Blob'
|
|
341
|
+
: kind === 'json'
|
|
342
|
+
? 'JSON'
|
|
343
|
+
: kind === 'boolean'
|
|
344
|
+
? 'Boolean'
|
|
345
|
+
: kind === 'number'
|
|
346
|
+
? 'Number'
|
|
347
|
+
: 'Text'}
|
|
348
|
+
</option>
|
|
349
|
+
))}
|
|
350
|
+
{allowNull ? (
|
|
351
|
+
<option value="null">NULL</option>
|
|
352
|
+
) : null}
|
|
353
|
+
</select>
|
|
354
|
+
</div>
|
|
355
|
+
) : (
|
|
356
|
+
<span className="sqlite-chip sqlite-chip-static">
|
|
357
|
+
{compatibleKinds[0] === 'blob-ish'
|
|
358
|
+
? 'Blob'
|
|
359
|
+
: compatibleKinds[0] === 'json'
|
|
360
|
+
? 'JSON'
|
|
361
|
+
: compatibleKinds[0] === 'boolean'
|
|
362
|
+
? 'Boolean'
|
|
363
|
+
: compatibleKinds[0] === 'number'
|
|
364
|
+
? 'Number'
|
|
365
|
+
: 'Text'}
|
|
366
|
+
</span>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<div className="mt-3">
|
|
371
|
+
{draft.kind === 'null' ? (
|
|
372
|
+
<div className="rounded-xl border border-dashed border-white/10 bg-black/10 px-3 py-2 text-sm text-slate-400">
|
|
373
|
+
This field will be saved as SQL NULL.
|
|
374
|
+
</div>
|
|
375
|
+
) : draft.kind === 'boolean' ? (
|
|
376
|
+
<select
|
|
377
|
+
id={`sqlite-row-edit-${column.name}`}
|
|
378
|
+
className="sqlite-select w-full"
|
|
379
|
+
value={draft.rawValue}
|
|
380
|
+
disabled={submitting}
|
|
381
|
+
onChange={(event) =>
|
|
382
|
+
setDrafts((current) => ({
|
|
383
|
+
...current,
|
|
384
|
+
[column.name]: {
|
|
385
|
+
...draft,
|
|
386
|
+
rawValue: event.target.value,
|
|
387
|
+
},
|
|
388
|
+
}))
|
|
389
|
+
}
|
|
390
|
+
>
|
|
391
|
+
<option value="true">true</option>
|
|
392
|
+
<option value="false">false</option>
|
|
393
|
+
</select>
|
|
394
|
+
) : shouldUseTextArea ? (
|
|
395
|
+
<textarea
|
|
396
|
+
id={`sqlite-row-edit-${column.name}`}
|
|
397
|
+
className="sqlite-input min-h-28 w-full resize-y py-3"
|
|
398
|
+
value={draft.rawValue}
|
|
399
|
+
disabled={submitting}
|
|
400
|
+
onChange={(event) =>
|
|
401
|
+
setDrafts((current) => ({
|
|
402
|
+
...current,
|
|
403
|
+
[column.name]: {
|
|
404
|
+
...draft,
|
|
405
|
+
rawValue: event.target.value,
|
|
406
|
+
},
|
|
407
|
+
}))
|
|
408
|
+
}
|
|
409
|
+
/>
|
|
410
|
+
) : (
|
|
411
|
+
<input
|
|
412
|
+
id={`sqlite-row-edit-${column.name}`}
|
|
413
|
+
type="text"
|
|
414
|
+
className="sqlite-input w-full"
|
|
415
|
+
value={draft.rawValue}
|
|
416
|
+
disabled={submitting}
|
|
417
|
+
onChange={(event) =>
|
|
418
|
+
setDrafts((current) => ({
|
|
419
|
+
...current,
|
|
420
|
+
[column.name]: {
|
|
421
|
+
...draft,
|
|
422
|
+
rawValue: event.target.value,
|
|
423
|
+
},
|
|
424
|
+
}))
|
|
425
|
+
}
|
|
426
|
+
/>
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<p className="mt-2 text-xs text-slate-500">
|
|
431
|
+
Current value:{' '}
|
|
432
|
+
{getValuePreview(row?.[column.name])} (
|
|
433
|
+
{getValueKind(row?.[column.name])}) · Compatible:{' '}
|
|
434
|
+
{compatibleKinds
|
|
435
|
+
.map((kind) =>
|
|
436
|
+
kind === 'blob-ish'
|
|
437
|
+
? 'blob'
|
|
438
|
+
: kind === 'json'
|
|
439
|
+
? 'json'
|
|
440
|
+
: kind,
|
|
441
|
+
)
|
|
442
|
+
.join(', ')}
|
|
443
|
+
{allowNull ? ', null' : ''}
|
|
444
|
+
</p>
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
})}
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
</section>
|
|
451
|
+
|
|
452
|
+
{error ? (
|
|
453
|
+
<div className="sqlite-inline-error" aria-live="polite">
|
|
454
|
+
<div>
|
|
455
|
+
<p className="font-medium text-rose-100">Save Failed</p>
|
|
456
|
+
<p className="mt-1 text-sm text-rose-100/90">{error}</p>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
) : null}
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<div className="flex items-center justify-end gap-3 border-t border-white/8 px-5 py-5">
|
|
463
|
+
<button
|
|
464
|
+
type="button"
|
|
465
|
+
className={sqliteSecondaryButtonClassName}
|
|
466
|
+
onClick={onClose}
|
|
467
|
+
disabled={submitting}
|
|
468
|
+
>
|
|
469
|
+
Cancel
|
|
470
|
+
</button>
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
className={sqlitePrimaryButtonClassName}
|
|
474
|
+
onClick={() => void handleSave()}
|
|
475
|
+
disabled={submitting || editableColumns.length === 0}
|
|
476
|
+
>
|
|
477
|
+
<Save aria-hidden="true" className="h-4 w-4" />
|
|
478
|
+
{submitting ? 'Saving…' : 'Save Changes'}
|
|
479
|
+
</button>
|
|
480
|
+
</div>
|
|
481
|
+
</Modal.Body>
|
|
482
|
+
</Modal.Dialog>
|
|
483
|
+
</Modal.Container>
|
|
484
|
+
</Modal.Backdrop>
|
|
485
|
+
</Modal>
|
|
486
|
+
);
|
|
487
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type EditableValueKind =
|
|
2
|
+
| 'null'
|
|
3
|
+
| 'text'
|
|
4
|
+
| 'number'
|
|
5
|
+
| 'boolean'
|
|
6
|
+
| 'blob-ish'
|
|
7
|
+
| 'json';
|
|
8
|
+
|
|
9
|
+
export type EditableFieldDraft = {
|
|
10
|
+
kind: EditableValueKind;
|
|
11
|
+
rawValue: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const parseEditableFieldValue = (draft: EditableFieldDraft): unknown => {
|
|
15
|
+
switch (draft.kind) {
|
|
16
|
+
case 'null':
|
|
17
|
+
return null;
|
|
18
|
+
case 'text':
|
|
19
|
+
return draft.rawValue;
|
|
20
|
+
case 'number': {
|
|
21
|
+
const value = Number(draft.rawValue.trim());
|
|
22
|
+
|
|
23
|
+
if (Number.isNaN(value)) {
|
|
24
|
+
throw new Error('Enter a valid number.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
case 'boolean': {
|
|
30
|
+
if (draft.rawValue !== 'true' && draft.rawValue !== 'false') {
|
|
31
|
+
throw new Error('Choose either true or false.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return draft.rawValue === 'true';
|
|
35
|
+
}
|
|
36
|
+
case 'blob-ish': {
|
|
37
|
+
const parsed = JSON.parse(draft.rawValue);
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
!Array.isArray(parsed) ||
|
|
41
|
+
!parsed.every((item) => typeof item === 'number')
|
|
42
|
+
) {
|
|
43
|
+
throw new Error('Blob values must be JSON arrays of numbers.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return new Uint8Array(parsed);
|
|
47
|
+
}
|
|
48
|
+
case 'json': {
|
|
49
|
+
const parsed = JSON.parse(draft.rawValue);
|
|
50
|
+
return JSON.stringify(parsed);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|