@buoy-gg/redux 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.
- package/lib/commonjs/index.js +25 -0
- package/lib/commonjs/redux/components/ReduxActionButton.js +3 -1
- package/lib/commonjs/redux/components/ReduxActionDetailContent.js +60 -6
- package/lib/commonjs/redux/components/ReduxActionInfoView.js +61 -12
- package/lib/commonjs/redux/components/ReduxActionItem.js +20 -5
- package/lib/commonjs/redux/components/ReduxDetailViewToggle.js +7 -0
- package/lib/commonjs/redux/components/ReduxModal.js +169 -32
- package/lib/commonjs/redux/utils/activeStoreRegistry.js +31 -0
- package/lib/commonjs/redux/utils/autoInstrument.js +23 -40
- package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +36 -9
- package/lib/commonjs/redux/utils/devtoolsHook.js +4 -0
- package/lib/commonjs/redux/utils/testIDs.js +75 -0
- package/lib/module/index.js +5 -0
- package/lib/module/redux/components/ReduxActionButton.js +3 -1
- package/lib/module/redux/components/ReduxActionDetailContent.js +60 -6
- package/lib/module/redux/components/ReduxActionInfoView.js +63 -14
- package/lib/module/redux/components/ReduxActionItem.js +20 -5
- package/lib/module/redux/components/ReduxDetailViewToggle.js +7 -0
- package/lib/module/redux/components/ReduxModal.js +172 -34
- package/lib/module/redux/utils/activeStoreRegistry.js +29 -0
- package/lib/module/redux/utils/autoInstrument.js +8 -40
- package/lib/module/redux/utils/buoyReduxMiddleware.js +32 -10
- package/lib/module/redux/utils/devtoolsHook.js +5 -1
- package/lib/module/redux/utils/testIDs.js +68 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/redux/components/ReduxActionButton.d.ts +2 -1
- package/lib/typescript/redux/components/ReduxActionButton.d.ts.map +1 -1
- package/lib/typescript/redux/components/ReduxActionDetailContent.d.ts.map +1 -1
- package/lib/typescript/redux/components/ReduxActionInfoView.d.ts.map +1 -1
- package/lib/typescript/redux/components/ReduxActionItem.d.ts +15 -0
- package/lib/typescript/redux/components/ReduxActionItem.d.ts.map +1 -1
- package/lib/typescript/redux/components/ReduxDetailViewToggle.d.ts.map +1 -1
- package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -1
- package/lib/typescript/redux/utils/activeStoreRegistry.d.ts +27 -0
- package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +1 -1
- package/lib/typescript/redux/utils/autoInstrument.d.ts +7 -11
- package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -1
- package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts +17 -3
- package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -1
- package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +1 -1
- package/lib/typescript/redux/utils/testIDs.d.ts +51 -0
- package/lib/typescript/redux/utils/testIDs.d.ts.map +1 -0
- package/package.json +4 -4
|
@@ -15,6 +15,7 @@ var _reduxActionStore = require("../utils/reduxActionStore");
|
|
|
15
15
|
var _useAutoInstrumentRedux = require("../hooks/useAutoInstrumentRedux");
|
|
16
16
|
var _ReduxActionItem = require("./ReduxActionItem");
|
|
17
17
|
var _ReduxActionDetailContent = require("./ReduxActionDetailContent");
|
|
18
|
+
var _testIDs = require("../utils/testIDs");
|
|
18
19
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
19
20
|
/**
|
|
20
21
|
* Main Redux DevTools modal
|
|
@@ -29,20 +30,31 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
29
30
|
* anonymous tier. Kept because it is part of the package's public exports.
|
|
30
31
|
*/
|
|
31
32
|
const FREE_TIER_ACTION_LIMIT = exports.FREE_TIER_ACTION_LIMIT = _sharedUi.TIER_LIMITS["redux.actions"].free;
|
|
33
|
+
/**
|
|
34
|
+
* Why this takes the query as well as the enabled flag: "nothing was captured"
|
|
35
|
+
* and "nothing matched what you typed" are different problems, and an empty
|
|
36
|
+
* list under a live search reads as a dead tool. Naming the query back is what
|
|
37
|
+
* makes the difference legible without clearing the field to find out.
|
|
38
|
+
*/
|
|
32
39
|
function EmptyState({
|
|
33
|
-
isEnabled
|
|
40
|
+
isEnabled,
|
|
41
|
+
query
|
|
34
42
|
}) {
|
|
43
|
+
const isFiltered = query.length > 0;
|
|
35
44
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
36
45
|
style: styles.emptyState,
|
|
46
|
+
testID: `${_testIDs.REDUX_TEST_ID}-empty`,
|
|
37
47
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
|
|
38
48
|
size: 32,
|
|
39
49
|
color: _sharedUi.night.color.textTertiary
|
|
40
50
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
41
51
|
style: styles.emptyTitle,
|
|
42
|
-
|
|
52
|
+
testID: `${_testIDs.REDUX_TEST_ID}-empty-title`,
|
|
53
|
+
children: isFiltered ? "No Matches" : "No Redux actions"
|
|
43
54
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
44
55
|
style: styles.emptyText,
|
|
45
|
-
|
|
56
|
+
testID: `${_testIDs.REDUX_TEST_ID}-empty-text`,
|
|
57
|
+
children: isFiltered ? `No action type contains "${query}"` : isEnabled ? "Actions will appear here as they are dispatched" : "Enable capture to start recording actions"
|
|
46
58
|
})]
|
|
47
59
|
});
|
|
48
60
|
}
|
|
@@ -85,14 +97,27 @@ function ReduxModal({
|
|
|
85
97
|
const lockedActionCount = (0, _react.useMemo)(() => Math.max(0, filteredActions.length - actionLimit), [filteredActions.length, actionLimit]);
|
|
86
98
|
const hasLockedActions = lockedActionCount > 0;
|
|
87
99
|
|
|
88
|
-
// Selection state
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
100
|
+
// Selection state, held as an action ID rather than as a list position.
|
|
101
|
+
//
|
|
102
|
+
// This list grows at the HEAD: every dispatch prepends a row and pushes
|
|
103
|
+
// everything below it down one. An index therefore names a different action
|
|
104
|
+
// a moment after it is taken — so a detail page opened while the app was
|
|
105
|
+
// still dispatching silently became a detail page for some OTHER action,
|
|
106
|
+
// with no transition and nothing to say it had happened. A devtool that
|
|
107
|
+
// quietly swaps what it is showing you is worse than one that shows nothing.
|
|
108
|
+
//
|
|
109
|
+
// The index is still what the stepper and the diff's "previous action" are
|
|
110
|
+
// expressed in — both are genuinely about position — so it is derived here
|
|
111
|
+
// and re-derived whenever the list moves.
|
|
112
|
+
const [selectedActionId, setSelectedActionId] = (0, _react.useState)(null);
|
|
113
|
+
const selectedActionIndex = (0, _react.useMemo)(() => {
|
|
114
|
+
if (selectedActionId === null) return null;
|
|
115
|
+
const index = visibleActions.findIndex(a => a.id === selectedActionId);
|
|
116
|
+
// Below zero means the action is gone — cleared, or aged out past the tier
|
|
117
|
+
// cap. Falling back to the list is the honest outcome; the alternative is
|
|
118
|
+
// showing whatever moved into its place.
|
|
119
|
+
return index >= 0 ? index : null;
|
|
120
|
+
}, [selectedActionId, visibleActions]);
|
|
96
121
|
const selectedAction = selectedActionIndex !== null ? visibleActions[selectedActionIndex] : null;
|
|
97
122
|
|
|
98
123
|
// Notify the host on selection (keyed by action id so re-renders with the
|
|
@@ -116,7 +141,7 @@ function ReduxModal({
|
|
|
116
141
|
const index = visibleActions.findIndex(a => a.id === initialSelectedActionId);
|
|
117
142
|
if (index < 0) return;
|
|
118
143
|
appliedInitialSelectionRef.current = initialSelectedActionId;
|
|
119
|
-
|
|
144
|
+
setSelectedActionId(initialSelectedActionId);
|
|
120
145
|
}, [initialSelectedActionId, visibleActions]);
|
|
121
146
|
const [searchText, setSearchText] = (0, _react.useState)("");
|
|
122
147
|
const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
|
|
@@ -132,19 +157,33 @@ function ReduxModal({
|
|
|
132
157
|
searchText: text
|
|
133
158
|
}));
|
|
134
159
|
};
|
|
160
|
+
|
|
161
|
+
/** Drop the query AND put the header back — the ✕ means "stop filtering". */
|
|
162
|
+
const closeSearch = (0, _react.useCallback)(() => {
|
|
163
|
+
setSearchText("");
|
|
164
|
+
setFilter(prev => ({
|
|
165
|
+
...prev,
|
|
166
|
+
searchText: ""
|
|
167
|
+
}));
|
|
168
|
+
setIsSearchActive(false);
|
|
169
|
+
}, [setFilter]);
|
|
135
170
|
const handleActionPress = (0, _react.useCallback)(action => {
|
|
136
|
-
|
|
137
|
-
setSelectedActionIndex(index >= 0 ? index : 0);
|
|
171
|
+
setSelectedActionId(action.id);
|
|
138
172
|
}, []);
|
|
139
173
|
const handleUpgradePress = (0, _react.useCallback)(() => {
|
|
140
174
|
setShowProModal(true);
|
|
141
175
|
}, []);
|
|
142
176
|
const handleBack = (0, _react.useCallback)(() => {
|
|
143
|
-
|
|
177
|
+
setSelectedActionId(null);
|
|
144
178
|
}, []);
|
|
179
|
+
|
|
180
|
+
// The stepper and the async timeline both navigate by POSITION; the
|
|
181
|
+
// selection is kept by identity, so the position is resolved to an id here,
|
|
182
|
+
// once, at the moment of the tap.
|
|
145
183
|
const handleIndexChange = (0, _react.useCallback)(newIndex => {
|
|
146
|
-
|
|
147
|
-
|
|
184
|
+
const next = visibleActions[newIndex];
|
|
185
|
+
if (next) setSelectedActionId(next.id);
|
|
186
|
+
}, [visibleActions]);
|
|
148
187
|
const handleCopyAll = async () => {
|
|
149
188
|
if (!isPro) {
|
|
150
189
|
setShowProModal(true);
|
|
@@ -166,14 +205,49 @@ function ReduxModal({
|
|
|
166
205
|
|
|
167
206
|
// FlatList optimization
|
|
168
207
|
const keyExtractor = (0, _react.useCallback)(item => item.id, []);
|
|
208
|
+
|
|
209
|
+
// An action row's handle carries an ordinal among rows of the SAME type, and
|
|
210
|
+
// an ordinal is a fact about the list rather than about the row — see
|
|
211
|
+
// ../utils/testIDs.ts for why a type alone cannot address a redux row.
|
|
212
|
+
const rowTestIDs = (0, _react.useMemo)(() => (0, _testIDs.buildActionRowTestIDs)(visibleActions), [visibleActions]);
|
|
213
|
+
|
|
214
|
+
// How many captured actions share each requestId, i.e. how far along each
|
|
215
|
+
// async operation is.
|
|
216
|
+
//
|
|
217
|
+
// Computed HERE rather than in the row, and passed down, because a row's
|
|
218
|
+
// async badge is a fact about OTHER rows. `pending` is captured and drawn
|
|
219
|
+
// while it is still alone in the world; `fulfilled` arrives a moment later
|
|
220
|
+
// and makes the earlier row's badge wrong. `ReduxActionItem` is memoized on
|
|
221
|
+
// its props and the action object it holds never changes identity, so
|
|
222
|
+
// nothing about that second arrival reached the first row — the pending half
|
|
223
|
+
// of every settled thunk permanently showed no flow badge, which is exactly
|
|
224
|
+
// backwards from what the badge is for.
|
|
225
|
+
//
|
|
226
|
+
// One pass over the store instead of a per-row scan: the previous shape was
|
|
227
|
+
// O(rows x actions) at 200 actions, on a list that re-renders on every
|
|
228
|
+
// dispatch.
|
|
229
|
+
const linkedCounts = (0, _react.useMemo)(() => {
|
|
230
|
+
const counts = new Map();
|
|
231
|
+
for (const action of _reduxActionStore.reduxActionStore.getActions()) {
|
|
232
|
+
const requestId = action.meta?.requestId;
|
|
233
|
+
if (!requestId) continue;
|
|
234
|
+
counts.set(requestId, (counts.get(requestId) ?? 0) + 1);
|
|
235
|
+
}
|
|
236
|
+
return counts;
|
|
237
|
+
// `filteredActions` takes a new identity whenever the store notifies, which
|
|
238
|
+
// is the only moment these counts can change.
|
|
239
|
+
}, [filteredActions]);
|
|
169
240
|
const renderItem = (0, _react.useCallback)(({
|
|
170
241
|
item
|
|
171
242
|
}) => {
|
|
243
|
+
const requestId = item.meta?.requestId;
|
|
172
244
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxActionItem.ReduxActionItem, {
|
|
173
245
|
action: item,
|
|
174
|
-
onPress: handleActionPress
|
|
246
|
+
onPress: handleActionPress,
|
|
247
|
+
linkedCount: requestId ? linkedCounts.get(requestId) ?? 0 : 0,
|
|
248
|
+
testID: rowTestIDs.get(item.id)
|
|
175
249
|
});
|
|
176
|
-
}, [handleActionPress]);
|
|
250
|
+
}, [handleActionPress, rowTestIDs, linkedCounts]);
|
|
177
251
|
const renderHeaderContent = () => {
|
|
178
252
|
// Detail view header - show action type
|
|
179
253
|
if (selectedAction) {
|
|
@@ -182,7 +256,8 @@ function ReduxModal({
|
|
|
182
256
|
onBack: handleBack
|
|
183
257
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
184
258
|
title: selectedAction.type,
|
|
185
|
-
centered: true
|
|
259
|
+
centered: true,
|
|
260
|
+
testID: `${_testIDs.REDUX_TEST_ID}-detail-title`
|
|
186
261
|
})]
|
|
187
262
|
});
|
|
188
263
|
}
|
|
@@ -195,27 +270,42 @@ function ReduxModal({
|
|
|
195
270
|
title: "",
|
|
196
271
|
children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
197
272
|
style: styles.headerSearchContainer,
|
|
273
|
+
testID: `${_testIDs.REDUX_TEST_ID}-search`,
|
|
198
274
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
|
|
199
275
|
size: 14,
|
|
200
276
|
color: _sharedUi.night.color.textSecondary
|
|
201
277
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
|
|
202
278
|
ref: searchInputRef,
|
|
279
|
+
testID: `${_testIDs.REDUX_TEST_ID}-search-field`,
|
|
203
280
|
style: styles.headerSearchInput,
|
|
204
281
|
placeholder: "Search action types...",
|
|
205
282
|
placeholderTextColor: _sharedUi.night.color.textTertiary,
|
|
206
283
|
value: searchText,
|
|
207
284
|
onChangeText: handleSearch,
|
|
208
|
-
onSubmitEditing: () =>
|
|
209
|
-
|
|
285
|
+
onSubmitEditing: () => searchInputRef.current?.blur()
|
|
286
|
+
// Stay open while the query still filters the list.
|
|
287
|
+
//
|
|
288
|
+
// Collapsing it on blur hid the reason rows were missing: the
|
|
289
|
+
// header went back to its chips — which count the WHOLE
|
|
290
|
+
// capture, not the filtered list — while the list stayed
|
|
291
|
+
// narrowed, so the tool showed "4 actions" above a single row
|
|
292
|
+
// and offered nothing to explain the gap. The only way back
|
|
293
|
+
// was to reopen a field the user had no reason to think was
|
|
294
|
+
// still armed.
|
|
295
|
+
,
|
|
296
|
+
onBlur: () => {
|
|
297
|
+
if (searchText.length === 0) setIsSearchActive(false);
|
|
298
|
+
},
|
|
299
|
+
accessibilityLabel: "Search action types",
|
|
210
300
|
autoCapitalize: "none",
|
|
211
301
|
autoCorrect: false,
|
|
212
302
|
returnKeyType: "search"
|
|
213
|
-
}),
|
|
214
|
-
onPress:
|
|
215
|
-
handleSearch("");
|
|
216
|
-
setIsSearchActive(false);
|
|
217
|
-
},
|
|
303
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
304
|
+
onPress: closeSearch,
|
|
218
305
|
style: styles.headerSearchClear,
|
|
306
|
+
testID: `${_testIDs.REDUX_TEST_ID}-search-clear`,
|
|
307
|
+
accessibilityRole: "button",
|
|
308
|
+
accessibilityLabel: "Close search",
|
|
219
309
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
|
|
220
310
|
size: 14,
|
|
221
311
|
color: _sharedUi.night.color.textSecondary
|
|
@@ -229,6 +319,12 @@ function ReduxModal({
|
|
|
229
319
|
...prev,
|
|
230
320
|
onlyWithChanges: false
|
|
231
321
|
})),
|
|
322
|
+
testID: `${_testIDs.REDUX_TEST_ID}-chip-all`,
|
|
323
|
+
accessibilityRole: "button",
|
|
324
|
+
accessibilityLabel: `Show all actions, ${stats.totalActions} captured`,
|
|
325
|
+
accessibilityState: {
|
|
326
|
+
selected: !filter.onlyWithChanges
|
|
327
|
+
},
|
|
232
328
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
233
329
|
style: styles.headerChipLabel,
|
|
234
330
|
children: "ALL"
|
|
@@ -236,6 +332,7 @@ function ReduxModal({
|
|
|
236
332
|
style: [styles.headerChipValue, {
|
|
237
333
|
color: _sharedUi.night.color.text
|
|
238
334
|
}],
|
|
335
|
+
testID: `${_testIDs.REDUX_TEST_ID}-chip-all-value`,
|
|
239
336
|
children: stats.totalActions
|
|
240
337
|
})]
|
|
241
338
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
|
|
@@ -244,6 +341,12 @@ function ReduxModal({
|
|
|
244
341
|
...prev,
|
|
245
342
|
onlyWithChanges: true
|
|
246
343
|
})),
|
|
344
|
+
testID: `${_testIDs.REDUX_TEST_ID}-chip-changed`,
|
|
345
|
+
accessibilityRole: "button",
|
|
346
|
+
accessibilityLabel: `Show only actions that changed state, ${stats.actionsWithChanges} of them`,
|
|
347
|
+
accessibilityState: {
|
|
348
|
+
selected: !!filter.onlyWithChanges
|
|
349
|
+
},
|
|
247
350
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
|
|
248
351
|
size: 12,
|
|
249
352
|
color: _sharedUi.macOSColors.semantic.warning
|
|
@@ -251,10 +354,13 @@ function ReduxModal({
|
|
|
251
354
|
style: [styles.headerChipValue, {
|
|
252
355
|
color: _sharedUi.macOSColors.semantic.warning
|
|
253
356
|
}],
|
|
357
|
+
testID: `${_testIDs.REDUX_TEST_ID}-chip-changed-value`,
|
|
254
358
|
children: stats.actionsWithChanges
|
|
255
359
|
})]
|
|
256
360
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
257
361
|
style: styles.headerChip,
|
|
362
|
+
testID: `${_testIDs.REDUX_TEST_ID}-chip-types`,
|
|
363
|
+
accessibilityLabel: `${stats.uniqueActionTypes} distinct action types`,
|
|
258
364
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CheckCircle, {
|
|
259
365
|
size: 12,
|
|
260
366
|
color: _sharedUi.macOSColors.semantic.success
|
|
@@ -262,22 +368,32 @@ function ReduxModal({
|
|
|
262
368
|
style: [styles.headerChipValue, {
|
|
263
369
|
color: _sharedUi.macOSColors.semantic.success
|
|
264
370
|
}],
|
|
371
|
+
testID: `${_testIDs.REDUX_TEST_ID}-chip-types-value`,
|
|
265
372
|
children: stats.uniqueActionTypes
|
|
266
373
|
})]
|
|
267
374
|
})]
|
|
268
375
|
})
|
|
269
376
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
|
|
270
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
377
|
+
children: [!isSearchActive && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
271
378
|
onPress: () => setIsSearchActive(true),
|
|
272
|
-
style: styles.headerActionButton,
|
|
379
|
+
style: [styles.headerActionButton, searchText.length > 0 && styles.headerActionButtonActive],
|
|
380
|
+
testID: `${_testIDs.REDUX_TEST_ID}-search-open`,
|
|
381
|
+
accessibilityRole: "button",
|
|
382
|
+
accessibilityLabel: "Search action types",
|
|
273
383
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
|
|
274
384
|
size: 14,
|
|
275
|
-
color: _sharedUi.night.color.textSecondary
|
|
385
|
+
color: searchText.length > 0 ? _sharedUi.night.color.accent : _sharedUi.night.color.textSecondary
|
|
276
386
|
})
|
|
277
387
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
278
388
|
onPress: handleCopyAll,
|
|
279
389
|
style: [styles.headerActionButton, filteredActions.length === 0 && styles.headerActionButtonDisabled],
|
|
280
390
|
disabled: filteredActions.length === 0,
|
|
391
|
+
testID: `${_testIDs.REDUX_TEST_ID}-copy`,
|
|
392
|
+
accessibilityRole: "button",
|
|
393
|
+
accessibilityLabel: "Copy all actions",
|
|
394
|
+
accessibilityState: {
|
|
395
|
+
disabled: filteredActions.length === 0
|
|
396
|
+
},
|
|
281
397
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Copy, {
|
|
282
398
|
size: 14,
|
|
283
399
|
color: filteredActions.length > 0 ? _sharedUi.night.color.textSecondary : _sharedUi.night.color.textTertiary
|
|
@@ -285,11 +401,18 @@ function ReduxModal({
|
|
|
285
401
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
|
|
286
402
|
isEnabled: isEnabled,
|
|
287
403
|
onToggle: toggleCapture,
|
|
288
|
-
accessibilityLabel: "Toggle Redux action capture"
|
|
404
|
+
accessibilityLabel: "Toggle Redux action capture",
|
|
405
|
+
testID: `${_testIDs.REDUX_TEST_ID}-power`
|
|
289
406
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
290
407
|
onPress: clearActions,
|
|
291
408
|
style: [styles.headerActionButton, filteredActions.length === 0 && styles.headerActionButtonDisabled],
|
|
292
409
|
disabled: filteredActions.length === 0,
|
|
410
|
+
testID: `${_testIDs.REDUX_TEST_ID}-clear`,
|
|
411
|
+
accessibilityRole: "button",
|
|
412
|
+
accessibilityLabel: "Clear actions",
|
|
413
|
+
accessibilityState: {
|
|
414
|
+
disabled: filteredActions.length === 0
|
|
415
|
+
},
|
|
293
416
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
|
|
294
417
|
size: 14,
|
|
295
418
|
color: filteredActions.length > 0 ? _sharedUi.night.color.textTertiary : _sharedUi.night.color.textTertiary
|
|
@@ -325,6 +448,7 @@ function ReduxModal({
|
|
|
325
448
|
}) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
326
449
|
children: [instrumentError && !isInstrumented && !_reduxActionStore.reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
327
450
|
style: styles.errorBanner,
|
|
451
|
+
testID: `${_testIDs.REDUX_TEST_ID}-banner-error`,
|
|
328
452
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.AlertTriangle, {
|
|
329
453
|
size: 14,
|
|
330
454
|
color: _sharedUi.night.color.danger
|
|
@@ -334,6 +458,7 @@ function ReduxModal({
|
|
|
334
458
|
})]
|
|
335
459
|
}), !isEnabled && isInstrumented && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
336
460
|
style: styles.disabledBanner,
|
|
461
|
+
testID: `${_testIDs.REDUX_TEST_ID}-banner-disabled`,
|
|
337
462
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Power, {
|
|
338
463
|
size: 14,
|
|
339
464
|
color: _sharedUi.night.color.warning
|
|
@@ -343,6 +468,7 @@ function ReduxModal({
|
|
|
343
468
|
})]
|
|
344
469
|
}), captureMechanism === "patch" && !_reduxActionStore.reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
345
470
|
style: styles.disabledBanner,
|
|
471
|
+
testID: `${_testIDs.REDUX_TEST_ID}-banner-patch`,
|
|
346
472
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.AlertTriangle, {
|
|
347
473
|
size: 14,
|
|
348
474
|
color: _sharedUi.night.color.warning
|
|
@@ -354,11 +480,15 @@ function ReduxModal({
|
|
|
354
480
|
style: styles.lockedBanner,
|
|
355
481
|
onPress: handleUpgradePress,
|
|
356
482
|
activeOpacity: 0.8,
|
|
483
|
+
testID: `${_testIDs.REDUX_TEST_ID}-locked`,
|
|
484
|
+
accessibilityRole: "button",
|
|
485
|
+
accessibilityLabel: `${lockedActionCount} older ${lockedActionCount === 1 ? "action" : "actions"} locked. Upgrade to see them.`,
|
|
357
486
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Lock, {
|
|
358
487
|
size: 14,
|
|
359
488
|
color: _sharedUi.night.color.accent
|
|
360
489
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
361
490
|
style: styles.lockedText,
|
|
491
|
+
testID: `${_testIDs.REDUX_TEST_ID}-locked-text`,
|
|
362
492
|
children: [lockedActionCount, " older ", lockedActionCount === 1 ? "action" : "actions", " locked"]
|
|
363
493
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
364
494
|
style: styles.upgradeBadge,
|
|
@@ -369,6 +499,7 @@ function ReduxModal({
|
|
|
369
499
|
})]
|
|
370
500
|
}), visibleActions.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
371
501
|
ref: flatListRef,
|
|
502
|
+
testID: `${_testIDs.REDUX_TEST_ID}-list`,
|
|
372
503
|
data: visibleActions,
|
|
373
504
|
renderItem: renderItem,
|
|
374
505
|
keyExtractor: keyExtractor,
|
|
@@ -380,7 +511,8 @@ function ReduxModal({
|
|
|
380
511
|
windowSize: 5,
|
|
381
512
|
updateCellsBatchingPeriod: 50
|
|
382
513
|
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyState, {
|
|
383
|
-
isEnabled: isEnabled
|
|
514
|
+
isEnabled: isEnabled,
|
|
515
|
+
query: searchText
|
|
384
516
|
})]
|
|
385
517
|
})
|
|
386
518
|
});
|
|
@@ -398,6 +530,7 @@ function ReduxModal({
|
|
|
398
530
|
style: styles.inlineRoot,
|
|
399
531
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
400
532
|
style: styles.inlineHeader,
|
|
533
|
+
..._sharedUi.windowDragProps,
|
|
401
534
|
children: renderHeaderContent()
|
|
402
535
|
}), body, footerNode]
|
|
403
536
|
}), upgradeModal]
|
|
@@ -526,6 +659,10 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
526
659
|
headerActionButtonDisabled: {
|
|
527
660
|
opacity: 0.55
|
|
528
661
|
},
|
|
662
|
+
headerActionButtonActive: {
|
|
663
|
+
backgroundColor: _sharedUi.night.color.accentSoft,
|
|
664
|
+
borderColor: _sharedUi.night.color.accentBorderStrong
|
|
665
|
+
},
|
|
529
666
|
startButton: {
|
|
530
667
|
backgroundColor: _sharedUi.macOSColors.semantic.successBackground,
|
|
531
668
|
borderColor: _sharedUi.macOSColors.semantic.success + "40"
|
|
@@ -7,6 +7,8 @@ exports.ENHANCER_SYMBOL = void 0;
|
|
|
7
7
|
exports.getActiveStore = getActiveStore;
|
|
8
8
|
exports.getCaptureMechanism = getCaptureMechanism;
|
|
9
9
|
exports.getInstrumentationStatus = getInstrumentationStatus;
|
|
10
|
+
exports.isTimeTravelAvailable = isTimeTravelAvailable;
|
|
11
|
+
exports.markTimeTravelReducer = markTimeTravelReducer;
|
|
10
12
|
exports.registerActiveStore = registerActiveStore;
|
|
11
13
|
exports.setReduxDiagnostic = setReduxDiagnostic;
|
|
12
14
|
exports.unregisterActiveStore = unregisterActiveStore;
|
|
@@ -38,6 +40,7 @@ const ENHANCER_SYMBOL = exports.ENHANCER_SYMBOL = Symbol.for("@@buoy/enhancerIns
|
|
|
38
40
|
let activeStore = null;
|
|
39
41
|
let mechanism = null;
|
|
40
42
|
let diagnostic = null;
|
|
43
|
+
let timeTravelReducer = false;
|
|
41
44
|
function registerActiveStore(store, mech) {
|
|
42
45
|
activeStore = store;
|
|
43
46
|
mechanism = mech;
|
|
@@ -56,6 +59,33 @@ function getCaptureMechanism() {
|
|
|
56
59
|
return mechanism;
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Record that a reducer able to serve `BUOY_JUMP_TO_STATE` is in the store.
|
|
64
|
+
*
|
|
65
|
+
* Called by the devtools-hook enhancer and by `withBuoyDevTools` — the only
|
|
66
|
+
* two things that wrap the root reducer. Capture and time travel are separate
|
|
67
|
+
* capabilities and they are wired separately: a store can be fully captured
|
|
68
|
+
* through `buoyReduxMiddleware` and still have no way to restore a state,
|
|
69
|
+
* because middleware sits ABOVE the reducer and cannot replace what it
|
|
70
|
+
* returns.
|
|
71
|
+
*/
|
|
72
|
+
function markTimeTravelReducer() {
|
|
73
|
+
timeTravelReducer = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether pressing JUMP can actually do anything.
|
|
78
|
+
*
|
|
79
|
+
* Worth a first-class answer rather than a try-and-see, because the failure is
|
|
80
|
+
* invisible: `jumpToState` dispatches into a store that simply does not
|
|
81
|
+
* recognise the action, the reducer returns the state it already had, and the
|
|
82
|
+
* screen does not move. From the outside that is indistinguishable from a
|
|
83
|
+
* devtool that is broken.
|
|
84
|
+
*/
|
|
85
|
+
function isTimeTravelAvailable() {
|
|
86
|
+
return timeTravelReducer && activeStore !== null;
|
|
87
|
+
}
|
|
88
|
+
|
|
59
89
|
/**
|
|
60
90
|
* Recorded by the auto-instrument hook so non-React surfaces (sync adapter,
|
|
61
91
|
* MCP) can report WHY there's no store instead of a generic failure.
|
|
@@ -68,6 +98,7 @@ function getInstrumentationStatus() {
|
|
|
68
98
|
mechanism,
|
|
69
99
|
capture: mechanism === "enhancer" || mechanism === "middleware" ? "full" : mechanism === "patch" ? "top-level-only" : "none",
|
|
70
100
|
hasStore: activeStore !== null,
|
|
101
|
+
timeTravel: isTimeTravelAvailable(),
|
|
71
102
|
diagnostic
|
|
72
103
|
};
|
|
73
104
|
}
|
|
@@ -13,9 +13,25 @@ Object.defineProperty(exports, "getInstrumentationStatus", {
|
|
|
13
13
|
exports.instrumentStore = instrumentStore;
|
|
14
14
|
exports.isAutoInstrumentActive = isAutoInstrumentActive;
|
|
15
15
|
exports.isStoreInstrumented = isStoreInstrumented;
|
|
16
|
-
exports
|
|
16
|
+
Object.defineProperty(exports, "isTimeTravelAvailable", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () {
|
|
19
|
+
return _buoyReduxMiddleware.isTimeTravelAvailable;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(exports, "jumpToState", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
get: function () {
|
|
25
|
+
return _buoyReduxMiddleware.jumpToState;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
17
28
|
exports.registerReduxStore = registerReduxStore;
|
|
18
|
-
exports
|
|
29
|
+
Object.defineProperty(exports, "replayAction", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: function () {
|
|
32
|
+
return _buoyReduxMiddleware.replayAction;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
19
35
|
exports.uninstrumentStore = uninstrumentStore;
|
|
20
36
|
var _reduxActionStore = require("./reduxActionStore");
|
|
21
37
|
var _buoyReduxMiddleware = require("./buoyReduxMiddleware");
|
|
@@ -187,47 +203,14 @@ function registerReduxStore(store, options = {}) {
|
|
|
187
203
|
// Time-Travel / Introspection
|
|
188
204
|
// ============================================
|
|
189
205
|
|
|
190
|
-
let warnedJumpUnsupported = false;
|
|
191
|
-
|
|
192
206
|
/**
|
|
193
|
-
*
|
|
207
|
+
* Time travel lives in buoyReduxMiddleware.ts and is re-exported here.
|
|
194
208
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
209
|
+
* It used to be reimplemented in this file, and the two copies drifted: this
|
|
210
|
+
* one learned to notice that a jump had been ignored, the one the modal's JUMP
|
|
211
|
+
* button actually calls never did. So the detection existed and no user could
|
|
212
|
+
* ever have seen it. One implementation, one behaviour.
|
|
199
213
|
*/
|
|
200
|
-
function jumpToState(state) {
|
|
201
|
-
if ((0, _reduxActionStore.isEvictedState)(state)) {
|
|
202
|
-
console.warn("[BuoyRedux] Cannot jump - this action's state was evicted (raw state is retained for recent actions only)");
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const store = (0, _activeStoreRegistry.getActiveStore)();
|
|
206
|
-
if (!store) {
|
|
207
|
-
console.warn("[BuoyRedux] Cannot jump to state - no store instrumented");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
store.dispatch({
|
|
211
|
-
type: _buoyReduxMiddleware.BUOY_JUMP_TO_STATE,
|
|
212
|
-
payload: state
|
|
213
|
-
});
|
|
214
|
-
if (typeof __DEV__ !== "undefined" && __DEV__ && !warnedJumpUnsupported && store.getState() !== state) {
|
|
215
|
-
warnedJumpUnsupported = true;
|
|
216
|
-
console.warn("[BuoyRedux] Time-travel jump was ignored by the store. Wrap your root " + "reducer with withBuoyDevTools(), or ensure @buoy-gg/redux is imported " + "before your store is created (enhancer path) for real time travel.");
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Replay an action (dispatch it again)
|
|
222
|
-
*/
|
|
223
|
-
function replayAction(action) {
|
|
224
|
-
const store = (0, _activeStoreRegistry.getActiveStore)();
|
|
225
|
-
if (store) {
|
|
226
|
-
store.dispatch(action);
|
|
227
|
-
} else {
|
|
228
|
-
console.warn("[BuoyRedux] Cannot replay action - no store instrumented");
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
214
|
|
|
232
215
|
/**
|
|
233
216
|
* Get the currently active (instrumented) store.
|
|
@@ -6,6 +6,12 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.buoyReduxMiddleware = exports.BUOY_REPLAY_ACTION = exports.BUOY_JUMP_TO_STATE = void 0;
|
|
7
7
|
exports.createBuoyReduxMiddleware = createBuoyReduxMiddleware;
|
|
8
8
|
exports.isMiddlewareActive = isMiddlewareActive;
|
|
9
|
+
Object.defineProperty(exports, "isTimeTravelAvailable", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function () {
|
|
12
|
+
return _activeStoreRegistry.isTimeTravelAvailable;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
9
15
|
exports.jumpToState = jumpToState;
|
|
10
16
|
exports.replayAction = replayAction;
|
|
11
17
|
exports.withBuoyDevTools = withBuoyDevTools;
|
|
@@ -51,9 +57,18 @@ const BUOY_REPLAY_ACTION = exports.BUOY_REPLAY_ACTION = "@@buoy/REPLAY_ACTION";
|
|
|
51
57
|
/**
|
|
52
58
|
* Jump to a specific state (time-travel).
|
|
53
59
|
*
|
|
54
|
-
* Registry-backed, so it
|
|
55
|
-
*
|
|
56
|
-
*
|
|
60
|
+
* Registry-backed, so it reaches the store on every capture path — but
|
|
61
|
+
* REACHING the store is not the same as being able to jump, and conflating the
|
|
62
|
+
* two is what made this fail invisibly. A jump is an action like any other: it
|
|
63
|
+
* only does something if a reducer handles it, and the only two things that
|
|
64
|
+
* install such a reducer are the devtools-hook enhancer and `withBuoyDevTools`.
|
|
65
|
+
* An app that captures through `buoyReduxMiddleware` alone dispatches the jump
|
|
66
|
+
* into a store that has never heard of it, gets its own state back, and shows
|
|
67
|
+
* a screen that does not move — with no error, because nothing went wrong.
|
|
68
|
+
*
|
|
69
|
+
* So the capability is checked up front and refused out loud. The UI asks the
|
|
70
|
+
* same question (`isTimeTravelAvailable`) and disables the button, which is the
|
|
71
|
+
* half a warning cannot do.
|
|
57
72
|
*/
|
|
58
73
|
function jumpToState(state) {
|
|
59
74
|
if ((0, _reduxActionStore.isEvictedState)(state)) {
|
|
@@ -61,16 +76,25 @@ function jumpToState(state) {
|
|
|
61
76
|
return;
|
|
62
77
|
}
|
|
63
78
|
const store = (0, _activeStoreRegistry.getActiveStore)();
|
|
64
|
-
if (store) {
|
|
65
|
-
store.dispatch({
|
|
66
|
-
type: BUOY_JUMP_TO_STATE,
|
|
67
|
-
payload: state
|
|
68
|
-
});
|
|
69
|
-
} else {
|
|
79
|
+
if (!store) {
|
|
70
80
|
console.warn("[BuoyRedux] Cannot jump to state - no store instrumented");
|
|
81
|
+
return;
|
|
71
82
|
}
|
|
83
|
+
if (!(0, _activeStoreRegistry.isTimeTravelAvailable)()) {
|
|
84
|
+
console.warn("[BuoyRedux] Cannot jump to state - the root reducer cannot serve a " + "state jump. Capture and time travel are wired separately: either put " + "`import '@buoy-gg/redux';` first in your app entry (so Buoy is the " + "store enhancer), or wrap your root reducer with withBuoyDevTools().");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
store.dispatch({
|
|
88
|
+
type: BUOY_JUMP_TO_STATE,
|
|
89
|
+
payload: state
|
|
90
|
+
});
|
|
72
91
|
}
|
|
73
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Whether the JUMP affordance can do anything in this app. Re-exported from
|
|
95
|
+
* the registry so hosts have one import for the whole time-travel surface.
|
|
96
|
+
*/
|
|
97
|
+
|
|
74
98
|
/**
|
|
75
99
|
* Replay an action (dispatch it again)
|
|
76
100
|
*/
|
|
@@ -95,6 +119,9 @@ function replayAction(action) {
|
|
|
95
119
|
* ```
|
|
96
120
|
*/
|
|
97
121
|
function withBuoyDevTools(reducer) {
|
|
122
|
+
// The other half of the time-travel wiring (see jumpToState): this is one of
|
|
123
|
+
// exactly two places that can answer BUOY_JUMP_TO_STATE.
|
|
124
|
+
(0, _activeStoreRegistry.markTimeTravelReducer)();
|
|
98
125
|
return (state, action) => {
|
|
99
126
|
// Handle jump to state action
|
|
100
127
|
if (action.type === BUOY_JUMP_TO_STATE) {
|
|
@@ -43,6 +43,10 @@ function composeFns(...funcs) {
|
|
|
43
43
|
|
|
44
44
|
/** Wrap a reducer so BUOY_JUMP_TO_STATE performs a real state jump. */
|
|
45
45
|
function wrapReducer(reducer) {
|
|
46
|
+
// Announce the capability, not just perform it: with nothing wrapped, a jump
|
|
47
|
+
// is dispatched, ignored, and silent — so the UI needs to know in advance
|
|
48
|
+
// whether the button it is drawing can keep its promise.
|
|
49
|
+
(0, _activeStoreRegistry.markTimeTravelReducer)();
|
|
46
50
|
return (state, action) => {
|
|
47
51
|
if (action && action.type === _buoyReduxMiddleware.BUOY_JUMP_TO_STATE) {
|
|
48
52
|
return action.payload;
|