@buoy-gg/storage 7.0.30 → 7.0.32

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.
Files changed (41) hide show
  1. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +92 -21
  2. package/lib/commonjs/storage/components/StorageBrowserMode.js +8 -2
  3. package/lib/commonjs/storage/components/StorageKeyDetailScreen.js +572 -0
  4. package/lib/commonjs/storage/components/StorageKeyRow.js +21 -2
  5. package/lib/commonjs/storage/components/StorageKeySection.js +44 -21
  6. package/lib/commonjs/storage/components/StorageModalWithTabs.js +25 -2
  7. package/lib/commonjs/storage/components/StorageSaveNotice.js +171 -0
  8. package/lib/commonjs/storage/components/StorageValueEditModal.js +11 -364
  9. package/lib/commonjs/storage/hooks/useKeyEditor.js +12 -85
  10. package/lib/commonjs/storage/hooks/useTreeDraft.js +7 -100
  11. package/lib/module/storage/components/GameUIStorageBrowser.js +94 -23
  12. package/lib/module/storage/components/StorageBrowserMode.js +8 -2
  13. package/lib/module/storage/components/StorageKeyDetailScreen.js +570 -0
  14. package/lib/module/storage/components/StorageKeyRow.js +20 -3
  15. package/lib/module/storage/components/StorageKeySection.js +43 -21
  16. package/lib/module/storage/components/StorageModalWithTabs.js +26 -3
  17. package/lib/module/storage/components/StorageSaveNotice.js +168 -0
  18. package/lib/module/storage/components/StorageValueEditModal.js +13 -366
  19. package/lib/module/storage/hooks/useKeyEditor.js +14 -87
  20. package/lib/module/storage/hooks/useTreeDraft.js +9 -102
  21. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +13 -2
  22. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  23. package/lib/typescript/storage/components/StorageBrowserMode.d.ts +7 -2
  24. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
  25. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts +46 -0
  26. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts.map +1 -0
  27. package/lib/typescript/storage/components/StorageKeyRow.d.ts +9 -0
  28. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  29. package/lib/typescript/storage/components/StorageKeySection.d.ts +19 -1
  30. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  31. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  32. package/lib/typescript/storage/components/StorageSaveNotice.d.ts +6 -0
  33. package/lib/typescript/storage/components/StorageSaveNotice.d.ts.map +1 -0
  34. package/lib/typescript/storage/components/StorageValueEditModal.d.ts +9 -31
  35. package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -1
  36. package/lib/typescript/storage/hooks/useKeyEditor.d.ts +7 -31
  37. package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -1
  38. package/lib/typescript/storage/hooks/useTreeDraft.d.ts +7 -66
  39. package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -1
  40. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +1 -1
  41. package/package.json +5 -5
@@ -1,18 +1,19 @@
1
1
  "use strict";
2
2
 
3
- import { useMemo, useCallback, useState, useEffect } from "react";
3
+ import { useMemo, useCallback, useState, useEffect, useRef } from "react";
4
4
  import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Alert } from "react-native";
5
5
  import { absoluteFill, useLimit, TIER_LIMITS } from "@buoy-gg/shared-ui";
6
6
  import { Database, Zap, ProBadge, ProUpgradeModal } from "@buoy-gg/shared-ui";
7
7
  import { isDevToolsStorageKey } from "@buoy-gg/shared-ui";
8
- import { StorageKeySection } from "./StorageKeySection";
9
- import { StorageKeyEditorScreen } from "./StorageKeyEditorScreen";
8
+ import { StorageKeySection, storageRowId } from "./StorageKeySection";
9
+ import { StorageKeyDetailScreen } from "./StorageKeyDetailScreen";
10
10
  import { StorageFilterCards } from "./StorageFilterCards";
11
11
  import { useAsyncStorageKeys } from "../hooks/useAsyncStorageKeys";
12
12
  import { addListener } from "../utils/AsyncStorageListener";
