@buoy-gg/storage 7.0.30 → 7.0.31
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/lib/commonjs/storage/components/GameUIStorageBrowser.js +92 -21
- package/lib/commonjs/storage/components/StorageBrowserMode.js +8 -2
- package/lib/commonjs/storage/components/StorageKeyDetailScreen.js +572 -0
- package/lib/commonjs/storage/components/StorageKeyRow.js +21 -2
- package/lib/commonjs/storage/components/StorageKeySection.js +44 -21
- package/lib/commonjs/storage/components/StorageModalWithTabs.js +24 -2
- package/lib/commonjs/storage/components/StorageSaveNotice.js +171 -0
- package/lib/commonjs/storage/components/StorageValueEditModal.js +11 -364
- package/lib/commonjs/storage/hooks/useKeyEditor.js +12 -85
- package/lib/commonjs/storage/hooks/useTreeDraft.js +7 -100
- package/lib/module/storage/components/GameUIStorageBrowser.js +94 -23
- package/lib/module/storage/components/StorageBrowserMode.js +8 -2
- package/lib/module/storage/components/StorageKeyDetailScreen.js +570 -0
- package/lib/module/storage/components/StorageKeyRow.js +20 -3
- package/lib/module/storage/components/StorageKeySection.js +43 -21
- package/lib/module/storage/components/StorageModalWithTabs.js +24 -2
- package/lib/module/storage/components/StorageSaveNotice.js +168 -0
- package/lib/module/storage/components/StorageValueEditModal.js +13 -366
- package/lib/module/storage/hooks/useKeyEditor.js +14 -87
- package/lib/module/storage/hooks/useTreeDraft.js +9 -102
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +13 -2
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts +7 -2
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts +46 -0
- package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts +9 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeySection.d.ts +19 -1
- package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageSaveNotice.d.ts +6 -0
- package/lib/typescript/storage/components/StorageSaveNotice.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts +9 -31
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useKeyEditor.d.ts +7 -31
- package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts +7 -66
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -1
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +1 -1
- package/package.json +5 -5
|
@@ -1,373 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Storage's value-typing panel.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* simply underneath it. At the top it stays put and stays readable.
|
|
12
|
-
*
|
|
13
|
-
* The field is multiline for anything textual. Stored values are routinely
|
|
14
|
-
* tokens, URLs and serialized blobs, and a single line shows you roughly forty
|
|
15
|
-
* characters of them, scrolling sideways — which is how you edit the wrong part
|
|
16
|
-
* of a string without noticing.
|
|
17
|
-
*
|
|
18
|
-
* Type is preserved by construction: the ORIGINAL value's type picks the editor
|
|
19
|
-
* AND decides how the draft is read back, so editing a number can only produce
|
|
20
|
-
* a number. Raw JSON — the escape hatch for containers — re-checks the parsed
|
|
21
|
-
* result and refuses a different shape.
|
|
22
|
-
*
|
|
23
|
-
* Nothing here writes. `onSave` hands the value to whoever owns it.
|
|
6
|
+
* The panel itself — top-pinned so the keyboard can't reach it, type preserved
|
|
7
|
+
* by construction, raw-JSON shape check — lives in shared-ui's
|
|
8
|
+
* {@link ValueEditModal}, shared with the Zustand store editor. This wrapper
|
|
9
|
+
* only pins storage's testID namespace so the Detox suite's handles
|
|
10
|
+
* (`buoy-storage-value-input`, `-save`, …) stay exactly what they were.
|
|
24
11
|
*/
|
|
25
12
|
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* How this value gets edited. Driven by the value's own type, which is also
|
|
33
|
-
* what makes the result type-safe without asking the user to declare anything.
|
|
34
|
-
*/
|
|
35
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
36
|
-
function modeFor(value) {
|
|
37
|
-
if (typeof value === "boolean") return "boolean";
|
|
38
|
-
if (typeof value === "number") return "number";
|
|
39
|
-
if (value !== null && typeof value === "object") return "raw";
|
|
40
|
-
return "text";
|
|
41
|
-
}
|
|
42
|
-
function initialText(value, mode) {
|
|
43
|
-
if (mode === "raw") return JSON.stringify(value, null, 2);
|
|
44
|
-
if (value === null || value === undefined) return "";
|
|
45
|
-
return String(value);
|
|
46
|
-
}
|
|
47
|
-
function hintFor(mode, type) {
|
|
48
|
-
switch (mode) {
|
|
49
|
-
case "number":
|
|
50
|
-
return "Numeric only — anything else is refused, so this stays a number.";
|
|
51
|
-
case "raw":
|
|
52
|
-
return `Raw JSON. Must stay ${type === "array" ? "an array" : "an object"} — a different shape is refused.`;
|
|
53
|
-
default:
|
|
54
|
-
return "Saves as a string. The type never changes here.";
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
export function StorageValueEditModal({
|
|
58
|
-
path,
|
|
59
|
-
value,
|
|
60
|
-
onSave,
|
|
61
|
-
onCancel,
|
|
62
|
-
saveLabel = "Save",
|
|
63
|
-
busy = false
|
|
64
|
-
}) {
|
|
65
|
-
const mode = modeFor(value);
|
|
66
|
-
const type = typeLabel(value);
|
|
67
|
-
const [text, setText] = useState(() => initialText(value, mode));
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Advisory only — {@link commit} re-derives it, so a disabled button and a
|
|
71
|
-
* refused save can never disagree about what's valid.
|
|
72
|
-
*/
|
|
73
|
-
const error = validate(text, mode, type);
|
|
74
|
-
const blocked = busy || error != null;
|
|
75
|
-
const commit = () => {
|
|
76
|
-
if (busy || validate(text, mode, type) != null) return;
|
|
77
|
-
onSave(parse(text, mode, value));
|
|
78
|
-
};
|
|
79
|
-
return /*#__PURE__*/_jsxs(View, {
|
|
80
|
-
style: styles.overlay,
|
|
81
|
-
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
82
|
-
style: styles.scrim,
|
|
83
|
-
activeOpacity: 1,
|
|
84
|
-
onPress: busy ? undefined : onCancel,
|
|
85
|
-
testID: "buoy-storage-value-scrim",
|
|
86
|
-
accessibilityRole: "button",
|
|
87
|
-
accessibilityLabel: "Dismiss editor"
|
|
88
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
89
|
-
style: styles.panel,
|
|
90
|
-
pointerEvents: "box-none",
|
|
91
|
-
children: /*#__PURE__*/_jsxs(View, {
|
|
92
|
-
style: styles.card,
|
|
93
|
-
children: [/*#__PURE__*/_jsxs(View, {
|
|
94
|
-
style: styles.headerRow,
|
|
95
|
-
children: [/*#__PURE__*/_jsx(Text, {
|
|
96
|
-
style: styles.title,
|
|
97
|
-
children: "Edit value"
|
|
98
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
99
|
-
style: styles.typeBadge,
|
|
100
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
101
|
-
style: styles.typeBadgeText,
|
|
102
|
-
children: type
|
|
103
|
-
})
|
|
104
|
-
})]
|
|
105
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
106
|
-
style: styles.path,
|
|
107
|
-
numberOfLines: 1,
|
|
108
|
-
children: breadcrumb(path)
|
|
109
|
-
}), mode === "boolean" ? /*#__PURE__*/_jsx(BooleanChoice, {
|
|
110
|
-
current: value === true,
|
|
111
|
-
onPick: onSave
|
|
112
|
-
}) : /*#__PURE__*/_jsx(ScrollView, {
|
|
113
|
-
style: styles.fieldScroll,
|
|
114
|
-
keyboardShouldPersistTaps: "handled",
|
|
115
|
-
children: /*#__PURE__*/_jsx(TextInput, {
|
|
116
|
-
testID: "buoy-storage-value-input",
|
|
117
|
-
accessibilityLabel: "Value",
|
|
118
|
-
value: text,
|
|
119
|
-
onChangeText: setText,
|
|
120
|
-
autoFocus: true,
|
|
121
|
-
autoCapitalize: "none",
|
|
122
|
-
autoCorrect: false,
|
|
123
|
-
spellCheck: false,
|
|
124
|
-
editable: !busy
|
|
125
|
-
// Multiline for anything textual: stored values are long more
|
|
126
|
-
// often than they're short, and a single line hides that.
|
|
127
|
-
,
|
|
128
|
-
multiline: mode !== "number"
|
|
129
|
-
// ASCII for raw JSON, because iOS Smart Punctuation is ON by
|
|
130
|
-
// default and rewrites the quotes you type as curly ones —
|
|
131
|
-
// which makes the text unparseable, disables Save, and leaves
|
|
132
|
-
// an "invalid JSON" message whose cause is invisible at a
|
|
133
|
-
// glance. An ASCII keyboard doesn't do the substitution.
|
|
134
|
-
,
|
|
135
|
-
keyboardType: mode === "number" ? "numeric" : mode === "raw" ? "ascii-capable" : "default",
|
|
136
|
-
onSubmitEditing: mode === "number" ? commit : undefined,
|
|
137
|
-
placeholder: mode === "raw" ? "{ }" : "value",
|
|
138
|
-
placeholderTextColor: night.color.placeholder,
|
|
139
|
-
style: [styles.input, mode === "raw" && styles.inputRaw, mode !== "number" && styles.inputMultiline, error != null && styles.inputInvalid]
|
|
140
|
-
})
|
|
141
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
142
|
-
style: error != null ? styles.error : styles.hint,
|
|
143
|
-
children: error ?? hintFor(mode, type)
|
|
144
|
-
}), /*#__PURE__*/_jsxs(View, {
|
|
145
|
-
style: styles.actions,
|
|
146
|
-
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
147
|
-
onPress: onCancel,
|
|
148
|
-
disabled: busy,
|
|
149
|
-
testID: "buoy-storage-value-cancel",
|
|
150
|
-
accessibilityRole: "button",
|
|
151
|
-
accessibilityLabel: "Cancel edit",
|
|
152
|
-
style: styles.cancelButton,
|
|
153
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
154
|
-
style: styles.cancelText,
|
|
155
|
-
children: "Cancel"
|
|
156
|
-
})
|
|
157
|
-
}), mode === "boolean" ? null : /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
158
|
-
onPress: commit,
|
|
159
|
-
disabled: blocked,
|
|
160
|
-
testID: "buoy-storage-value-save",
|
|
161
|
-
accessibilityRole: "button",
|
|
162
|
-
accessibilityLabel: saveLabel,
|
|
163
|
-
style: [styles.saveButton, blocked && styles.saveDisabled],
|
|
164
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
165
|
-
style: styles.saveText,
|
|
166
|
-
children: busy ? "Saving…" : saveLabel
|
|
167
|
-
})
|
|
168
|
-
})]
|
|
169
|
-
})]
|
|
170
|
-
})
|
|
171
|
-
})]
|
|
13
|
+
import { ValueEditModal } from "@buoy-gg/shared-ui/dataViewer";
|
|
14
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
15
|
+
export function StorageValueEditModal(props) {
|
|
16
|
+
return /*#__PURE__*/_jsx(ValueEditModal, {
|
|
17
|
+
...props,
|
|
18
|
+
testIDPrefix: "buoy-storage-value"
|
|
172
19
|
});
|
|
173
|
-
}
|
|
174
|
-
function BooleanChoice({
|
|
175
|
-
current,
|
|
176
|
-
onPick
|
|
177
|
-
}) {
|
|
178
|
-
return /*#__PURE__*/_jsx(View, {
|
|
179
|
-
style: styles.boolRow,
|
|
180
|
-
children: [true, false].map(option => /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
181
|
-
onPress: () => onPick(option),
|
|
182
|
-
testID: `buoy-storage-value-${option}`,
|
|
183
|
-
accessibilityRole: "button",
|
|
184
|
-
accessibilityLabel: `Set ${option}`,
|
|
185
|
-
style: [styles.boolButton, current === option && styles.boolActive],
|
|
186
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
187
|
-
style: [styles.boolText, current === option && styles.boolTextActive],
|
|
188
|
-
children: String(option)
|
|
189
|
-
})
|
|
190
|
-
}, String(option)))
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Null when the draft is applicable, else why it isn't. */
|
|
195
|
-
function validate(text, mode, type) {
|
|
196
|
-
if (mode === "number") {
|
|
197
|
-
if (text.trim() === "") return "Enter a number.";
|
|
198
|
-
if (Number.isNaN(Number(text))) return `"${text}" isn't a number.`;
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
if (mode === "raw") {
|
|
202
|
-
let parsed;
|
|
203
|
-
try {
|
|
204
|
-
parsed = JSON.parse(text);
|
|
205
|
-
} catch (e) {
|
|
206
|
-
return e instanceof Error ? e.message : "Invalid JSON.";
|
|
207
|
-
}
|
|
208
|
-
if (typeLabel(parsed) !== type) {
|
|
209
|
-
return `Must stay ${type} — that parses as ${typeLabel(parsed)}.`;
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
function parse(text, mode, original) {
|
|
216
|
-
if (mode === "number") return Number(text);
|
|
217
|
-
if (mode === "raw") return JSON.parse(text);
|
|
218
|
-
// An emptied field on a null keeps it null rather than silently minting an
|
|
219
|
-
// empty string, which would change the type this promised to preserve.
|
|
220
|
-
if (original === null && text === "") return null;
|
|
221
|
-
return text;
|
|
222
|
-
}
|
|
223
|
-
const styles = StyleSheet.create({
|
|
224
|
-
overlay: {
|
|
225
|
-
...absoluteFill,
|
|
226
|
-
zIndex: 20
|
|
227
|
-
},
|
|
228
|
-
scrim: {
|
|
229
|
-
...absoluteFill,
|
|
230
|
-
backgroundColor: "rgba(0,0,0,0.55)"
|
|
231
|
-
},
|
|
232
|
-
/** Pinned to the top so the keyboard can't reach it. */
|
|
233
|
-
panel: {
|
|
234
|
-
padding: 12
|
|
235
|
-
},
|
|
236
|
-
card: {
|
|
237
|
-
backgroundColor: night.color.surface,
|
|
238
|
-
borderRadius: 14,
|
|
239
|
-
borderWidth: 1,
|
|
240
|
-
borderColor: night.color.border,
|
|
241
|
-
padding: 14,
|
|
242
|
-
gap: 8
|
|
243
|
-
},
|
|
244
|
-
headerRow: {
|
|
245
|
-
flexDirection: "row",
|
|
246
|
-
alignItems: "center",
|
|
247
|
-
gap: 8
|
|
248
|
-
},
|
|
249
|
-
title: {
|
|
250
|
-
flex: 1,
|
|
251
|
-
color: night.color.text,
|
|
252
|
-
fontSize: 14,
|
|
253
|
-
fontWeight: "700"
|
|
254
|
-
},
|
|
255
|
-
typeBadge: {
|
|
256
|
-
paddingHorizontal: 8,
|
|
257
|
-
paddingVertical: 2,
|
|
258
|
-
borderRadius: 6,
|
|
259
|
-
backgroundColor: night.color.surfaceElevated
|
|
260
|
-
},
|
|
261
|
-
typeBadgeText: {
|
|
262
|
-
color: macOSColors.semantic.warning,
|
|
263
|
-
fontSize: 10,
|
|
264
|
-
fontWeight: "700",
|
|
265
|
-
fontFamily: "monospace"
|
|
266
|
-
},
|
|
267
|
-
path: {
|
|
268
|
-
color: night.color.textTertiary,
|
|
269
|
-
fontSize: 11,
|
|
270
|
-
fontFamily: "monospace"
|
|
271
|
-
},
|
|
272
|
-
/** Caps the panel's height so a huge blob can't push the buttons off-screen. */
|
|
273
|
-
fieldScroll: {
|
|
274
|
-
maxHeight: 240
|
|
275
|
-
},
|
|
276
|
-
input: {
|
|
277
|
-
color: night.color.text,
|
|
278
|
-
backgroundColor: night.color.surfaceElevated,
|
|
279
|
-
borderWidth: 1,
|
|
280
|
-
borderColor: night.color.border,
|
|
281
|
-
borderRadius: 10,
|
|
282
|
-
paddingHorizontal: 12,
|
|
283
|
-
paddingVertical: 10,
|
|
284
|
-
fontSize: 14,
|
|
285
|
-
fontFamily: "monospace"
|
|
286
|
-
},
|
|
287
|
-
inputMultiline: {
|
|
288
|
-
minHeight: 110,
|
|
289
|
-
lineHeight: 20,
|
|
290
|
-
textAlignVertical: "top"
|
|
291
|
-
},
|
|
292
|
-
inputRaw: {
|
|
293
|
-
minHeight: 180,
|
|
294
|
-
fontSize: 12,
|
|
295
|
-
lineHeight: 18
|
|
296
|
-
},
|
|
297
|
-
inputInvalid: {
|
|
298
|
-
borderColor: night.color.danger
|
|
299
|
-
},
|
|
300
|
-
hint: {
|
|
301
|
-
color: night.color.textTertiary,
|
|
302
|
-
fontSize: 11,
|
|
303
|
-
lineHeight: 16
|
|
304
|
-
},
|
|
305
|
-
error: {
|
|
306
|
-
color: night.color.danger,
|
|
307
|
-
fontSize: 11,
|
|
308
|
-
lineHeight: 16,
|
|
309
|
-
fontFamily: "monospace"
|
|
310
|
-
},
|
|
311
|
-
actions: {
|
|
312
|
-
flexDirection: "row",
|
|
313
|
-
gap: 8,
|
|
314
|
-
paddingTop: 2
|
|
315
|
-
},
|
|
316
|
-
cancelButton: {
|
|
317
|
-
flex: 1,
|
|
318
|
-
minHeight: 44,
|
|
319
|
-
alignItems: "center",
|
|
320
|
-
justifyContent: "center",
|
|
321
|
-
borderRadius: 10,
|
|
322
|
-
borderWidth: 1,
|
|
323
|
-
borderColor: night.color.border
|
|
324
|
-
},
|
|
325
|
-
cancelText: {
|
|
326
|
-
color: night.color.textSecondary,
|
|
327
|
-
fontSize: 14,
|
|
328
|
-
fontWeight: "600"
|
|
329
|
-
},
|
|
330
|
-
saveButton: {
|
|
331
|
-
flex: 1.4,
|
|
332
|
-
minHeight: 44,
|
|
333
|
-
alignItems: "center",
|
|
334
|
-
justifyContent: "center",
|
|
335
|
-
borderRadius: 10,
|
|
336
|
-
backgroundColor: night.color.accent
|
|
337
|
-
},
|
|
338
|
-
saveDisabled: {
|
|
339
|
-
opacity: 0.35
|
|
340
|
-
},
|
|
341
|
-
saveText: {
|
|
342
|
-
color: night.color.onAccent,
|
|
343
|
-
fontSize: 15,
|
|
344
|
-
fontWeight: "700"
|
|
345
|
-
},
|
|
346
|
-
boolRow: {
|
|
347
|
-
flexDirection: "row",
|
|
348
|
-
gap: 10
|
|
349
|
-
},
|
|
350
|
-
boolButton: {
|
|
351
|
-
flex: 1,
|
|
352
|
-
minHeight: 64,
|
|
353
|
-
alignItems: "center",
|
|
354
|
-
justifyContent: "center",
|
|
355
|
-
borderRadius: 12,
|
|
356
|
-
borderWidth: 1,
|
|
357
|
-
borderColor: night.color.border,
|
|
358
|
-
backgroundColor: night.color.surfaceElevated
|
|
359
|
-
},
|
|
360
|
-
boolActive: {
|
|
361
|
-
borderColor: macOSColors.semantic.warning,
|
|
362
|
-
backgroundColor: macOSColors.semantic.warning + "18"
|
|
363
|
-
},
|
|
364
|
-
boolText: {
|
|
365
|
-
color: night.color.textTertiary,
|
|
366
|
-
fontSize: 16,
|
|
367
|
-
fontWeight: "700",
|
|
368
|
-
fontFamily: "monospace"
|
|
369
|
-
},
|
|
370
|
-
boolTextActive: {
|
|
371
|
-
color: macOSColors.semantic.warning
|
|
372
|
-
}
|
|
373
|
-
});
|
|
20
|
+
}
|
|
@@ -1,100 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Storage's view of the shared tree editor.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* chips docked in a panel — which is a rendering concern and nothing more.
|
|
11
|
-
*
|
|
12
|
-
* Keeping the rules here means the two surfaces can't drift on the things that
|
|
13
|
-
* would actually hurt: whether a boolean opens an editor, whether a failed
|
|
14
|
-
* write closes the editor anyway, what "dirty" means.
|
|
6
|
+
* Everything editing a key involves — buffered draft, selection, value modal,
|
|
7
|
+
* dirty-close guard — lives in shared-ui's `useTreeEditor`, shared with the
|
|
8
|
+
* Zustand store editor so the two can't drift. Storage's single write takes
|
|
9
|
+
* the SERIALIZED value, so this wrapper is exactly the stringify.
|
|
15
10
|
*/
|
|
16
11
|
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { useTreeDraft } from "./useTreeDraft";
|
|
12
|
+
import { useMemo } from "react";
|
|
13
|
+
import { useTreeEditor } from "@buoy-gg/shared-ui/dataViewer";
|
|
20
14
|
export function useKeyEditor({
|
|
21
15
|
value,
|
|
22
16
|
save,
|
|
23
17
|
onClose,
|
|
24
18
|
confirmDiscard
|
|
25
19
|
}) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
begin();
|
|
34
|
-
// Seed the draft ONCE, when the editor opens. `begin` closes over the live
|
|
35
|
-
// value, whose identity changes every time the key list refreshes
|
|
36
|
-
// underneath us (a parsed JSON string is a new object each pass), so
|
|
37
|
-
// re-running would reset the draft mid-edit and discard the user's work.
|
|
38
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
|
-
}, []);
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* "Change this node". For a boolean that's a flip rather than a modal — the
|
|
43
|
-
* same rule `treeActionsFor` applies to the action controls, so the gesture
|
|
44
|
-
* and the button can't mean different things.
|
|
45
|
-
*/
|
|
46
|
-
const openValueEditor = useCallback(path => {
|
|
47
|
-
const current = getAtPath(draft.value, path);
|
|
48
|
-
if (typeof current === "boolean") {
|
|
49
|
-
draft.apply({
|
|
50
|
-
kind: "replace",
|
|
51
|
-
path,
|
|
52
|
-
value: !current
|
|
53
|
-
});
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
setValuePath(path);
|
|
57
|
-
}, [draft]);
|
|
58
|
-
const applyValue = useCallback(next => {
|
|
59
|
-
draft.apply({
|
|
60
|
-
kind: "replace",
|
|
61
|
-
path: valuePath ?? [],
|
|
62
|
-
value: next
|
|
63
|
-
});
|
|
64
|
-
setValuePath(null);
|
|
65
|
-
}, [draft, valuePath]);
|
|
66
|
-
|
|
67
|
-
// Only leave once the write actually landed — `commit` resolves false and
|
|
68
|
-
// keeps the draft on screen when the device refuses it, and closing anyway
|
|
69
|
-
// would throw the work away while showing an error nobody ever sees.
|
|
70
|
-
const saveAndClose = useCallback(() => {
|
|
71
|
-
void draft.commit().then(saved => saved && onClose());
|
|
72
|
-
}, [draft, onClose]);
|
|
73
|
-
const requestClose = useCallback(() => {
|
|
74
|
-
if (!draft.isDirty) {
|
|
75
|
-
onClose();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
// The draft is the only copy of this work, so closing over it silently is
|
|
79
|
-
// the one way this editor can lose something the user can't get back.
|
|
80
|
-
confirmDiscard({
|
|
81
|
-
save: saveAndClose,
|
|
82
|
-
discard: () => {
|
|
83
|
-
draft.discard();
|
|
84
|
-
onClose();
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}, [confirmDiscard, draft, onClose, saveAndClose]);
|
|
88
|
-
return {
|
|
89
|
-
draft,
|
|
90
|
-
selection,
|
|
91
|
-
setSelection,
|
|
92
|
-
valuePath,
|
|
93
|
-
openValueEditor,
|
|
94
|
-
closeValueEditor: () => setValuePath(null),
|
|
95
|
-
applyValue,
|
|
96
|
-
openRawEditor: () => setValuePath([]),
|
|
97
|
-
requestClose,
|
|
98
|
-
saveAndClose
|
|
99
|
-
};
|
|
20
|
+
const saveValue = useMemo(() => next => save(JSON.stringify(next)), [save]);
|
|
21
|
+
return useTreeEditor({
|
|
22
|
+
value,
|
|
23
|
+
save: saveValue,
|
|
24
|
+
onClose,
|
|
25
|
+
confirmDiscard
|
|
26
|
+
});
|
|
100
27
|
}
|
|
@@ -1,114 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Storage's view of the shared buffered tree draft.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* event, a snapshot comes back, and the browser re-reads every backend — so
|
|
11
|
-
* a six-tap reorder is six writes and six full refreshes. The tree rebuilds
|
|
12
|
-
* under the user mid-gesture.
|
|
13
|
-
* 2. **The highlight and the value fall out of step.** Selection is local and
|
|
14
|
-
* moves instantly; the value it points at only catches up when the round
|
|
15
|
-
* trip completes, so for a beat the highlight sits on the old contents.
|
|
16
|
-
*
|
|
17
|
-
* Optimistic updates — the React Query pattern of rendering the expected result
|
|
18
|
-
* and reconciling later — fix (2) but not (1): they still write per change.
|
|
19
|
-
* Buffering fixes both, and it's what every editor that owns a document does
|
|
20
|
-
* (a form's dirty state, an IDE's unsaved buffer, JSON Editor's in-memory doc
|
|
21
|
-
* with an explicit save).
|
|
22
|
-
*
|
|
23
|
-
* The trade is that the buffer can go stale if the app writes the same key
|
|
24
|
-
* while you're editing. That's the same last-write-wins a text editor has, and
|
|
25
|
-
* `isDirty` at least makes it visible that you're holding unsaved changes.
|
|
6
|
+
* The draft itself lives in shared-ui (`useTreeDraft` there — one editor for
|
|
7
|
+
* storage keys and zustand stores, so the rules can't drift). Storage's write
|
|
8
|
+
* path takes the SERIALIZED value — `setItem(key, raw)` — so this wrapper is
|
|
9
|
+
* exactly the stringify.
|
|
26
10
|
*/
|
|
27
11
|
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
12
|
+
import { useMemo } from "react";
|
|
13
|
+
import { useTreeDraft as useSharedTreeDraft } from "@buoy-gg/shared-ui/dataViewer";
|
|
30
14
|
/**
|
|
31
15
|
* @param liveValue the value as storage currently holds it (already parsed)
|
|
32
16
|
* @param save persists the serialized draft; rejecting keeps edit mode open
|
|
33
17
|
*/
|
|
34
18
|
export function useTreeDraft(liveValue, save) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
38
|
-
const [error, setError] = useState(null);
|
|
39
|
-
const [selectPath, setSelectPath] = useState();
|
|
40
|
-
/**
|
|
41
|
-
* The value as it was when editing began. Compared against for dirtiness
|
|
42
|
-
* rather than against `liveValue`, which can move underneath us if the app
|
|
43
|
-
* writes the same key — that would otherwise flip a clean draft to dirty.
|
|
44
|
-
*/
|
|
45
|
-
const [baseline, setBaseline] = useState("");
|
|
46
|
-
const value = isEditing && draft !== undefined ? draft : liveValue;
|
|
47
|
-
const isDirty = isEditing && JSON.stringify(draft) !== baseline;
|
|
48
|
-
const begin = useCallback(() => {
|
|
49
|
-
setDraft(liveValue);
|
|
50
|
-
setBaseline(JSON.stringify(liveValue));
|
|
51
|
-
setSelectPath(undefined);
|
|
52
|
-
setError(null);
|
|
53
|
-
setIsEditing(true);
|
|
54
|
-
}, [liveValue]);
|
|
55
|
-
const apply = useCallback(op => {
|
|
56
|
-
// Purely local — this is the whole point. No write, so no event, no
|
|
57
|
-
// snapshot, no refresh, and the tree re-renders from state synchronously.
|
|
58
|
-
setDraft(prev => applyJsonOp(prev, op));
|
|
59
|
-
const next = selectionPathAfterOp(op);
|
|
60
|
-
if (next !== undefined) setSelectPath(next);
|
|
61
|
-
}, []);
|
|
62
|
-
const selectAt = useCallback(path => {
|
|
63
|
-
setSelectPath(path);
|
|
64
|
-
}, []);
|
|
65
|
-
|
|
66
|
-
/** Leave edit mode entirely. Only correct on the way out (after a write). */
|
|
67
|
-
const reset = useCallback(() => {
|
|
68
|
-
setIsEditing(false);
|
|
69
|
-
setDraft(undefined);
|
|
70
|
-
setSelectPath(undefined);
|
|
71
|
-
setError(null);
|
|
72
|
-
}, []);
|
|
73
|
-
|
|
74
|
-
/** Re-seed the draft from what's stored, staying in edit mode. */
|
|
75
|
-
const discard = useCallback(() => {
|
|
76
|
-
setDraft(liveValue);
|
|
77
|
-
setBaseline(JSON.stringify(liveValue));
|
|
78
|
-
setSelectPath(undefined);
|
|
79
|
-
setError(null);
|
|
80
|
-
setIsEditing(true);
|
|
81
|
-
}, [liveValue]);
|
|
82
|
-
const commit = useCallback(async () => {
|
|
83
|
-
if (!isDirty || !save) {
|
|
84
|
-
reset();
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
setIsSaving(true);
|
|
88
|
-
setError(null);
|
|
89
|
-
try {
|
|
90
|
-
await save(JSON.stringify(draft));
|
|
91
|
-
reset();
|
|
92
|
-
return true;
|
|
93
|
-
} catch (e) {
|
|
94
|
-
// Stay in edit mode so the work isn't lost to a failed write.
|
|
95
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
96
|
-
return false;
|
|
97
|
-
} finally {
|
|
98
|
-
setIsSaving(false);
|
|
99
|
-
}
|
|
100
|
-
}, [draft, isDirty, reset, save]);
|
|
101
|
-
return {
|
|
102
|
-
isEditing,
|
|
103
|
-
value,
|
|
104
|
-
isDirty,
|
|
105
|
-
isSaving,
|
|
106
|
-
error,
|
|
107
|
-
selectPath,
|
|
108
|
-
begin,
|
|
109
|
-
apply,
|
|
110
|
-
selectAt,
|
|
111
|
-
commit,
|
|
112
|
-
discard
|
|
113
|
-
};
|
|
19
|
+
const saveValue = useMemo(() => save ? value => save(JSON.stringify(value)) : undefined, [save]);
|
|
20
|
+
return useSharedTreeDraft(liveValue, saveValue);
|
|
114
21
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MutableRefObject } from "react";
|
|
2
|
-
import { RequiredStorageKey } from "../types";
|
|
2
|
+
import { StorageKeyInfo, RequiredStorageKey } from "../types";
|
|
3
3
|
import { type StorageFilterType, type StorageTypeFilter } from "./StorageFilterCards";
|
|
4
4
|
/**
|
|
5
5
|
* @deprecated Read the live limit with `useLimit("storage.keys")` — this only
|
|
@@ -47,7 +47,18 @@ interface GameUIStorageBrowserProps {
|
|
|
47
47
|
onStorageTypeFilterChange?: (type: StorageTypeFilter) => void;
|
|
48
48
|
/** Pad full-screen views past the home indicator (docked sheet only). */
|
|
49
49
|
applySafeAreaInset?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The key whose DETAIL screen is open, or null for the list. CONTROLLED by
|
|
52
|
+
* the host modal — the modal's header shows the back arrow and the key's
|
|
53
|
+
* name (the React Query interaction), so the modal must know. Only the
|
|
54
|
+
* identity matters; the info rendered is resolved fresh each pass.
|
|
55
|
+
*/
|
|
56
|
+
detailKey?: StorageKeyInfo | null;
|
|
57
|
+
/** A card was tapped — the host should set `detailKey`. */
|
|
58
|
+
onOpenKey?: (storageKey: StorageKeyInfo) => void;
|
|
59
|
+
/** The detail screen wants to close (a delete or hide landed). */
|
|
60
|
+
onCloseDetail?: () => void;
|
|
50
61
|
}
|
|
51
|
-
export declare function GameUIStorageBrowser({ requiredStorageKeys, showFilters, ignoredPatterns, onTogglePattern, onAddPattern, searchQuery, storageDataRef, eventCountByKey, lastWriteByKey, onViewHistory, enabledStorageTypes, statusFilter, onStatusFilterChange, onStatusCountsChange, pinnedKeys, onTogglePin, storageTypeFilter, onStorageTypeFilterChange, applySafeAreaInset, }: GameUIStorageBrowserProps): import("react").JSX.Element;
|
|
62
|
+
export declare function GameUIStorageBrowser({ requiredStorageKeys, showFilters, ignoredPatterns, onTogglePattern, onAddPattern, searchQuery, storageDataRef, eventCountByKey, lastWriteByKey, onViewHistory, enabledStorageTypes, statusFilter, onStatusFilterChange, onStatusCountsChange, pinnedKeys, onTogglePin, storageTypeFilter, onStorageTypeFilterChange, applySafeAreaInset, detailKey, onOpenKey, onCloseDetail, }: GameUIStorageBrowserProps): import("react").JSX.Element;
|
|
52
63
|
export {};
|
|
53
64
|
//# sourceMappingURL=GameUIStorageBrowser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GameUIStorageBrowser.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/GameUIStorageBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,gBAAgB,EACjB,MAAM,OAAO,CAAC;AAWf,OAAO,
|
|
1
|
+
{"version":3,"file":"GameUIStorageBrowser.d.ts","sourceRoot":"","sources":["../../../../src/storage/components/GameUIStorageBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,gBAAgB,EACjB,MAAM,OAAO,CAAC;AAWf,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAmB,MAAM,UAAU,CAAC;AAK/E,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AA4C9B;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAAmC,CAAC;AAEpE,UAAU,yBAAyB;IACjC,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,gBAAgB,CAAC,GAAG,EAAE,CAAC,CAAC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,mBAAmB,CAAC,EAAE,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;IACvD;;;;OAIG;IACH,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC,oBAAoB,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC3D,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,CAAC,MAAM,EAAE;QAC9B,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;KACrB,KAAK,IAAI,CAAC;IACX,yCAAyC;IACzC,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,kCAAkC;IAClC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,yBAAyB,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC9D,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,2DAA2D;IAC3D,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,IAAI,CAAC;IACjD,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,wBAAgB,oBAAoB,CAAC,EACnC,mBAAwB,EACxB,WAAmB,EACnB,eAA0C,EAC1C,eAAe,EACf,YAAY,EACZ,WAAgB,EAChB,cAAc,EACd,eAAe,EACf,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,oBAAoB,EACpB,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,yBAAyB,EACzB,kBAAkB,EAClB,SAAgB,EAChB,SAAS,EACT,aAAa,GACd,EAAE,yBAAyB,+BA8lC3B"}
|