@buoy-gg/storage 7.0.28 → 7.0.30
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 +51 -1
- package/lib/commonjs/storage/components/SelectionActionBar.js +10 -0
- package/lib/commonjs/storage/components/StorageActionButtons.js +16 -0
- package/lib/commonjs/storage/components/StorageEventActionButton.js +1 -0
- package/lib/commonjs/storage/components/StorageEventCard.js +5 -2
- package/lib/commonjs/storage/components/StorageEventFilterView.js +18 -0
- package/lib/commonjs/storage/components/StorageFilterCards.js +9 -0
- package/lib/commonjs/storage/components/StorageKeyEditorScreen.js +26 -5
- package/lib/commonjs/storage/components/StorageKeyRow.js +48 -1
- package/lib/commonjs/storage/components/StorageKeySection.js +23 -7
- package/lib/commonjs/storage/components/StorageModalWithTabs.js +26 -7
- package/lib/commonjs/storage/components/StorageScalarKeyEditor.js +16 -1
- package/lib/commonjs/storage/components/StorageTypeFilterBar.js +2 -0
- package/lib/commonjs/storage/components/StorageValueEditModal.js +18 -2
- package/lib/commonjs/storage/hooks/useTreeDraft.js +12 -1
- package/lib/commonjs/storage/utils/deleteStorageKeys.js +34 -5
- package/lib/module/storage/components/GameUIStorageBrowser.js +51 -1
- package/lib/module/storage/components/SelectionActionBar.js +10 -0
- package/lib/module/storage/components/StorageActionButtons.js +16 -0
- package/lib/module/storage/components/StorageEventActionButton.js +1 -0
- package/lib/module/storage/components/StorageEventCard.js +5 -2
- package/lib/module/storage/components/StorageEventFilterView.js +18 -0
- package/lib/module/storage/components/StorageFilterCards.js +9 -0
- package/lib/module/storage/components/StorageKeyEditorScreen.js +27 -6
- package/lib/module/storage/components/StorageKeyRow.js +49 -2
- package/lib/module/storage/components/StorageKeySection.js +23 -7
- package/lib/module/storage/components/StorageModalWithTabs.js +26 -7
- package/lib/module/storage/components/StorageScalarKeyEditor.js +16 -1
- package/lib/module/storage/components/StorageTypeFilterBar.js +2 -0
- package/lib/module/storage/components/StorageValueEditModal.js +18 -2
- package/lib/module/storage/hooks/useTreeDraft.js +12 -1
- package/lib/module/storage/utils/deleteStorageKeys.js +34 -5
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
- package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageActionButtons.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageEventActionButton.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageEventCard.d.ts +5 -0
- package/lib/typescript/storage/components/StorageEventCard.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageEventFilterView.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts.map +1 -1
- 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 +3 -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/StorageScalarKeyEditor.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts +9 -1
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -1
- package/lib/typescript/storage/utils/deleteStorageKeys.d.ts +15 -5
- package/lib/typescript/storage/utils/deleteStorageKeys.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -21,6 +21,21 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
21
21
|
* screen it came from.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The text `setStorageValue` should store.
|
|
26
|
+
*
|
|
27
|
+
* A string is stored AS ITSELF — `JSON.stringify` here was adding quotes a
|
|
28
|
+
* plain string never had. AsyncStorage hid it (the browser reads values back
|
|
29
|
+
* through `JSON.parse`, so the tool agreed with itself) but the app's own
|
|
30
|
+
* `getItem` saw `"dark"` where it had written `dark`; MMKV stores strings
|
|
31
|
+
* natively and showed the quotes right in the row, compounding on every edit.
|
|
32
|
+
*
|
|
33
|
+
* Everything else keeps JSON: for a number or a boolean that IS its text, and
|
|
34
|
+
* containers never reach this editor.
|
|
35
|
+
*/
|
|
36
|
+
function serializeScalar(value) {
|
|
37
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
38
|
+
}
|
|
24
39
|
function StorageScalarKeyEditor({
|
|
25
40
|
storageKey,
|
|
26
41
|
value,
|
|
@@ -31,7 +46,7 @@ function StorageScalarKeyEditor({
|
|
|
31
46
|
const save = (0, _react.useCallback)(next => {
|
|
32
47
|
if (saving) return;
|
|
33
48
|
setSaving(true);
|
|
34
|
-
onSave(
|
|
49
|
+
onSave(serializeScalar(next)).then(onClose).catch(e => {
|
|
35
50
|
setSaving(false);
|
|
36
51
|
_reactNative.Alert.alert("Couldn't save", e instanceof Error ? e.message : String(e));
|
|
37
52
|
});
|
|
@@ -57,6 +57,8 @@ function StorageTypeFilterBar({
|
|
|
57
57
|
const isActive = active === segment.id;
|
|
58
58
|
const accent = segment.color ?? _sharedUi.night.color.accent;
|
|
59
59
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
|
|
60
|
+
testID: `buoy-storage-type-${segment.id}`,
|
|
61
|
+
accessibilityRole: "button",
|
|
60
62
|
accessibilityLabel: `Filter by ${segment.label}`,
|
|
61
63
|
accessibilityState: {
|
|
62
64
|
selected: isActive
|
|
@@ -86,6 +86,8 @@ function StorageValueEditModal({
|
|
|
86
86
|
style: styles.scrim,
|
|
87
87
|
activeOpacity: 1,
|
|
88
88
|
onPress: busy ? undefined : onCancel,
|
|
89
|
+
testID: "buoy-storage-value-scrim",
|
|
90
|
+
accessibilityRole: "button",
|
|
89
91
|
accessibilityLabel: "Dismiss editor"
|
|
90
92
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
91
93
|
style: styles.panel,
|
|
@@ -115,6 +117,8 @@ function StorageValueEditModal({
|
|
|
115
117
|
style: styles.fieldScroll,
|
|
116
118
|
keyboardShouldPersistTaps: "handled",
|
|
117
119
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
|
|
120
|
+
testID: "buoy-storage-value-input",
|
|
121
|
+
accessibilityLabel: "Value",
|
|
118
122
|
value: text,
|
|
119
123
|
onChangeText: setText,
|
|
120
124
|
autoFocus: true,
|
|
@@ -125,8 +129,14 @@ function StorageValueEditModal({
|
|
|
125
129
|
// Multiline for anything textual: stored values are long more
|
|
126
130
|
// often than they're short, and a single line hides that.
|
|
127
131
|
,
|
|
128
|
-
multiline: mode !== "number"
|
|
129
|
-
|
|
132
|
+
multiline: mode !== "number"
|
|
133
|
+
// ASCII for raw JSON, because iOS Smart Punctuation is ON by
|
|
134
|
+
// default and rewrites the quotes you type as curly ones —
|
|
135
|
+
// which makes the text unparseable, disables Save, and leaves
|
|
136
|
+
// an "invalid JSON" message whose cause is invisible at a
|
|
137
|
+
// glance. An ASCII keyboard doesn't do the substitution.
|
|
138
|
+
,
|
|
139
|
+
keyboardType: mode === "number" ? "numeric" : mode === "raw" ? "ascii-capable" : "default",
|
|
130
140
|
onSubmitEditing: mode === "number" ? commit : undefined,
|
|
131
141
|
placeholder: mode === "raw" ? "{ }" : "value",
|
|
132
142
|
placeholderTextColor: _sharedUi.night.color.placeholder,
|
|
@@ -140,6 +150,8 @@ function StorageValueEditModal({
|
|
|
140
150
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
141
151
|
onPress: onCancel,
|
|
142
152
|
disabled: busy,
|
|
153
|
+
testID: "buoy-storage-value-cancel",
|
|
154
|
+
accessibilityRole: "button",
|
|
143
155
|
accessibilityLabel: "Cancel edit",
|
|
144
156
|
style: styles.cancelButton,
|
|
145
157
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -149,6 +161,8 @@ function StorageValueEditModal({
|
|
|
149
161
|
}), mode === "boolean" ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
150
162
|
onPress: commit,
|
|
151
163
|
disabled: blocked,
|
|
164
|
+
testID: "buoy-storage-value-save",
|
|
165
|
+
accessibilityRole: "button",
|
|
152
166
|
accessibilityLabel: saveLabel,
|
|
153
167
|
style: [styles.saveButton, blocked && styles.saveDisabled],
|
|
154
168
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -169,6 +183,8 @@ function BooleanChoice({
|
|
|
169
183
|
style: styles.boolRow,
|
|
170
184
|
children: [true, false].map(option => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
171
185
|
onPress: () => onPick(option),
|
|
186
|
+
testID: `buoy-storage-value-${option}`,
|
|
187
|
+
accessibilityRole: "button",
|
|
172
188
|
accessibilityLabel: `Set ${option}`,
|
|
173
189
|
style: [styles.boolButton, current === option && styles.boolActive],
|
|
174
190
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -66,12 +66,23 @@ function useTreeDraft(liveValue, save) {
|
|
|
66
66
|
const selectAt = (0, _react.useCallback)(path => {
|
|
67
67
|
setSelectPath(path);
|
|
68
68
|
}, []);
|
|
69
|
+
|
|
70
|
+
/** Leave edit mode entirely. Only correct on the way out (after a write). */
|
|
69
71
|
const reset = (0, _react.useCallback)(() => {
|
|
70
72
|
setIsEditing(false);
|
|
71
73
|
setDraft(undefined);
|
|
72
74
|
setSelectPath(undefined);
|
|
73
75
|
setError(null);
|
|
74
76
|
}, []);
|
|
77
|
+
|
|
78
|
+
/** Re-seed the draft from what's stored, staying in edit mode. */
|
|
79
|
+
const discard = (0, _react.useCallback)(() => {
|
|
80
|
+
setDraft(liveValue);
|
|
81
|
+
setBaseline(JSON.stringify(liveValue));
|
|
82
|
+
setSelectPath(undefined);
|
|
83
|
+
setError(null);
|
|
84
|
+
setIsEditing(true);
|
|
85
|
+
}, [liveValue]);
|
|
75
86
|
const commit = (0, _react.useCallback)(async () => {
|
|
76
87
|
if (!isDirty || !save) {
|
|
77
88
|
reset();
|
|
@@ -102,6 +113,6 @@ function useTreeDraft(liveValue, save) {
|
|
|
102
113
|
apply,
|
|
103
114
|
selectAt,
|
|
104
115
|
commit,
|
|
105
|
-
discard
|
|
116
|
+
discard
|
|
106
117
|
};
|
|
107
118
|
}
|
|
@@ -11,15 +11,44 @@ var _SecureStoreRegistry = require("./SecureStoreRegistry");
|
|
|
11
11
|
*
|
|
12
12
|
* Each backend removes keys differently — AsyncStorage takes a batch,
|
|
13
13
|
* SecureStore has to go through the registry (only it knows each key's
|
|
14
|
-
* `keychainService` options), and MMKV deletes per registered instance.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* `keychainService` options), and MMKV deletes per registered instance. A
|
|
15
|
+
* second copy of that fan-out is exactly how one backend quietly stops being
|
|
16
|
+
* cleaned up — which is not hypothetical: the MMKV branch called a method
|
|
17
|
+
* react-native-mmkv v4 had renamed, and deleting an MMKV key silently did
|
|
18
|
+
* nothing until an e2e test deleted from all three backends at once.
|
|
19
|
+
*
|
|
20
|
+
* One call site today: the selection action bar. Deleting is deliberately not
|
|
21
|
+
* an affordance on the row itself — see `StorageKeyRow`, whose expanded card
|
|
22
|
+
* carries three actions and only one of them changes anything.
|
|
18
23
|
*
|
|
19
24
|
* Pro gating and confirmation are deliberately NOT here: they belong to the UI
|
|
20
25
|
* that has the user's attention. This function assumes the caller already asked.
|
|
21
26
|
*/
|
|
22
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Remove one key from an MMKV instance, whichever version it is.
|
|
30
|
+
*
|
|
31
|
+
* react-native-mmkv v4 renamed `delete(key)` to `remove(key)`. Calling the old
|
|
32
|
+
* name on a v4 instance throws, and because the callers wrap deletion in a
|
|
33
|
+
* try/catch that only surfaces an alert, the visible result was a key that
|
|
34
|
+
* refused to delete — the same shape as a permissions problem or a read-only
|
|
35
|
+
* instance, and nothing to point at the real cause.
|
|
36
|
+
*
|
|
37
|
+
* `storageSyncAdapter` and `snapshotProvider` already did this; this path was
|
|
38
|
+
* the one that missed it.
|
|
39
|
+
*/
|
|
40
|
+
function removeMMKVKey(instance, key) {
|
|
41
|
+
if (typeof instance.remove === "function") {
|
|
42
|
+
instance.remove(key);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (typeof instance.delete === "function") {
|
|
46
|
+
instance.delete(key);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
throw new Error("MMKV instance exposes neither remove() nor delete()");
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
/**
|
|
24
53
|
* Removes every key from whichever backend holds it. Rejects on the first
|
|
25
54
|
* failure — callers surface that, rather than reporting a partial delete as
|
|
@@ -46,7 +75,7 @@ async function deleteStorageKeys(keys, mmkvInstances) {
|
|
|
46
75
|
Object.entries(byInstance).forEach(([instanceId, instanceKeys]) => {
|
|
47
76
|
const inst = mmkvInstances.find(i => i.id === instanceId);
|
|
48
77
|
if (inst) {
|
|
49
|
-
instanceKeys.forEach(key => inst.instance
|
|
78
|
+
instanceKeys.forEach(key => removeMMKVKey(inst.instance, key));
|
|
50
79
|
}
|
|
51
80
|
});
|
|
52
81
|
}
|
|
@@ -19,6 +19,7 @@ import { SelectionActionBar } from "./SelectionActionBar";
|
|
|
19
19
|
// instance, including remote-backed wrappers on the desktop dashboard.
|
|
20
20
|
import { addMMKVListener } from "../utils/MMKVListener";
|
|
21
21
|
import { onStorageBrowserRefresh } from "../utils/storageBrowserRefresh";
|
|
22
|
+
import { deleteStorageKeys } from "../utils/deleteStorageKeys";
|
|
22
23
|
import { setStorageValue, storageEditBlockedReason } from "../utils/setStorageValue";
|
|
23
24
|
|
|
24
25
|
// Import shared Game UI components
|
|
@@ -254,6 +255,42 @@ export function GameUIStorageBrowser({
|
|
|
254
255
|
});
|
|
255
256
|
}, [handleSaveValue]);
|
|
256
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Delete ONE key, from its own card.
|
|
260
|
+
*
|
|
261
|
+
* Routed through `deleteStorageKeys` exactly like the selection bar, so the
|
|
262
|
+
* per-backend fan-out keeps having a single implementation — the last time
|
|
263
|
+
* that fan-out was wrong (an MMKV method renamed upstream) it was wrong in
|
|
264
|
+
* silence, and a second copy is how one backend quietly stops being deleted.
|
|
265
|
+
*
|
|
266
|
+
* Pro is checked here rather than on the button because it's an upsell, not
|
|
267
|
+
* a capability: the row shows the action and explains the wall on press.
|
|
268
|
+
*/
|
|
269
|
+
const handleDeleteKey = useCallback(storageKey => {
|
|
270
|
+
if (!isPro) {
|
|
271
|
+
setShowUpgradeModal(true);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
Alert.alert("Delete key", `Delete "${storageKey.key}"? This cannot be undone.`, [{
|
|
275
|
+
text: "Cancel",
|
|
276
|
+
style: "cancel"
|
|
277
|
+
}, {
|
|
278
|
+
text: "Delete",
|
|
279
|
+
style: "destructive",
|
|
280
|
+
onPress: async () => {
|
|
281
|
+
try {
|
|
282
|
+
await deleteStorageKeys([storageKey], mmkvInstances.map(inst => ({
|
|
283
|
+
id: inst.id,
|
|
284
|
+
instance: inst.instance
|
|
285
|
+
})));
|
|
286
|
+
refresh();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
Alert.alert("Error", `Failed to delete key: ${error}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}]);
|
|
292
|
+
}, [isPro, mmkvInstances, refresh]);
|
|
293
|
+
|
|
257
294
|
/** Permanent, per-key reasons a value can't be edited (never the licence). */
|
|
258
295
|
const editBlockedFor = useCallback(storageKey => storageEditBlockedReason(storageKey, mmkvInstances), [mmkvInstances]);
|
|
259
296
|
|
|
@@ -642,6 +679,9 @@ export function GameUIStorageBrowser({
|
|
|
642
679
|
contentContainerStyle: styles.instanceNavbarScroll,
|
|
643
680
|
children: [(isPro || mmkvInstances.length === 1) && /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
644
681
|
onPress: () => setSelectedMMKVInstance(null),
|
|
682
|
+
testID: "buoy-storage-instance-all",
|
|
683
|
+
accessibilityRole: "button",
|
|
684
|
+
accessibilityLabel: "All MMKV instances",
|
|
645
685
|
style: [styles.instanceNavbarButton, selectedMMKVInstance === null && styles.instanceNavbarButtonActive],
|
|
646
686
|
children: [/*#__PURE__*/_jsx(HardDrive, {
|
|
647
687
|
size: 12,
|
|
@@ -659,6 +699,9 @@ export function GameUIStorageBrowser({
|
|
|
659
699
|
}), mmkvInstances.map((inst, index) => {
|
|
660
700
|
const isLocked = index >= mmkvInstanceLimit;
|
|
661
701
|
return /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
702
|
+
testID: `buoy-storage-instance-${inst.id}`,
|
|
703
|
+
accessibilityRole: "button",
|
|
704
|
+
accessibilityLabel: `MMKV instance ${inst.id}`,
|
|
662
705
|
onPress: () => {
|
|
663
706
|
if (isLocked) {
|
|
664
707
|
setShowUpgradeModal(true);
|
|
@@ -726,6 +769,8 @@ export function GameUIStorageBrowser({
|
|
|
726
769
|
style: styles.limitBanner,
|
|
727
770
|
onPress: () => setShowUpgradeModal(true),
|
|
728
771
|
activeOpacity: 0.8,
|
|
772
|
+
testID: "buoy-storage-locked-banner",
|
|
773
|
+
accessibilityRole: "button",
|
|
729
774
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
730
775
|
style: styles.limitBannerContent,
|
|
731
776
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
@@ -763,7 +808,11 @@ export function GameUIStorageBrowser({
|
|
|
763
808
|
});
|
|
764
809
|
return /*#__PURE__*/_jsxs(View, {
|
|
765
810
|
style: styles.root,
|
|
766
|
-
children: [/*#__PURE__*/_jsxs(ScrollView
|
|
811
|
+
children: [/*#__PURE__*/_jsxs(ScrollView
|
|
812
|
+
// Named because `by.type("RCTScrollView")` doesn't match on Fabric
|
|
813
|
+
// (renamed class) — automation scrolls below-the-fold rows by this id.
|
|
814
|
+
, {
|
|
815
|
+
testID: "buoy-storage-scroll",
|
|
767
816
|
style: styles.scrollContainer,
|
|
768
817
|
contentContainerStyle: styles.container,
|
|
769
818
|
showsVerticalScrollIndicator: false,
|
|
@@ -813,6 +862,7 @@ export function GameUIStorageBrowser({
|
|
|
813
862
|
onHideKey: key => handleHideKeys([key]),
|
|
814
863
|
onEditKey: setEditingKey,
|
|
815
864
|
onToggleKey: handleToggleKey,
|
|
865
|
+
onDeleteKey: handleDeleteKey,
|
|
816
866
|
editBlockedFor: editBlockedFor,
|
|
817
867
|
pinnedKeys: pinnedKeys,
|
|
818
868
|
onTogglePin: onTogglePin
|
|
@@ -83,6 +83,9 @@ export function SelectionActionBar({
|
|
|
83
83
|
children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
84
84
|
style: styles.selectAllButton,
|
|
85
85
|
onPress: allSelected ? onClearSelection : onSelectAll,
|
|
86
|
+
testID: "buoy-storage-select-all",
|
|
87
|
+
accessibilityLabel: allSelected ? "Deselect all keys" : "Select all keys",
|
|
88
|
+
accessibilityRole: "button",
|
|
86
89
|
children: [/*#__PURE__*/_jsx(CheckSquare, {
|
|
87
90
|
size: 14,
|
|
88
91
|
color: allSelected ? night.color.accent : night.color.textSecondary
|
|
@@ -102,6 +105,7 @@ export function SelectionActionBar({
|
|
|
102
105
|
children: [/*#__PURE__*/_jsx(CopyButton, {
|
|
103
106
|
value: getCopyData(),
|
|
104
107
|
size: 14,
|
|
108
|
+
testID: "buoy-storage-selection-copy",
|
|
105
109
|
buttonStyle: styles.actionButton,
|
|
106
110
|
colors: {
|
|
107
111
|
idle: night.color.accent,
|
|
@@ -111,6 +115,9 @@ export function SelectionActionBar({
|
|
|
111
115
|
}), onHideKeys && /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
112
116
|
style: styles.actionButton,
|
|
113
117
|
onPress: handleHideSelected,
|
|
118
|
+
testID: "buoy-storage-selection-hide",
|
|
119
|
+
accessibilityLabel: "Hide selected keys",
|
|
120
|
+
accessibilityRole: "button",
|
|
114
121
|
children: /*#__PURE__*/_jsx(EyeOff, {
|
|
115
122
|
size: 14,
|
|
116
123
|
color: night.color.warning
|
|
@@ -118,6 +125,9 @@ export function SelectionActionBar({
|
|
|
118
125
|
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
119
126
|
style: styles.actionButton,
|
|
120
127
|
onPress: handleDeleteSelected,
|
|
128
|
+
testID: "buoy-storage-selection-delete",
|
|
129
|
+
accessibilityLabel: "Delete selected keys",
|
|
130
|
+
accessibilityRole: "button",
|
|
121
131
|
children: /*#__PURE__*/_jsx(Trash2, {
|
|
122
132
|
size: 14,
|
|
123
133
|
color: night.color.danger
|
|
@@ -129,6 +129,9 @@ export function StorageActionButtons({
|
|
|
129
129
|
style: styles.container,
|
|
130
130
|
children: [onToggleSelectMode && /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
131
131
|
onPress: onToggleSelectMode,
|
|
132
|
+
testID: "buoy-storage-select-mode",
|
|
133
|
+
accessibilityLabel: isSelectMode ? "Exit selection mode" : "Select keys",
|
|
134
|
+
accessibilityRole: "button",
|
|
132
135
|
style: [styles.actionButton, isSelectMode && styles.actionButtonActive],
|
|
133
136
|
children: isSelectMode ? /*#__PURE__*/_jsxs(View, {
|
|
134
137
|
style: styles.selectModeActiveContainer,
|
|
@@ -146,6 +149,7 @@ export function StorageActionButtons({
|
|
|
146
149
|
}), !isSelectMode && /*#__PURE__*/_jsx(CopyButton, {
|
|
147
150
|
value: copyValue,
|
|
148
151
|
size: 16,
|
|
152
|
+
testID: "buoy-storage-export-copy",
|
|
149
153
|
buttonStyle: styles.actionButton,
|
|
150
154
|
colors: {
|
|
151
155
|
idle: night.color.textSecondary,
|
|
@@ -154,6 +158,9 @@ export function StorageActionButtons({
|
|
|
154
158
|
}
|
|
155
159
|
}), !isSelectMode && showClearAll && /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
156
160
|
onPress: handleClearAll,
|
|
161
|
+
testID: "buoy-storage-clear-all",
|
|
162
|
+
accessibilityLabel: "Clear all storage",
|
|
163
|
+
accessibilityRole: "button",
|
|
157
164
|
style: styles.actionButton,
|
|
158
165
|
children: /*#__PURE__*/_jsx(Trash2, {
|
|
159
166
|
size: 16,
|
|
@@ -161,6 +168,9 @@ export function StorageActionButtons({
|
|
|
161
168
|
})
|
|
162
169
|
}), !isSelectMode && showClearAsync && /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
163
170
|
onPress: handleClearAsyncStorage,
|
|
171
|
+
testID: "buoy-storage-clear-async",
|
|
172
|
+
accessibilityLabel: "Clear AsyncStorage",
|
|
173
|
+
accessibilityRole: "button",
|
|
164
174
|
style: styles.actionButton,
|
|
165
175
|
children: /*#__PURE__*/_jsx(Trash2, {
|
|
166
176
|
size: 16,
|
|
@@ -168,6 +178,9 @@ export function StorageActionButtons({
|
|
|
168
178
|
})
|
|
169
179
|
}), !isSelectMode && showClearAllMMKV && /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
170
180
|
onPress: handleClearAllMMKV,
|
|
181
|
+
testID: "buoy-storage-clear-mmkv-all",
|
|
182
|
+
accessibilityLabel: "Clear all MMKV instances",
|
|
183
|
+
accessibilityRole: "button",
|
|
171
184
|
style: styles.actionButton,
|
|
172
185
|
children: /*#__PURE__*/_jsx(Trash2, {
|
|
173
186
|
size: 16,
|
|
@@ -175,6 +188,9 @@ export function StorageActionButtons({
|
|
|
175
188
|
})
|
|
176
189
|
}), !isSelectMode && showClearMMKVIndividual && mmkvInstances.length === 1 && /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
177
190
|
onPress: () => handleClearMMKV(mmkvInstances[0].id, mmkvInstances[0].id),
|
|
191
|
+
testID: "buoy-storage-clear-mmkv",
|
|
192
|
+
accessibilityLabel: "Clear MMKV instance",
|
|
193
|
+
accessibilityRole: "button",
|
|
178
194
|
style: styles.actionButton,
|
|
179
195
|
children: /*#__PURE__*/_jsx(Trash2, {
|
|
180
196
|
size: 16,
|
|
@@ -77,7 +77,8 @@ export const StorageEventCard = /*#__PURE__*/memo(function StorageEventCard({
|
|
|
77
77
|
data,
|
|
78
78
|
onPress,
|
|
79
79
|
isSelected = false,
|
|
80
|
-
showChevron = true
|
|
80
|
+
showChevron = true,
|
|
81
|
+
testID
|
|
81
82
|
}) {
|
|
82
83
|
const actionColor = getActionColor(data.lastAction);
|
|
83
84
|
const actionLabel = translateStorageAction(data.lastAction);
|
|
@@ -104,6 +105,8 @@ export const StorageEventCard = /*#__PURE__*/memo(function StorageEventCard({
|
|
|
104
105
|
// Interaction
|
|
105
106
|
,
|
|
106
107
|
isSelected: isSelected,
|
|
107
|
-
onPress: onPress
|
|
108
|
+
onPress: onPress,
|
|
109
|
+
testID: testID,
|
|
110
|
+
accessibilityLabel: `${actionLabel} ${data.key}`
|
|
108
111
|
});
|
|
109
112
|
});
|
|
@@ -49,6 +49,12 @@ export function StorageEventFilterView({
|
|
|
49
49
|
borderColor: macOSColors.semantic.warning + '40'
|
|
50
50
|
}],
|
|
51
51
|
onPress: () => onToggleStorageType('async'),
|
|
52
|
+
testID: "buoy-storage-backend-async",
|
|
53
|
+
accessibilityRole: "button",
|
|
54
|
+
accessibilityLabel: "Toggle AsyncStorage capture",
|
|
55
|
+
accessibilityState: {
|
|
56
|
+
selected: enabledStorageTypes.has('async')
|
|
57
|
+
},
|
|
52
58
|
children: [/*#__PURE__*/_jsx(Database, {
|
|
53
59
|
size: 16,
|
|
54
60
|
color: enabledStorageTypes.has('async') ? macOSColors.semantic.warning : night.color.textTertiary
|
|
@@ -63,6 +69,12 @@ export function StorageEventFilterView({
|
|
|
63
69
|
borderColor: macOSColors.semantic.info + '40'
|
|
64
70
|
}],
|
|
65
71
|
onPress: () => onToggleStorageType('mmkv'),
|
|
72
|
+
testID: "buoy-storage-backend-mmkv",
|
|
73
|
+
accessibilityRole: "button",
|
|
74
|
+
accessibilityLabel: "Toggle MMKV capture",
|
|
75
|
+
accessibilityState: {
|
|
76
|
+
selected: enabledStorageTypes.has('mmkv')
|
|
77
|
+
},
|
|
66
78
|
children: [/*#__PURE__*/_jsx(HardDrive, {
|
|
67
79
|
size: 16,
|
|
68
80
|
color: enabledStorageTypes.has('mmkv') ? macOSColors.semantic.info : night.color.textTertiary
|
|
@@ -77,6 +89,12 @@ export function StorageEventFilterView({
|
|
|
77
89
|
borderColor: macOSColors.semantic.success + '40'
|
|
78
90
|
}],
|
|
79
91
|
onPress: () => onToggleStorageType('secure'),
|
|
92
|
+
testID: "buoy-storage-backend-secure",
|
|
93
|
+
accessibilityRole: "button",
|
|
94
|
+
accessibilityLabel: "Toggle Secure Storage capture",
|
|
95
|
+
accessibilityState: {
|
|
96
|
+
selected: enabledStorageTypes.has('secure')
|
|
97
|
+
},
|
|
80
98
|
children: [/*#__PURE__*/_jsx(Shield, {
|
|
81
99
|
size: 16,
|
|
82
100
|
color: enabledStorageTypes.has('secure') ? macOSColors.semantic.success : night.color.textTertiary
|
|
@@ -44,6 +44,9 @@ export function StorageFilterCards({
|
|
|
44
44
|
}]],
|
|
45
45
|
onPress: () => onFilterChange?.("all"),
|
|
46
46
|
activeOpacity: 0.8,
|
|
47
|
+
testID: "buoy-storage-status-all",
|
|
48
|
+
accessibilityRole: "button",
|
|
49
|
+
accessibilityLabel: "Valid keys",
|
|
47
50
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
48
51
|
style: [styles.filterValue, {
|
|
49
52
|
color: night.color.text
|
|
@@ -61,6 +64,9 @@ export function StorageFilterCards({
|
|
|
61
64
|
}]],
|
|
62
65
|
onPress: () => onFilterChange?.("missing"),
|
|
63
66
|
activeOpacity: 0.8,
|
|
67
|
+
testID: "buoy-storage-status-missing",
|
|
68
|
+
accessibilityRole: "button",
|
|
69
|
+
accessibilityLabel: "Missing keys",
|
|
64
70
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
65
71
|
style: [styles.filterValue, {
|
|
66
72
|
color: displayStats.missingCount > 0 ? macOSColors.semantic.error : night.color.textTertiary
|
|
@@ -78,6 +84,9 @@ export function StorageFilterCards({
|
|
|
78
84
|
}]],
|
|
79
85
|
onPress: () => onFilterChange?.("issues"),
|
|
80
86
|
activeOpacity: 0.8,
|
|
87
|
+
testID: "buoy-storage-status-issues",
|
|
88
|
+
accessibilityRole: "button",
|
|
89
|
+
accessibilityLabel: "Issues keys",
|
|
81
90
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
82
91
|
style: [styles.filterValue, {
|
|
83
92
|
color: displayStats.issuesCount > 0 ? macOSColors.semantic.warning : night.color.textTertiary
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { useMemo } from "react";
|
|
21
21
|
import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
22
|
-
import { macOSColors, night, FileJson, PillBadge, useSafeAreaInsets, X } from "@buoy-gg/shared-ui";
|
|
22
|
+
import { macOSColors, night, FileJson, PillBadge, ToolBackground, useSafeAreaInsets, X } from "@buoy-gg/shared-ui";
|
|
23
23
|
import { DataViewer, DataTreeActionDock, getAtPath } from "@buoy-gg/shared-ui/dataViewer";
|
|
24
24
|
import { useKeyEditor } from "../hooks/useKeyEditor";
|
|
25
25
|
import { StorageValueEditModal } from "./StorageValueEditModal";
|
|
@@ -88,7 +88,7 @@ function StorageTreeEditor({
|
|
|
88
88
|
} = editor;
|
|
89
89
|
return /*#__PURE__*/_jsxs(View, {
|
|
90
90
|
style: styles.screen,
|
|
91
|
-
children: [/*#__PURE__*/_jsx(EditorHeader, {
|
|
91
|
+
children: [/*#__PURE__*/_jsx(ToolBackground, {}), /*#__PURE__*/_jsx(EditorHeader, {
|
|
92
92
|
storageKey: storageKey,
|
|
93
93
|
state: draft.isSaving ? "Saving…" : draft.isDirty ? "Draft · unsaved" : "In sync with device",
|
|
94
94
|
dirty: draft.isDirty,
|
|
@@ -107,6 +107,7 @@ function StorageTreeEditor({
|
|
|
107
107
|
children: summarize(draft.value)
|
|
108
108
|
})]
|
|
109
109
|
}), /*#__PURE__*/_jsxs(ScrollView, {
|
|
110
|
+
testID: "buoy-storage-editor-scroll",
|
|
110
111
|
style: styles.treeScroll,
|
|
111
112
|
contentContainerStyle: styles.treeContent,
|
|
112
113
|
keyboardShouldPersistTaps: "handled",
|
|
@@ -124,7 +125,11 @@ function StorageTreeEditor({
|
|
|
124
125
|
,
|
|
125
126
|
showTypeFilter: false,
|
|
126
127
|
onSelectionChange: editor.setSelection,
|
|
127
|
-
selectPath: draft.selectPath
|
|
128
|
+
selectPath: draft.selectPath
|
|
129
|
+
// Its own handle root: the card that opened this editor is still
|
|
130
|
+
// mounted underneath, rendering the same value in its preview tree.
|
|
131
|
+
,
|
|
132
|
+
testIDPrefix: "buoy-storage-editor-row",
|
|
128
133
|
onRequestInlineEdit: sel => editor.openValueEditor(sel.path)
|
|
129
134
|
})
|
|
130
135
|
}), /*#__PURE__*/_jsx(Text, {
|
|
@@ -152,6 +157,8 @@ function StorageTreeEditor({
|
|
|
152
157
|
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
153
158
|
onPress: draft.discard,
|
|
154
159
|
disabled: draft.isSaving,
|
|
160
|
+
testID: "buoy-storage-editor-discard",
|
|
161
|
+
accessibilityRole: "button",
|
|
155
162
|
accessibilityLabel: "Discard changes",
|
|
156
163
|
style: styles.discardButton,
|
|
157
164
|
children: /*#__PURE__*/_jsx(Text, {
|
|
@@ -161,6 +168,8 @@ function StorageTreeEditor({
|
|
|
161
168
|
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
162
169
|
onPress: editor.saveAndClose,
|
|
163
170
|
disabled: draft.isSaving,
|
|
171
|
+
testID: "buoy-storage-editor-save",
|
|
172
|
+
accessibilityRole: "button",
|
|
164
173
|
accessibilityLabel: "Save changes",
|
|
165
174
|
style: [styles.saveButton, draft.isSaving && styles.saveDisabled],
|
|
166
175
|
children: /*#__PURE__*/_jsx(Text, {
|
|
@@ -196,6 +205,8 @@ function EditorHeader({
|
|
|
196
205
|
style: styles.header,
|
|
197
206
|
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
198
207
|
onPress: onClose,
|
|
208
|
+
testID: "buoy-storage-editor-close",
|
|
209
|
+
accessibilityRole: "button",
|
|
199
210
|
accessibilityLabel: "Close editor",
|
|
200
211
|
style: styles.headerButton,
|
|
201
212
|
hitSlop: {
|
|
@@ -210,7 +221,13 @@ function EditorHeader({
|
|
|
210
221
|
})
|
|
211
222
|
}), /*#__PURE__*/_jsxs(View, {
|
|
212
223
|
style: styles.headerTitle,
|
|
213
|
-
children: [/*#__PURE__*/_jsx(Text
|
|
224
|
+
children: [/*#__PURE__*/_jsx(Text
|
|
225
|
+
// Which key this editor is pointed at. Worth a handle of its own:
|
|
226
|
+
// the editor is reached by expanding a card in a list of hundreds,
|
|
227
|
+
// and opening it on the wrong one looks exactly like opening it on
|
|
228
|
+
// the right one until a write lands somewhere unexpected.
|
|
229
|
+
, {
|
|
230
|
+
testID: "buoy-storage-editor-key",
|
|
214
231
|
style: styles.headerKey,
|
|
215
232
|
numberOfLines: 1,
|
|
216
233
|
children: storageKey.key
|
|
@@ -220,6 +237,8 @@ function EditorHeader({
|
|
|
220
237
|
})]
|
|
221
238
|
}), /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
222
239
|
onPress: onRaw,
|
|
240
|
+
testID: "buoy-storage-editor-raw",
|
|
241
|
+
accessibilityRole: "button",
|
|
223
242
|
accessibilityLabel: "Edit raw JSON",
|
|
224
243
|
style: styles.headerButton,
|
|
225
244
|
hitSlop: {
|
|
@@ -265,8 +284,10 @@ function storageTypeColor(storageType) {
|
|
|
265
284
|
const styles = StyleSheet.create({
|
|
266
285
|
screen: {
|
|
267
286
|
flex: 1,
|
|
268
|
-
//
|
|
269
|
-
|
|
287
|
+
// Opaque on purpose: transparency here showed the whole key list through
|
|
288
|
+
// the editor, not just the sheet's star field. The screen paints its own
|
|
289
|
+
// ToolBackground instead — see the mount site above.
|
|
290
|
+
backgroundColor: night.color.bg
|
|
270
291
|
},
|
|
271
292
|
header: {
|
|
272
293
|
flexDirection: "row",
|