13
13
  import { useMultiMMKVKeys } from "../hooks/useMMKVKeys";
14
14
  import { useSecureStoreKeys } from "../hooks/useSecureStoreKeys";
15
15
  import { useMMKVInstances } from "../hooks/useMMKVInstances";
16
+ import { StorageSaveNotice } from "./StorageSaveNotice";
16
17
  import { StorageActionButtons } from "./StorageActionButtons";
17
18
  import { SelectionActionBar } from "./SelectionActionBar";
18
19
  // Pure JS (no react-native-mmkv dependency) — works with any registered
@@ -75,7 +76,10 @@ export function GameUIStorageBrowser({
75
76
  onTogglePin,
76
77
  storageTypeFilter,
77
78
  onStorageTypeFilterChange,
78
- applySafeAreaInset
79
+ applySafeAreaInset,
80
+ detailKey = null,
81
+ onOpenKey,
82
+ onCloseDetail
79
83
  }) {
80
84
  const isPro = useIsPro();
81
85
  const keyLimit = useLimit("storage.keys");
@@ -100,16 +104,38 @@ export function GameUIStorageBrowser({
100
104
  const activeStorageType = isStorageTypeControlled ? storageTypeFilter : internalStorageType;
101
105
  const setActiveStorageType = isStorageTypeControlled ? onStorageTypeFilterChange : setInternalStorageType;
102
106
  const [selectedMMKVInstance, setSelectedMMKVInstance] = useState(null);
107
+ const scrollRef = useRef(null);
108
+ const scrollOffsetRef = useRef(0);
109
+ const rowNodesRef = useRef(new Map());
110
+ const handleRowRef = useCallback((id, node) => {
111
+ if (node) rowNodesRef.current.set(id, node);else rowNodesRef.current.delete(id);
112
+ }, []);
103
113
 
104
114
  /**
105
- * The key the full-screen editor is open on, if any.
115
+ * Shown after every successful write, with the same sentence every time.
116
+ *
117
+ * An earlier version looked up which live store owned the key and offered to
118
+ * make that store re-read it. That required knowing what is linked to what,
119
+ * and every route to that knowledge is a guess — key names collide across
120
+ * backends, only zustand stores register at all, and a store can register
121
+ * after the lookup ran. A message that is right most of the time is the
122
+ * problem this tool has, not the fix for it.
123
+ *
124
+ * There is also no general mechanism to find: the app owns its cache, and
125
+ * only the app can invalidate it. So the tool promises the one thing that is
126
+ * true for every key, every backend and every persistence library — this is
127
+ * what loads next launch — and "make it live now" lives in the state tools,
128
+ * where the store you mean is the one you are looking at.
129
+ */
130
+ /**
131
+ * Bumped on every save rather than flipped to `true`.
106
132
  *
107
- * Held here rather than in the row because the editor covers the whole
108
- * browser, and because the write it eventually performs is `handleSaveValue`
109
- * — which needs the MMKV instance registry and the refresh fan-out that live
110
- * at this level.
133
+ * The toast unmounts itself when it fades, and a boolean already `true` would
134
+ * not change on the next save — so a second edit would show nothing. The
135
+ * counter is the toast's `key`, so each save mounts a fresh one and the
136
+ * animation restarts from the beginning.
111
137
  */
112
- const [editingKey, setEditingKey] = useState(null);
138
+ const [saveToastId, setSaveToastId] = useState(0);
113
139
 
114
140
  // Selection mode state
115
141
  const [isSelectMode, setIsSelectMode] = useState(false);
@@ -167,6 +193,22 @@ export function GameUIStorageBrowser({
167
193
  return [...asyncStorageKeys, ...mmkvStorageKeys, ...secureStorageKeys];
168
194
  }, [asyncStorageKeys, mmkvStorageKeys, secureStorageKeys]);
169
195
 
196
+ /**
197
+ * The CURRENT snapshot of the key the detail screen is open on. `detailKey`
198
+ * is only an identity — resolving it fresh each pass is what makes the
199
+ * detail screen live: the app writes, the snapshot refreshes, the screen
200
+ * follows. Falls back to the captured info because the backend hooks
201
+ * transiently EMPTY their lists mid-refresh — treating "missing from this
202
+ * render's list" as "deleted" closed the screen the moment it opened, so
203
+ * absence here is never acted on (deletion closes explicitly, from the
204
+ * delete flow itself).
205
+ */
206
+ const detailInfo = useMemo(() => {
207
+ if (!detailKey) return null;
208
+ const id = storageRowId(detailKey);
209
+ return allStorageKeys.find(k => storageRowId(k) === id) ?? detailKey;
210
+ }, [detailKey, allStorageKeys]);
211
+
170
212
  // Determine loading and error states
171
213
  const isLoading = isLoadingAsync || isLoadingMMKV || isLoadingSecure;
172
214
  const error = asyncError || mmkvError || secureError;
@@ -240,6 +282,7 @@ export function GameUIStorageBrowser({
240
282
  }
241
283
  await setStorageValue(storageKey, raw, mmkvInstances);
242
284
  refresh();
285
+ setSaveToastId(id => id + 1);
243
286
  }, [isPro, mmkvInstances, refresh]);
244
287
 
245
288
  /**
@@ -266,7 +309,7 @@ export function GameUIStorageBrowser({
266
309
  * Pro is checked here rather than on the button because it's an upsell, not
267
310
  * a capability: the row shows the action and explains the wall on press.
268
311
  */
269
- const handleDeleteKey = useCallback(storageKey => {
312
+ const handleDeleteKey = useCallback((storageKey, onDeleted) => {
270
313
  if (!isPro) {
271
314
  setShowUpgradeModal(true);
272
315
  return;
@@ -284,6 +327,9 @@ export function GameUIStorageBrowser({
284
327
  instance: inst.instance
285
328
  })));
286
329
  refresh();
330
+ // Only after the delete actually landed — a cancelled alert
331
+ // or a failed delete leaves the caller's screen alone.
332
+ onDeleted?.();
287
333
  } catch (error) {
288
334
  Alert.alert("Error", `Failed to delete key: ${error}`);
289
335
  }
@@ -607,6 +653,31 @@ export function GameUIStorageBrowser({
607
653
  const healthStatus = healthPercentage >= 90 ? "OPTIMAL" : healthPercentage >= 70 ? "WARNING" : "CRITICAL";
608
654
  const healthColor = healthPercentage >= 90 ? gameUIColors.success : healthPercentage >= 70 ? gameUIColors.warning : gameUIColors.error;
609
655
 
656
+ // The detail screen REPLACES the list as the modal's whole body — the host
657
+ // modal's header carries back + the key's name, the React Query
658
+ // interaction (and the Zustand tool's). All hooks above have run; this
659
+ // return only swaps what's painted.
660
+ if (detailInfo) {
661
+ return /*#__PURE__*/_jsx(View, {
662
+ style: styles.root,
663
+ children: /*#__PURE__*/_jsx(StorageKeyDetailScreen, {
664
+ storageKey: detailInfo,
665
+ editBlockedReason: editBlockedFor(detailInfo),
666
+ eventCount: eventCountByKey?.[detailInfo.key],
667
+ onSave: raw => handleSaveValue(detailInfo, raw),
668
+ onViewHistory: onViewHistory ? () => onViewHistory(detailInfo.key) : undefined,
669
+ onDeleteKey: k => handleDeleteKey(k, () => onCloseDetail?.()),
670
+ onHideKey: k => {
671
+ handleHideKeys([k]);
672
+ onCloseDetail?.();
673
+ },
674
+ isPinned: pinnedKeys?.has(detailInfo.key) ?? false,
675
+ onTogglePin: onTogglePin,
676
+ applySafeAreaInset: applySafeAreaInset
677
+ })
678
+ });
679
+ }
680
+
610
681
  // Loading state
611
682
  if (isLoading && allStorageKeys.length === 0) {
612
683
  return /*#__PURE__*/_jsxs(View, {
@@ -808,11 +879,18 @@ export function GameUIStorageBrowser({
808
879
  });
809
880
  return /*#__PURE__*/_jsxs(View, {
810
881
  style: styles.root,
811
- children: [/*#__PURE__*/_jsxs(ScrollView
882
+ children: [saveToastId > 0 ? /*#__PURE__*/_jsx(StorageSaveNotice, {
883
+ onDone: () => setSaveToastId(0)
884
+ }, saveToastId) : null, /*#__PURE__*/_jsxs(ScrollView
812
885
  // Named because `by.type("RCTScrollView")` doesn't match on Fabric
813
886
  // (renamed class) — automation scrolls below-the-fold rows by this id.
814
887
  , {
815
888
  testID: "buoy-storage-scroll",
889
+ ref: scrollRef,
890
+ onScroll: e => {
891
+ scrollOffsetRef.current = e.nativeEvent.contentOffset.y;
892
+ },
893
+ scrollEventThrottle: 32,
816
894
  style: styles.scrollContainer,
817
895
  contentContainerStyle: styles.container,
818
896
  showsVerticalScrollIndicator: false,
@@ -860,26 +938,19 @@ export function GameUIStorageBrowser({
860
938
  eventCountByKey: eventCountByKey,
861
939
  onViewHistory: onViewHistory,
862
940
  onHideKey: key => handleHideKeys([key]),
863
- onEditKey: setEditingKey,
864
941
  onToggleKey: handleToggleKey,
865
942
  onDeleteKey: handleDeleteKey,
866
943
  editBlockedFor: editBlockedFor,
867
944
  pinnedKeys: pinnedKeys,
868
- onTogglePin: onTogglePin
945
+ onTogglePin: onTogglePin,
946
+ onRowRef: handleRowRef,
947
+ onOpenKey: onOpenKey
869
948
  }), lockedBannerNode]
870
949
  }) : emptyStateNode, /*#__PURE__*/_jsx(Text, {
871
950
  style: styles.techFooter,
872
951
  children: "ASYNC STORAGE | MMKV | SECURE STORAGE BACKENDS"
873
952
  }), upgradeModalNode]
874
- }), editingKey ? /*#__PURE__*/_jsx(View, {
875
- style: styles.editorOverlay,
876
- children: /*#__PURE__*/_jsx(StorageKeyEditorScreen, {
877
- storageKey: editingKey,
878
- onSave: raw => handleSaveValue(editingKey, raw),
879
- onClose: () => setEditingKey(null),
880
- applySafeAreaInset: applySafeAreaInset
881
- })
882
- }) : null]
953
+ })]
883
954
  });
884
955
  }
885
956
  const styles = StyleSheet.create({
@@ -25,7 +25,10 @@ export function StorageBrowserMode({
25
25
  onTogglePin,
26
26
  applySafeAreaInset,
27
27
  storageTypeFilter,
28
- onStorageTypeFilterChange
28
+ onStorageTypeFilterChange,
29
+ detailKey,
30
+ onOpenKey,
31
+ onCloseDetail
29
32
  }) {
30
33
  return /*#__PURE__*/_jsx(GameUIStorageBrowser, {
31
34
  requiredStorageKeys: requiredStorageKeys,
@@ -46,6 +49,9 @@ export function StorageBrowserMode({
46
49
  onTogglePin: onTogglePin,
47
50
  applySafeAreaInset: applySafeAreaInset,
48
51
  storageTypeFilter: storageTypeFilter,
49
- onStorageTypeFilterChange: onStorageTypeFilterChange
52
+ onStorageTypeFilterChange: onStorageTypeFilterChange,
53
+ detailKey: detailKey,
54
+ onOpenKey: onOpenKey,
55
+ onCloseDetail: onCloseDetail
50
56
  });
51
57
  }