@applicaster/zapp-react-native-utils 16.0.0-rc.124 → 16.0.0-rc.126
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/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
- package/actionsExecutor/__tests__/feedDecorator.test.ts +30 -0
- package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +72 -0
- package/actionsExecutor/actions/openBottomSheet.ts +1 -0
- package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
- package/actionsExecutor/feedDecorator.ts +6 -0
- package/analyticsUtils/__tests__/playerAnalyticsSafeguard.test.ts +62 -0
- package/analyticsUtils/playerAnalyticsTracker.ts +1 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +11 -0
- package/modalState/ModalOrchestrator.ts +1 -0
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +23 -0
- package/modalState/index.ts +2 -0
- package/modalState/types.ts +11 -0
- package/package.json +2 -2
- package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
- package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
- package/reactHooks/feed/useFeedLoader.tsx +28 -1
- package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ActionResult } from "../ActionExecutor";
|
|
2
|
+
import { localStorageToggleFlag } from "../StorageActions";
|
|
3
|
+
import { TOGGLE_FLAG_MAX_ITEMS_REACHED_EVENT } from "../consts";
|
|
4
|
+
import { postEvent } from "../../reactHooks/useSubscriberFor";
|
|
5
|
+
|
|
6
|
+
jest.mock("../ActionExecutorContext", () => ({
|
|
7
|
+
log_error: jest.fn(),
|
|
8
|
+
log_info: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
jest.mock("../../reactHooks/useSubscriberFor", () => ({
|
|
12
|
+
postEvent: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// In-memory stand-in for the storage-backed provider; the limit check under
|
|
16
|
+
// test lives in storageToggleFlag, not in the provider.
|
|
17
|
+
const mockStore: Record<string, string[]> = {};
|
|
18
|
+
|
|
19
|
+
jest.mock("../../storage/StorageMultiSelectProvider", () => ({
|
|
20
|
+
StorageMultiSelectProvider: {
|
|
21
|
+
getProvider: (keyNamespace: string) => ({
|
|
22
|
+
getSelectedAsync: async () => [...(mockStore[keyNamespace] ?? [])],
|
|
23
|
+
addItem: async (item: string) => {
|
|
24
|
+
mockStore[keyNamespace] = [...(mockStore[keyNamespace] ?? []), item];
|
|
25
|
+
},
|
|
26
|
+
removeItem: async (item: string) => {
|
|
27
|
+
mockStore[keyNamespace] = (mockStore[keyNamespace] ?? []).filter(
|
|
28
|
+
(it) => it !== item
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const KEY = "user_preferences.teams";
|
|
36
|
+
|
|
37
|
+
// Entries of a team selection feed with explicit tap actions
|
|
38
|
+
// (localStorageToggleFlag with key and selector).
|
|
39
|
+
const teamEntry = (n: number) =>
|
|
40
|
+
({
|
|
41
|
+
id: `team-${n}`,
|
|
42
|
+
title: `Team ${n}`,
|
|
43
|
+
type: { value: "action" },
|
|
44
|
+
extensions: { tag: `TEAM ${n}` },
|
|
45
|
+
}) as unknown as ZappEntry;
|
|
46
|
+
|
|
47
|
+
const tapAction = (extraOptions = {}) =>
|
|
48
|
+
({
|
|
49
|
+
type: "localStorageToggleFlag",
|
|
50
|
+
options: { key: KEY, selector: "extensions.tag", ...extraOptions },
|
|
51
|
+
}) as unknown as ActionType;
|
|
52
|
+
|
|
53
|
+
const tapTeams = async (count: number, extraOptions = {}) => {
|
|
54
|
+
const results: ActionResult[] = [];
|
|
55
|
+
|
|
56
|
+
for (let n = 1; n <= count; n++) {
|
|
57
|
+
results.push(
|
|
58
|
+
await localStorageToggleFlag(
|
|
59
|
+
{ entry: teamEntry(n) },
|
|
60
|
+
tapAction(extraOptions)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return results;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
describe("localStorageToggleFlag max_items", () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
delete mockStore[KEY];
|
|
71
|
+
jest.clearAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("blocks the 11th team when max_items is in the action options", async () => {
|
|
75
|
+
const results = await tapTeams(11, { max_items: "10" });
|
|
76
|
+
|
|
77
|
+
expect(results.slice(0, 10)).toEqual(Array(10).fill(ActionResult.Success));
|
|
78
|
+
expect(results[10]).toBe(ActionResult.Cancel);
|
|
79
|
+
expect(mockStore[KEY]).toHaveLength(10);
|
|
80
|
+
|
|
81
|
+
expect(postEvent).toHaveBeenCalledWith(
|
|
82
|
+
TOGGLE_FLAG_MAX_ITEMS_REACHED_EVENT,
|
|
83
|
+
[expect.objectContaining({ maxItems: "10", tag: "TEAM 11" })]
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("still lets a selected team be removed at the limit", async () => {
|
|
88
|
+
await tapTeams(10, { max_items: "10" });
|
|
89
|
+
|
|
90
|
+
const result = await localStorageToggleFlag(
|
|
91
|
+
{ entry: teamEntry(1) },
|
|
92
|
+
tapAction({ max_items: "10" })
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(result).toBe(ActionResult.Success);
|
|
96
|
+
expect(mockStore[KEY]).toHaveLength(9);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -59,3 +59,33 @@ describe("decorateFeed", () => {
|
|
|
59
59
|
expect(decorated.entry[0].extensions.tap_actions).toBeUndefined();
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
|
+
|
|
63
|
+
describe("decorateFeed max_items", () => {
|
|
64
|
+
const toggleAction = (feed: ZappFeed) =>
|
|
65
|
+
decorateFeed(feed).entry[0].extensions.tap_actions.actions[0];
|
|
66
|
+
|
|
67
|
+
it.each([
|
|
68
|
+
["screen", "screenToggleFlag"],
|
|
69
|
+
["session", "sessionStorageToggleFlag"],
|
|
70
|
+
["ctx", "localStorageToggleFlag"],
|
|
71
|
+
])("passes max_items to the %s toggle action", (scope, type) => {
|
|
72
|
+
const feed = preferenceFeed("multi");
|
|
73
|
+
feed.extensions.preference_editor_options.scope = scope;
|
|
74
|
+
feed.extensions.preference_editor_options.max_items = "10";
|
|
75
|
+
|
|
76
|
+
const toggle = toggleAction(feed);
|
|
77
|
+
|
|
78
|
+
expect(toggle.type).toBe(type);
|
|
79
|
+
expect(toggle.options).toEqual({ key: "agree", max_items: 10 });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it.each([undefined, 0, "0", "ten", -1])(
|
|
83
|
+
"does not set a limit for max_items %p",
|
|
84
|
+
(maxItems) => {
|
|
85
|
+
const feed = preferenceFeed("multi");
|
|
86
|
+
feed.extensions.preference_editor_options.max_items = maxItems;
|
|
87
|
+
|
|
88
|
+
expect(toggleAction(feed).options).toEqual({ key: "agree" });
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ActionExecutor, ActionResult } from "../ActionExecutor";
|
|
2
|
+
import { screenToggleFlagAction } from "../actions/screenToggleFlag";
|
|
3
|
+
|
|
4
|
+
jest.mock("../ActionExecutorContext", () => ({
|
|
5
|
+
log_error: jest.fn(),
|
|
6
|
+
log_info: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock("../StorageActions", () => ({
|
|
10
|
+
onMaxTagsReached: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// not used here, but it pulls the native bridge logger in at import time
|
|
14
|
+
jest.mock("../../storage/ScreenSingleValueProvider", () => ({}));
|
|
15
|
+
|
|
16
|
+
jest.mock("../../logger", () => ({
|
|
17
|
+
createLogger: () => ({
|
|
18
|
+
log_error: jest.fn(),
|
|
19
|
+
log_debug: jest.fn(),
|
|
20
|
+
log_info: jest.fn(),
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const mockSelected: string[] = [];
|
|
25
|
+
|
|
26
|
+
jest.mock("../../storage/ScreenStateMultiSelectProvider", () => ({
|
|
27
|
+
ScreenMultiSelectProvider: {
|
|
28
|
+
getProvider: () => ({
|
|
29
|
+
getSelectedAsync: async () => [...mockSelected],
|
|
30
|
+
addItem: async (item: string) => {
|
|
31
|
+
mockSelected.push(item);
|
|
32
|
+
},
|
|
33
|
+
removeItem: async (item: string) => {
|
|
34
|
+
mockSelected.splice(mockSelected.indexOf(item), 1);
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const toggleAction = (options = {}) =>
|
|
41
|
+
({
|
|
42
|
+
type: "screenToggleFlag",
|
|
43
|
+
options: { key: "teams", selector: "extensions.tag", ...options },
|
|
44
|
+
}) as unknown as ActionType;
|
|
45
|
+
|
|
46
|
+
const context = (tag: string) => ({
|
|
47
|
+
screenRoute: "/screen",
|
|
48
|
+
screenStateStore: {},
|
|
49
|
+
entry: { id: tag, extensions: { tag } },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("screenToggleFlagAction at the max_items limit", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mockSelected.length = 0;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns the cancellation so the chained actions do not run", async () => {
|
|
58
|
+
const afterToggle = jest.fn();
|
|
59
|
+
const executor = new ActionExecutor();
|
|
60
|
+
|
|
61
|
+
executor.registerAction("screenToggleFlag", screenToggleFlagAction);
|
|
62
|
+
|
|
63
|
+
executor.registerAction("sendCloudEvent", async () => {
|
|
64
|
+
afterToggle();
|
|
65
|
+
|
|
66
|
+
return ActionResult.Success;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
mockSelected.push("A", "B");
|
|
70
|
+
|
|
71
|
+
const result = await executor.handleActions(
|
|
72
|
+
[toggleAction({ max_items: 2 }), { type: "sendCloudEvent" }],
|
|
73
|
+
context("C") as never
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result).toBe(ActionResult.Cancel);
|
|
77
|
+
expect(afterToggle).not.toHaveBeenCalled();
|
|
78
|
+
expect(mockSelected).toEqual(["A", "B"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("lets the chain continue below the limit", async () => {
|
|
82
|
+
const result = await screenToggleFlagAction(
|
|
83
|
+
toggleAction({ max_items: 2 }),
|
|
84
|
+
context("A") as never
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(result).toBe(ActionResult.Success);
|
|
88
|
+
expect(mockSelected).toEqual(["A"]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -2,6 +2,22 @@ import { openBottomSheetAction } from "../openBottomSheet";
|
|
|
2
2
|
import { actionExecutor, ActionResult } from "../../ActionExecutor";
|
|
3
3
|
import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
|
|
4
4
|
|
|
5
|
+
jest.mock("@applicaster/zapp-pipes-v2-client", () => ({
|
|
6
|
+
PipesClientResponseHelper: jest.fn(() => ({ error: null })),
|
|
7
|
+
RequestBuilder: jest.fn(() => ({
|
|
8
|
+
setEntryContext: jest.fn().mockReturnThis(),
|
|
9
|
+
setScreenContext: jest.fn().mockReturnThis(),
|
|
10
|
+
setUrl: jest.fn().mockReturnThis(),
|
|
11
|
+
buildAxiosRequest: jest.fn().mockResolvedValue(undefined),
|
|
12
|
+
call: jest.fn().mockResolvedValue({
|
|
13
|
+
response: {
|
|
14
|
+
entry: [],
|
|
15
|
+
extensions: { dynamic_collection_options: { operations: "add" } },
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
5
21
|
jest.mock("../../../modalState/store", () => ({
|
|
6
22
|
openBottomSheetModal: jest.fn(),
|
|
7
23
|
}));
|
|
@@ -165,6 +181,62 @@ describe("openBottomSheetAction inline items", () => {
|
|
|
165
181
|
expect(menu.content.emptyMessage).toBe("Queued items will be added here");
|
|
166
182
|
});
|
|
167
183
|
|
|
184
|
+
it("forwards content.operationButtons onto the menu", async () => {
|
|
185
|
+
const result = await openBottomSheetAction({
|
|
186
|
+
type: "openBottomSheet",
|
|
187
|
+
options: {
|
|
188
|
+
header: { title: "Playlists" },
|
|
189
|
+
content: {
|
|
190
|
+
items: [{ id: "p1", title: "Playlist 1" }],
|
|
191
|
+
operationButtons: {
|
|
192
|
+
add: {
|
|
193
|
+
title: "Create New Playlist",
|
|
194
|
+
iconURL: "https://example.com/add.png",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
} as any);
|
|
200
|
+
|
|
201
|
+
expect(result).toBe(ActionResult.Success);
|
|
202
|
+
|
|
203
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
204
|
+
|
|
205
|
+
expect(menu.content.operationButtons).toEqual({
|
|
206
|
+
add: {
|
|
207
|
+
title: "Create New Playlist",
|
|
208
|
+
iconURL: "https://example.com/add.png",
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("preserves Add presentation when loading itemsUrl content", async () => {
|
|
214
|
+
const result = await openBottomSheetAction({
|
|
215
|
+
type: "openBottomSheet",
|
|
216
|
+
options: {
|
|
217
|
+
content: {
|
|
218
|
+
itemsUrl: "https://example.com/collections",
|
|
219
|
+
operationButtons: {
|
|
220
|
+
add: {
|
|
221
|
+
title: "Create Collection",
|
|
222
|
+
iconURL: "file:///bundled-add.png",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result).toBe(ActionResult.Success);
|
|
230
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
231
|
+
|
|
232
|
+
expect(menu.content).toMatchObject({
|
|
233
|
+
operationButtons: {
|
|
234
|
+
add: { title: "Create Collection", iconURL: "file:///bundled-add.png" },
|
|
235
|
+
},
|
|
236
|
+
dynamicCollection: { operations: ["add"] },
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
168
240
|
it("pushes a new sheet by default even when another sheet is already open", async () => {
|
|
169
241
|
(modalOrchestrator as any).stackSize = 1;
|
|
170
242
|
|
|
@@ -27,12 +27,10 @@ export const screenToggleFlagAction: ActionHandler<
|
|
|
27
27
|
const screenRoute = context?.screenRoute;
|
|
28
28
|
const screenStateStore = context?.screenStateStore;
|
|
29
29
|
|
|
30
|
-
await screenToggleFlag(
|
|
30
|
+
return await screenToggleFlag(
|
|
31
31
|
screenRoute,
|
|
32
32
|
screenStateStore,
|
|
33
33
|
{ entry: context?.entry, options: action.options },
|
|
34
34
|
action
|
|
35
35
|
);
|
|
36
|
-
|
|
37
|
-
return ActionResult.Success;
|
|
38
36
|
};
|
|
@@ -47,6 +47,11 @@ function makeMultiSelect(feed: ZappFeed, key, decoratedFeed) {
|
|
|
47
47
|
? "sessionStorageToggleFlag"
|
|
48
48
|
: "localStorageToggleFlag";
|
|
49
49
|
|
|
50
|
+
// the toggle actions read their limit from their own options
|
|
51
|
+
const maxItems = Number(
|
|
52
|
+
decoratedFeed.extensions?.preference_editor_options?.max_items
|
|
53
|
+
);
|
|
54
|
+
|
|
50
55
|
decoratedFeed.entry?.forEach((entry: ZappEntry) => {
|
|
51
56
|
entry.type.value = "action";
|
|
52
57
|
entry.id = entry.extensions?.tag || entry.id;
|
|
@@ -59,6 +64,7 @@ function makeMultiSelect(feed: ZappFeed, key, decoratedFeed) {
|
|
|
59
64
|
type: actionType,
|
|
60
65
|
options: {
|
|
61
66
|
key,
|
|
67
|
+
...(maxItems > 0 && { max_items: maxItems }),
|
|
62
68
|
},
|
|
63
69
|
},
|
|
64
70
|
],
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PlayerAnalyticsTracker } from "../playerAnalyticsTracker";
|
|
2
|
+
import { PLAYBACK_EVENT } from "../events";
|
|
3
|
+
|
|
4
|
+
const mockPostAnalyticEvent = jest.fn();
|
|
5
|
+
|
|
6
|
+
jest.mock("../manager", () => ({
|
|
7
|
+
postAnalyticEvent: (...args) => mockPostAnalyticEvent(...args),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe("Player Analytics Safeguards against nativeEvent leakage", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const mockEntry = {
|
|
16
|
+
id: "vod_123",
|
|
17
|
+
title: "Test Video",
|
|
18
|
+
type: { value: "video" },
|
|
19
|
+
content: { type: "video/mp4" },
|
|
20
|
+
extensions: { duration: 120 },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const mockPlayer = {
|
|
24
|
+
playerPluginId: "quick_brick_player",
|
|
25
|
+
getEntry: () => mockEntry,
|
|
26
|
+
getState: () => ({ isPaused: false, contentPosition: 0.63 }),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it("omits nativeEvent from analytics payload if present in eventData", () => {
|
|
30
|
+
const tracker = new PlayerAnalyticsTracker();
|
|
31
|
+
tracker.setPlayer(mockPlayer as any);
|
|
32
|
+
|
|
33
|
+
const payload = tracker.prepareAnalyticPayload(PLAYBACK_EVENT.play, {
|
|
34
|
+
nativeEvent: { currentTime: 0.63, reason: "video_ready" },
|
|
35
|
+
player_type: "quick_brick_player",
|
|
36
|
+
} as any);
|
|
37
|
+
|
|
38
|
+
expect(payload.nativeEvent).toBeUndefined();
|
|
39
|
+
expect(payload.media_id).toBe("vod_123");
|
|
40
|
+
expect(payload.name).toBe("Test Video");
|
|
41
|
+
expect(payload.player_type).toBe("quick_brick_player");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("properly prepares payload when eventData is unwrapped", () => {
|
|
45
|
+
const tracker = new PlayerAnalyticsTracker();
|
|
46
|
+
tracker.setPlayer(mockPlayer as any);
|
|
47
|
+
|
|
48
|
+
const payload = tracker.prepareAnalyticPayload(PLAYBACK_EVENT.play, {
|
|
49
|
+
currentTime: 0.63,
|
|
50
|
+
reason: "video_ready",
|
|
51
|
+
player_type: "quick_brick_player",
|
|
52
|
+
} as any);
|
|
53
|
+
|
|
54
|
+
expect(payload.nativeEvent).toBeUndefined();
|
|
55
|
+
expect(payload.currentTime).toBeUndefined();
|
|
56
|
+
expect(payload.current_position).toBe(0.63);
|
|
57
|
+
expect(payload.reason).toBe("video_ready");
|
|
58
|
+
expect(payload.media_id).toBe("vod_123");
|
|
59
|
+
expect(payload.name).toBe("Test Video");
|
|
60
|
+
expect(payload.player_type).toBe("quick_brick_player");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -171,6 +171,17 @@ describe("parseModalBlocksConfiguration", () => {
|
|
|
171
171
|
expect(result.item.checkbox.default).toBeUndefined();
|
|
172
172
|
expect(result.item.actionButtons.default).toBeUndefined();
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
it("parses the generic button leading icon asset", () => {
|
|
176
|
+
const result = parseModalBlocksConfiguration(
|
|
177
|
+
{ "button.leading_icon.asset": "https://example.com/add.png" },
|
|
178
|
+
"playlist-experience"
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(result.button.leadingIcon.default?.asset).toBe(
|
|
182
|
+
"https://example.com/add.png"
|
|
183
|
+
);
|
|
184
|
+
});
|
|
174
185
|
});
|
|
175
186
|
|
|
176
187
|
describe("getEmptyModalBlocksStructure", () => {
|
|
@@ -60,4 +60,27 @@ describe("ModalOrchestrator phase 3", () => {
|
|
|
60
60
|
|
|
61
61
|
expect(props.emptyMessage).toBe("Queued items will be added here");
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
it("forwards operationButtons onto the sheet props unchanged", () => {
|
|
65
|
+
const operationButtons = {
|
|
66
|
+
add: {
|
|
67
|
+
title: "Create New Playlist",
|
|
68
|
+
iconURL: "https://example.com/add.png",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
modalOrchestrator.open({
|
|
73
|
+
header: { title: "Playlists" },
|
|
74
|
+
content: {
|
|
75
|
+
title: "Playlists",
|
|
76
|
+
items: [],
|
|
77
|
+
operationButtons,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const props = (modalStore.getState().modalState.screen as any)
|
|
82
|
+
?.modalBottomSheetContentProps;
|
|
83
|
+
|
|
84
|
+
expect(props.operationButtons).toEqual(operationButtons);
|
|
85
|
+
});
|
|
63
86
|
});
|
package/modalState/index.ts
CHANGED
package/modalState/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EntryAction } from "../entryActions/types";
|
|
2
|
+
|
|
1
3
|
export interface Header {
|
|
2
4
|
title: string;
|
|
3
5
|
subtitle?: string;
|
|
@@ -107,6 +109,13 @@ export interface DynamicCollectionOptions {
|
|
|
107
109
|
events?: Record<CollectionScopedOperation | string, Action[]>;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/** Presentation of a sheet-synthesized operation control; same fields as an entry action button. */
|
|
113
|
+
export type OperationButton = Pick<EntryAction["button"], "title" | "iconURL">;
|
|
114
|
+
|
|
115
|
+
export type OperationButtons = Partial<
|
|
116
|
+
Record<CollectionScopedOperation | string, OperationButton>
|
|
117
|
+
>;
|
|
118
|
+
|
|
110
119
|
export interface Content {
|
|
111
120
|
title: string;
|
|
112
121
|
stickyTitle?: boolean;
|
|
@@ -120,6 +129,8 @@ export interface Content {
|
|
|
120
129
|
styleOverrides?: Record<string, any>;
|
|
121
130
|
/** Shown under Next Up when the queue sheet has no upcoming items. */
|
|
122
131
|
emptyMessage?: string;
|
|
132
|
+
/** Producer-owned presentation for sheet-synthesized operation controls (e.g. `add`). */
|
|
133
|
+
operationButtons?: OperationButtons;
|
|
123
134
|
context?: Record<string, any>;
|
|
124
135
|
}
|
|
125
136
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "16.0.0-rc.
|
|
3
|
+
"version": "16.0.0-rc.126",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "16.0.0-rc.
|
|
30
|
+
"@applicaster/applicaster-types": "16.0.0-rc.126",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Provider } from "react-redux";
|
|
3
|
+
import { configureStore } from "@reduxjs/toolkit";
|
|
4
|
+
import { act, renderHook } from "@testing-library/react-native";
|
|
5
|
+
import reducer, {
|
|
6
|
+
loadPipesData,
|
|
7
|
+
unsubscribeFromDataSource,
|
|
8
|
+
zappPipesClear,
|
|
9
|
+
} from "@applicaster/zapp-react-native-redux/ZappPipes";
|
|
10
|
+
import { zappPipesMiddleware } from "../../../../zapp-react-native-redux/ZappPipes/middleware";
|
|
11
|
+
import { useFeedLoader } from "../useFeedLoader";
|
|
12
|
+
|
|
13
|
+
const mockGet = jest.fn();
|
|
14
|
+
const mockPost = jest.fn();
|
|
15
|
+
const mockSubscribe = jest.fn();
|
|
16
|
+
const mockRelease = jest.fn();
|
|
17
|
+
let mockChange: () => void;
|
|
18
|
+
let mockLoad: ReturnType<typeof jest.fn>;
|
|
19
|
+
|
|
20
|
+
jest.mock("@applicaster/zapp-pipes-v2-client", () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
default: jest.fn(() => ({
|
|
23
|
+
get: (...args) => mockGet(...args),
|
|
24
|
+
post: (...args) => mockPost(...args),
|
|
25
|
+
})),
|
|
26
|
+
RequestBuilder: jest.fn(() => ({
|
|
27
|
+
setResolvers() {
|
|
28
|
+
return this;
|
|
29
|
+
},
|
|
30
|
+
build: async () => ({}),
|
|
31
|
+
})),
|
|
32
|
+
subscribeForUrlContextKeyChanges: (...args) => mockSubscribe(...args),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.mock("../useInflatedUrl", () => ({
|
|
36
|
+
useInflatedUrl: ({ feedUrl }) => feedUrl,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.mock("../useLoadPipesDataDispatch", () => ({
|
|
40
|
+
useLoadPipesDataDispatch: () => mockLoad,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const url = "https://example.test/feed";
|
|
44
|
+
const data = { id: "feed", type: { value: "feed" }, entry: [] };
|
|
45
|
+
|
|
46
|
+
const settle = async () => {
|
|
47
|
+
for (let index = 0; index < 30; index++) await Promise.resolve();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const createHarness = (plugins: QuickBrickPlugin[] = []) => {
|
|
51
|
+
const store = configureStore({
|
|
52
|
+
reducer: {
|
|
53
|
+
zappPipes: reducer,
|
|
54
|
+
appData: () => ({ layoutVersion: "v2" }),
|
|
55
|
+
plugins: () => plugins,
|
|
56
|
+
},
|
|
57
|
+
middleware: (defaults) =>
|
|
58
|
+
defaults({ serializableCheck: false }).concat(zappPipesMiddleware),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
mockLoad = jest.fn((feedUrl, options) =>
|
|
62
|
+
store.dispatch(loadPipesData(feedUrl, options))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const wrapper = ({ children }) => (
|
|
66
|
+
<Provider store={store}>{children}</Provider>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return { store, wrapper };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
describe("dependency-driven feed invalidation", () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
jest.clearAllMocks();
|
|
75
|
+
mockGet.mockResolvedValue({ response: data, code: 200 });
|
|
76
|
+
mockPost.mockResolvedValue({ response: data, code: 200 });
|
|
77
|
+
|
|
78
|
+
mockSubscribe.mockImplementation((_url, _options, onChange) => {
|
|
79
|
+
mockChange = onChange;
|
|
80
|
+
|
|
81
|
+
return mockRelease;
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => unsubscribeFromDataSource());
|
|
86
|
+
|
|
87
|
+
it("silently revalidates a shared mounted feed once and keeps its content", async () => {
|
|
88
|
+
const { store, wrapper } = createHarness();
|
|
89
|
+
|
|
90
|
+
const first = renderHook(() => useFeedLoader({ feedUrl: url }), {
|
|
91
|
+
wrapper,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const second = renderHook(() => useFeedLoader({ feedUrl: url }), {
|
|
95
|
+
wrapper,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await act(settle);
|
|
99
|
+
|
|
100
|
+
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(mockSubscribe).toHaveBeenCalledTimes(1);
|
|
102
|
+
let finish;
|
|
103
|
+
|
|
104
|
+
mockGet.mockImplementationOnce(
|
|
105
|
+
() =>
|
|
106
|
+
new Promise((resolve) => {
|
|
107
|
+
finish = resolve;
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await act(async () => {
|
|
112
|
+
mockChange();
|
|
113
|
+
await settle();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(mockGet).toHaveBeenCalledTimes(2);
|
|
117
|
+
expect(first.result.current.data).toEqual(data);
|
|
118
|
+
expect(first.result.current.loading).toBe(false);
|
|
119
|
+
expect(store.getState().zappPipes[url].refreshing).toBe(true);
|
|
120
|
+
|
|
121
|
+
await act(async () => {
|
|
122
|
+
finish({ response: data, code: 200 });
|
|
123
|
+
await settle();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(store.getState().zappPipes[url].stale).toBeUndefined();
|
|
127
|
+
first.unmount();
|
|
128
|
+
second.unmount();
|
|
129
|
+
expect(mockRelease).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("invalidates an unmounted cache without fetching and revalidates on remount", async () => {
|
|
133
|
+
const { store, wrapper } = createHarness();
|
|
134
|
+
|
|
135
|
+
const first = renderHook(() => useFeedLoader({ feedUrl: url }), {
|
|
136
|
+
wrapper,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await act(settle);
|
|
140
|
+
first.unmount();
|
|
141
|
+
|
|
142
|
+
mockChange();
|
|
143
|
+
mockChange();
|
|
144
|
+
await settle();
|
|
145
|
+
|
|
146
|
+
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
147
|
+
|
|
148
|
+
expect(store.getState().zappPipes[url]).toMatchObject({
|
|
149
|
+
stale: true,
|
|
150
|
+
invalidationVersion: 2,
|
|
151
|
+
data,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const second = renderHook(() => useFeedLoader({ feedUrl: url }), {
|
|
155
|
+
wrapper,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await act(settle);
|
|
159
|
+
|
|
160
|
+
expect(mockGet).toHaveBeenCalledTimes(2);
|
|
161
|
+
expect(store.getState().zappPipes[url].stale).toBeUndefined();
|
|
162
|
+
second.unmount();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("runs one follow-up when invalidated repeatedly during a request", async () => {
|
|
166
|
+
const { store, wrapper } = createHarness();
|
|
167
|
+
let finish;
|
|
168
|
+
|
|
169
|
+
mockGet.mockImplementationOnce(
|
|
170
|
+
() =>
|
|
171
|
+
new Promise((resolve) => {
|
|
172
|
+
finish = resolve;
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const hook = renderHook(() => useFeedLoader({ feedUrl: url }), { wrapper });
|
|
177
|
+
await act(settle);
|
|
178
|
+
|
|
179
|
+
await act(async () => {
|
|
180
|
+
mockChange();
|
|
181
|
+
mockChange();
|
|
182
|
+
await settle();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
186
|
+
|
|
187
|
+
await act(async () => {
|
|
188
|
+
finish({ response: data, code: 200 });
|
|
189
|
+
await settle();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(mockGet).toHaveBeenCalledTimes(2);
|
|
193
|
+
expect(store.getState().zappPipes[url].stale).toBeUndefined();
|
|
194
|
+
hook.unmount();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("stops a failed revalidation until another dependency change", async () => {
|
|
198
|
+
const { wrapper } = createHarness();
|
|
199
|
+
const hook = renderHook(() => useFeedLoader({ feedUrl: url }), { wrapper });
|
|
200
|
+
await act(settle);
|
|
201
|
+
mockGet.mockRejectedValueOnce(new Error("network unavailable"));
|
|
202
|
+
|
|
203
|
+
await act(async () => {
|
|
204
|
+
mockChange();
|
|
205
|
+
await settle();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(mockGet).toHaveBeenCalledTimes(2);
|
|
209
|
+
expect(hook.result.current.error).toBeDefined();
|
|
210
|
+
|
|
211
|
+
await act(async () => {
|
|
212
|
+
mockChange();
|
|
213
|
+
await settle();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(mockGet).toHaveBeenCalledTimes(3);
|
|
217
|
+
expect(hook.result.current.error).toBeUndefined();
|
|
218
|
+
hook.unmount();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("releases cache subscriptions for direct clear actions", async () => {
|
|
222
|
+
const { store } = createHarness();
|
|
223
|
+
await store.dispatch(loadPipesData(url));
|
|
224
|
+
await store.dispatch(loadPipesData(`${url}/two`));
|
|
225
|
+
store.dispatch(zappPipesClear(url));
|
|
226
|
+
expect(mockRelease).toHaveBeenCalledTimes(1);
|
|
227
|
+
store.dispatch(zappPipesClear());
|
|
228
|
+
expect(mockRelease).toHaveBeenCalledTimes(2);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("preserves POST method and body when revalidating", async () => {
|
|
232
|
+
const { wrapper } = createHarness();
|
|
233
|
+
const bodyParams = { filter: "recent" };
|
|
234
|
+
|
|
235
|
+
const hook = renderHook(
|
|
236
|
+
() =>
|
|
237
|
+
useFeedLoader({
|
|
238
|
+
feedUrl: url,
|
|
239
|
+
pipesOptions: { method: "post", bodyParams },
|
|
240
|
+
}),
|
|
241
|
+
{ wrapper }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
await act(settle);
|
|
245
|
+
|
|
246
|
+
await act(async () => {
|
|
247
|
+
mockChange();
|
|
248
|
+
await settle();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(mockSubscribe).toHaveBeenCalledWith(
|
|
252
|
+
url,
|
|
253
|
+
{ method: "post" },
|
|
254
|
+
expect.any(Function)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(mockPost).toHaveBeenCalledTimes(2);
|
|
258
|
+
expect(mockPost).toHaveBeenLastCalledWith(url, bodyParams);
|
|
259
|
+
expect(mockGet).not.toHaveBeenCalled();
|
|
260
|
+
hook.unmount();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("does not revalidate a skipped mounted feed until loading is enabled", async () => {
|
|
264
|
+
const { store, wrapper } = createHarness();
|
|
265
|
+
await store.dispatch(loadPipesData(url));
|
|
266
|
+
|
|
267
|
+
const hook = renderHook(
|
|
268
|
+
({ skipLoading }) =>
|
|
269
|
+
useFeedLoader({
|
|
270
|
+
feedUrl: url,
|
|
271
|
+
pipesOptions: { skipLoading },
|
|
272
|
+
}),
|
|
273
|
+
{ wrapper, initialProps: { skipLoading: true } }
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
await act(async () => {
|
|
277
|
+
mockChange();
|
|
278
|
+
await settle();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
282
|
+
expect(store.getState().zappPipes[url].stale).toBe(true);
|
|
283
|
+
|
|
284
|
+
hook.rerender({ skipLoading: false });
|
|
285
|
+
await act(settle);
|
|
286
|
+
expect(mockGet).toHaveBeenCalledTimes(2);
|
|
287
|
+
expect(store.getState().zappPipes[url].stale).toBeUndefined();
|
|
288
|
+
hook.unmount();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("shares GET revalidation with an omitted or empty unused request body", async () => {
|
|
292
|
+
const { store } = createHarness();
|
|
293
|
+
await store.dispatch(loadPipesData(url));
|
|
294
|
+
mockChange();
|
|
295
|
+
mockGet.mockClear();
|
|
296
|
+
|
|
297
|
+
await Promise.all([
|
|
298
|
+
store.dispatch(loadPipesData(url)),
|
|
299
|
+
store.dispatch(loadPipesData(url, { method: "get", bodyParams: {} })),
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("keeps plugin observation until its cache is cleared", async () => {
|
|
306
|
+
const release = jest.fn();
|
|
307
|
+
let changed;
|
|
308
|
+
|
|
309
|
+
const addDataSourceListener = jest.fn((callback) => {
|
|
310
|
+
changed = callback;
|
|
311
|
+
|
|
312
|
+
return release;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const { store, wrapper } = createHarness([
|
|
316
|
+
{
|
|
317
|
+
identifier: "generic-provider",
|
|
318
|
+
module: { addDataSourceListener },
|
|
319
|
+
} as QuickBrickPlugin,
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
const pluginUrl = "pipesv2://generic-provider/feed";
|
|
323
|
+
|
|
324
|
+
const hook = renderHook(() => useFeedLoader({ feedUrl: pluginUrl }), {
|
|
325
|
+
wrapper,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await act(settle);
|
|
329
|
+
hook.unmount();
|
|
330
|
+
changed();
|
|
331
|
+
await settle();
|
|
332
|
+
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
333
|
+
expect(store.getState().zappPipes[pluginUrl].stale).toBe(true);
|
|
334
|
+
expect(release).not.toHaveBeenCalled();
|
|
335
|
+
store.dispatch(zappPipesClear(pluginUrl));
|
|
336
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -2,6 +2,7 @@ import { renderHook, waitFor } from "@testing-library/react-native";
|
|
|
2
2
|
import { allFeedsIsReady, useBatchLoading } from "../useBatchLoading";
|
|
3
3
|
import { WrappedWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
|
|
4
4
|
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
|
|
5
|
+
import * as ZappPipes from "@applicaster/zapp-react-native-redux/ZappPipes";
|
|
5
6
|
|
|
6
7
|
jest.mock("../../navigation");
|
|
7
8
|
|
|
@@ -32,11 +33,16 @@ describe("useBatchLoading", () => {
|
|
|
32
33
|
// ... more items
|
|
33
34
|
];
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
jest
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest
|
|
38
|
+
.spyOn(ZappPipes, "loadPipesData")
|
|
39
|
+
.mockImplementation((url, options) => async (dispatch) => {
|
|
40
|
+
dispatch(ZappPipes.zappPipesRequestStart(url, options));
|
|
41
|
+
});
|
|
37
42
|
});
|
|
38
43
|
|
|
39
44
|
afterEach(() => {
|
|
45
|
+
jest.restoreAllMocks();
|
|
40
46
|
jest.clearAllMocks();
|
|
41
47
|
});
|
|
42
48
|
|
|
@@ -15,6 +15,7 @@ type Props = {
|
|
|
15
15
|
pipesOptions?: {
|
|
16
16
|
clearCache?: boolean;
|
|
17
17
|
loadLocalFavourites?: boolean;
|
|
18
|
+
loadLocalFavorites?: boolean;
|
|
18
19
|
silentRefresh?: boolean;
|
|
19
20
|
method?: "get" | "post";
|
|
20
21
|
bodyParams?: Record<string, unknown>;
|
|
@@ -45,11 +46,36 @@ export const useFeedLoader = ({
|
|
|
45
46
|
|
|
46
47
|
const loadPipesDataDispatcher = useLoadPipesDataDispatch();
|
|
47
48
|
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (
|
|
51
|
+
currentFeed?.stale &&
|
|
52
|
+
!currentFeed.refreshing &&
|
|
53
|
+
(!currentFeed.error || isInitialRender) &&
|
|
54
|
+
!pipesOptions.skipLoading
|
|
55
|
+
) {
|
|
56
|
+
loadPipesDataDispatcher(
|
|
57
|
+
callableFeedUrl,
|
|
58
|
+
{ ...pipesOptions, clearCache: false, silentRefresh: true },
|
|
59
|
+
{ withResolvers: true }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}, [
|
|
63
|
+
callableFeedUrl,
|
|
64
|
+
currentFeed?.stale,
|
|
65
|
+
currentFeed?.invalidationVersion,
|
|
66
|
+
currentFeed?.refreshing,
|
|
67
|
+
currentFeed?.error,
|
|
68
|
+
pipesOptions,
|
|
69
|
+
loadPipesDataDispatcher,
|
|
70
|
+
isInitialRender,
|
|
71
|
+
]);
|
|
72
|
+
|
|
48
73
|
const reloadData = React.useCallback<ReloadDataFunction>(
|
|
49
74
|
(silentRefresh = true, callback) => {
|
|
50
75
|
loadPipesDataDispatcher(
|
|
51
76
|
callableFeedUrl,
|
|
52
77
|
{
|
|
78
|
+
...pipesOptions,
|
|
53
79
|
clearCache: true,
|
|
54
80
|
silentRefresh,
|
|
55
81
|
callback,
|
|
@@ -59,7 +85,7 @@ export const useFeedLoader = ({
|
|
|
59
85
|
}
|
|
60
86
|
);
|
|
61
87
|
},
|
|
62
|
-
[callableFeedUrl]
|
|
88
|
+
[callableFeedUrl, pipesOptions, loadPipesDataDispatcher]
|
|
63
89
|
);
|
|
64
90
|
|
|
65
91
|
const loadNext: FeedLoaderResponse["loadNext"] = React.useCallback(() => {
|
|
@@ -81,6 +107,7 @@ export const useFeedLoader = ({
|
|
|
81
107
|
|
|
82
108
|
useEffect(() => {
|
|
83
109
|
if (
|
|
110
|
+
!currentFeed?.stale &&
|
|
84
111
|
shouldDispatchData(callableFeedUrl, currentFeed, pipesOptions.clearCache)
|
|
85
112
|
) {
|
|
86
113
|
if (callableFeedUrl && !pipesOptions.skipLoading) {
|
|
@@ -19,14 +19,6 @@ export const useLoadPipesDataDispatch = () => {
|
|
|
19
19
|
(screenData as LegacyNavigationScreenData)?.targetScreen?.id ??
|
|
20
20
|
screenData?.id;
|
|
21
21
|
|
|
22
|
-
const onLoadCB = (options) => (data, _err) => {
|
|
23
|
-
options?.callback?.();
|
|
24
|
-
|
|
25
|
-
if (data) {
|
|
26
|
-
applyScreenRouteDefaults(data, screenStateStore, pathname);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
22
|
return React.useCallback(
|
|
31
23
|
(
|
|
32
24
|
url: string,
|
|
@@ -36,6 +28,9 @@ export const useLoadPipesDataDispatch = () => {
|
|
|
36
28
|
clearCache?: boolean;
|
|
37
29
|
silentRefresh?: boolean;
|
|
38
30
|
parentFeed?: string;
|
|
31
|
+
method?: "get" | "post";
|
|
32
|
+
bodyParams?: Record<string, unknown>;
|
|
33
|
+
loadLocalFavorites?: boolean;
|
|
39
34
|
} = {},
|
|
40
35
|
{
|
|
41
36
|
withResolvers = false,
|
|
@@ -46,18 +41,24 @@ export const useLoadPipesDataDispatch = () => {
|
|
|
46
41
|
} = {}
|
|
47
42
|
) => {
|
|
48
43
|
if (url) {
|
|
49
|
-
dispatch(
|
|
44
|
+
return dispatch(
|
|
50
45
|
ZappPipes.loadPipesData(url, {
|
|
51
46
|
riverId,
|
|
52
47
|
resolvers: withResolvers ? resolvers : undefined,
|
|
53
48
|
...options,
|
|
54
49
|
callback: withScreenRouteMapping
|
|
55
|
-
?
|
|
50
|
+
? (data, error) => {
|
|
51
|
+
options.callback?.(data, error);
|
|
52
|
+
|
|
53
|
+
if (data) {
|
|
54
|
+
applyScreenRouteDefaults(data, screenStateStore, pathname);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
56
57
|
: options?.callback,
|
|
57
58
|
})
|
|
58
59
|
);
|
|
59
60
|
}
|
|
60
61
|
},
|
|
61
|
-
[]
|
|
62
|
+
[dispatch, riverId, resolvers, screenStateStore, pathname]
|
|
62
63
|
);
|
|
63
64
|
};
|