@applicaster/zapp-react-native-ui-components 13.0.8 → 13.0.9-alpha.8843371874
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/Components/Cell/__tests__/CellWIthFocusable.test.js +3 -2
- package/Components/FeedLoader/FeedLoader.tsx +4 -14
- package/Components/FeedLoader/FeedLoaderHOC.tsx +19 -0
- package/Components/FeedLoader/index.js +2 -8
- package/Components/GeneralContentScreen/utils/useCurationAPI.ts +5 -6
- package/Components/MasterCell/DefaultComponents/ActionButton.tsx +2 -0
- package/Components/MasterCell/utils/behaviorProvider.ts +82 -14
- package/Components/MasterCell/utils/index.ts +11 -5
- package/Components/River/RefreshControl.tsx +11 -17
- package/Components/River/__tests__/river.test.js +12 -26
- package/Contexts/ScreenContext/index.tsx +46 -6
- package/Decorators/RiverFeedLoader/__tests__/__snapshots__/riverFeedLoader.test.tsx.snap +221 -209
- package/Decorators/RiverFeedLoader/__tests__/riverFeedLoader.test.tsx +14 -16
- package/Decorators/RiverFeedLoader/__tests__/utils.test.ts +0 -20
- package/Decorators/RiverFeedLoader/index.tsx +22 -4
- package/Decorators/RiverFeedLoader/utils/index.ts +0 -18
- package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +368 -0
- package/Decorators/ZappPipesDataConnector/index.tsx +20 -5
- package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +266 -0
- package/Decorators/ZappPipesDataConnector/utils/mongoFilter.ts +738 -0
- package/Decorators/ZappPipesDataConnector/utils/useFilter.tsx +159 -0
- package/package.json +5 -5
- package/Components/River/__tests__/__snapshots__/river.test.js.snap +0 -27
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import * as useFeedLoaderModule from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader";
|
|
3
|
+
import * as useFeedRefreshModule from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedRefresh";
|
|
4
|
+
import { favoritesListener } from "@applicaster/zapp-react-native-bridge/Favorites";
|
|
5
|
+
import { UrlFeedResolver } from "../resolvers/UrlFeedResolver";
|
|
6
|
+
import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
|
|
7
|
+
import * as ZappPipesModule from "@applicaster/zapp-pipes-v2-client";
|
|
8
|
+
|
|
9
|
+
const mockSubscribeForKeyChanges = jest.fn();
|
|
10
|
+
|
|
11
|
+
jest
|
|
12
|
+
.spyOn(ZappPipesModule, "subscribeForKeyChanges")
|
|
13
|
+
.mockImplementation(mockSubscribeForKeyChanges);
|
|
14
|
+
|
|
15
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader");
|
|
16
|
+
|
|
17
|
+
jest.mock(
|
|
18
|
+
"@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedRefresh"
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
jest.mock("@applicaster/zapp-pipes-v2-client");
|
|
22
|
+
|
|
23
|
+
jest.mock("@applicaster/zapp-react-native-bridge/Favorites", () => ({
|
|
24
|
+
favoritesListener: {
|
|
25
|
+
on: jest.fn(),
|
|
26
|
+
removeHandler: jest.fn(),
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock("../utils", () => ({
|
|
31
|
+
isVerticalListOrGrid: jest.fn(
|
|
32
|
+
(component) =>
|
|
33
|
+
component?.type === "vertical_list" || component?.type === "vertical_grid"
|
|
34
|
+
),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const componentRequiredKeys = { rules: { item_limit: 10 } };
|
|
38
|
+
|
|
39
|
+
describe("UrlFeedResolver", () => {
|
|
40
|
+
const mockChildren = jest.fn(() => <div data-testid="mock-children" />);
|
|
41
|
+
const mockReloadData = jest.fn();
|
|
42
|
+
const mockLoadNext = jest.fn();
|
|
43
|
+
|
|
44
|
+
const mockAddDataSourceListener = jest.fn().mockReturnValue(jest.fn());
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
jest.clearAllMocks();
|
|
48
|
+
|
|
49
|
+
(useFeedLoaderModule.useFeedLoader as jest.Mock).mockReturnValue({
|
|
50
|
+
reloadData: mockReloadData,
|
|
51
|
+
loadNext: mockLoadNext,
|
|
52
|
+
error: null,
|
|
53
|
+
loading: false,
|
|
54
|
+
url: "test-url",
|
|
55
|
+
data: { entry: [{ id: "1" }] },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should use feedUrl directly when provided", () => {
|
|
60
|
+
const props = {
|
|
61
|
+
feedUrl: "https://example.com/feed",
|
|
62
|
+
component: { ...componentRequiredKeys } as any,
|
|
63
|
+
children: mockChildren,
|
|
64
|
+
riverId: "test-river",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
68
|
+
|
|
69
|
+
expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({
|
|
70
|
+
feedUrl: "https://example.com/feed",
|
|
71
|
+
pipesOptions: expect.any(Object),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should use component.data.source when no feedUrl is provided", () => {
|
|
76
|
+
const props = {
|
|
77
|
+
component: {
|
|
78
|
+
data: {
|
|
79
|
+
source: "https://example.com/source",
|
|
80
|
+
},
|
|
81
|
+
...componentRequiredKeys,
|
|
82
|
+
} as any,
|
|
83
|
+
children: mockChildren,
|
|
84
|
+
riverId: "test-river",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
88
|
+
|
|
89
|
+
expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({
|
|
90
|
+
feedUrl: "https://example.com/source",
|
|
91
|
+
pipesOptions: expect.any(Object),
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should handle FAVOURITES type correctly", () => {
|
|
96
|
+
const props = {
|
|
97
|
+
component: {
|
|
98
|
+
data: {
|
|
99
|
+
source: "favorites-source",
|
|
100
|
+
type: "FAVOURITES",
|
|
101
|
+
},
|
|
102
|
+
...componentRequiredKeys,
|
|
103
|
+
} as any,
|
|
104
|
+
children: mockChildren,
|
|
105
|
+
riverId: "test-river",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
109
|
+
|
|
110
|
+
expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({
|
|
111
|
+
feedUrl: "favorites-source",
|
|
112
|
+
pipesOptions: expect.objectContaining({
|
|
113
|
+
clearCache: true,
|
|
114
|
+
loadLocalFavorites: true,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(favoritesListener.on).toHaveBeenCalledWith(
|
|
119
|
+
"FAVORITES_CHANGED",
|
|
120
|
+
mockReloadData
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should handle APPLICASTER_COLLECTION type correctly", () => {
|
|
125
|
+
const props = {
|
|
126
|
+
component: {
|
|
127
|
+
data: {
|
|
128
|
+
source: "collection-id",
|
|
129
|
+
type: "APPLICASTER_COLLECTION",
|
|
130
|
+
},
|
|
131
|
+
...componentRequiredKeys,
|
|
132
|
+
} as any,
|
|
133
|
+
children: mockChildren,
|
|
134
|
+
riverId: "test-river",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
138
|
+
|
|
139
|
+
expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({
|
|
140
|
+
feedUrl:
|
|
141
|
+
"applicaster://fetchData?type=APPLICASTER_COLLECTION&collectionId=collection-id",
|
|
142
|
+
pipesOptions: expect.any(Object),
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should handle APPLICASTER_CATEGORY type correctly", () => {
|
|
147
|
+
const props = {
|
|
148
|
+
component: {
|
|
149
|
+
data: {
|
|
150
|
+
source: "category-id",
|
|
151
|
+
type: "APPLICASTER_CATEGORY",
|
|
152
|
+
},
|
|
153
|
+
...componentRequiredKeys,
|
|
154
|
+
} as any,
|
|
155
|
+
children: mockChildren,
|
|
156
|
+
riverId: "test-river",
|
|
157
|
+
plugins: [],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
161
|
+
|
|
162
|
+
expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({
|
|
163
|
+
feedUrl: expect.stringContaining(
|
|
164
|
+
"applicaster://fetchData?type=APPLICASTER_CATEGORY&categoryId=category-id"
|
|
165
|
+
),
|
|
166
|
+
pipesOptions: expect.any(Object),
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should set up URL context key changes listener", () => {
|
|
171
|
+
const props = {
|
|
172
|
+
feedUrl: "https://example.com/feed",
|
|
173
|
+
component: { ...componentRequiredKeys } as any,
|
|
174
|
+
children: mockChildren,
|
|
175
|
+
riverId: "test-river",
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
179
|
+
|
|
180
|
+
expect(mockSubscribeForKeyChanges.mock.calls[0][0]).toMatchObject({
|
|
181
|
+
url: "https://example.com/feed",
|
|
182
|
+
callback: mockReloadData,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should set up pipesv2 URL listener from plugin if available", () => {
|
|
187
|
+
const props = {
|
|
188
|
+
feedUrl: "pipesv2://test-plugin/endpoint",
|
|
189
|
+
component: { ...componentRequiredKeys } as any,
|
|
190
|
+
children: mockChildren,
|
|
191
|
+
riverId: "test-river",
|
|
192
|
+
plugins: [
|
|
193
|
+
{
|
|
194
|
+
identifier: "test-plugin",
|
|
195
|
+
module: {
|
|
196
|
+
addDataSourceListener: mockAddDataSourceListener,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
] as any,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
203
|
+
|
|
204
|
+
expect(mockAddDataSourceListener).toHaveBeenCalledWith(mockReloadData);
|
|
205
|
+
expect(mockSubscribeForKeyChanges).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should apply item limit from component rules", () => {
|
|
209
|
+
const props = {
|
|
210
|
+
feedUrl: "https://example.com/feed",
|
|
211
|
+
component: {
|
|
212
|
+
rules: {
|
|
213
|
+
item_limit: "2",
|
|
214
|
+
},
|
|
215
|
+
} as any,
|
|
216
|
+
children: mockChildren,
|
|
217
|
+
riverId: "test-river",
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
(useFeedLoaderModule.useFeedLoader as jest.Mock).mockReturnValue({
|
|
221
|
+
reloadData: mockReloadData,
|
|
222
|
+
loadNext: mockLoadNext,
|
|
223
|
+
error: null,
|
|
224
|
+
loading: false,
|
|
225
|
+
url: "test-url",
|
|
226
|
+
data: { entry: [{ id: "1" }, { id: "2" }, { id: "3" }] },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
230
|
+
|
|
231
|
+
expect(mockChildren).toHaveBeenCalledWith(
|
|
232
|
+
expect.objectContaining({
|
|
233
|
+
zappPipesData: expect.objectContaining({
|
|
234
|
+
data: expect.objectContaining({
|
|
235
|
+
entry: [{ id: "1" }, { id: "2" }],
|
|
236
|
+
}),
|
|
237
|
+
}),
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should conditionally include loadNextData based on isLast and component type", () => {
|
|
243
|
+
// Case 1: isLast=true, should not include loadNextData regardless of component type
|
|
244
|
+
const props1 = {
|
|
245
|
+
feedUrl: "https://example.com/feed",
|
|
246
|
+
component: {
|
|
247
|
+
type: "vertical_list",
|
|
248
|
+
...componentRequiredKeys,
|
|
249
|
+
} as any,
|
|
250
|
+
children: mockChildren,
|
|
251
|
+
riverId: "test-river",
|
|
252
|
+
isLast: true,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
renderWithProviders(<UrlFeedResolver {...props1} />);
|
|
256
|
+
|
|
257
|
+
expect(mockChildren).toHaveBeenCalledWith(
|
|
258
|
+
expect.objectContaining({
|
|
259
|
+
loadNextData: mockLoadNext,
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
mockChildren.mockClear();
|
|
264
|
+
|
|
265
|
+
// Case 2: isLast=false, vertical_list component, should not include loadNextData
|
|
266
|
+
const props2 = {
|
|
267
|
+
feedUrl: "https://example.com/feed",
|
|
268
|
+
component: {
|
|
269
|
+
type: "vertical_list",
|
|
270
|
+
...componentRequiredKeys,
|
|
271
|
+
} as any,
|
|
272
|
+
children: mockChildren,
|
|
273
|
+
riverId: "test-river",
|
|
274
|
+
isLast: false,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
renderWithProviders(<UrlFeedResolver {...props2} />);
|
|
278
|
+
|
|
279
|
+
expect(mockChildren).toHaveBeenCalledWith(
|
|
280
|
+
expect.objectContaining({
|
|
281
|
+
loadNextData: undefined,
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
mockChildren.mockClear();
|
|
286
|
+
|
|
287
|
+
// Case 3: isLast=false, non-vertical component, should include loadNextData
|
|
288
|
+
const props3 = {
|
|
289
|
+
feedUrl: "https://example.com/feed",
|
|
290
|
+
component: {
|
|
291
|
+
type: "horizontal_list",
|
|
292
|
+
...componentRequiredKeys,
|
|
293
|
+
} as any,
|
|
294
|
+
children: mockChildren,
|
|
295
|
+
riverId: "test-river",
|
|
296
|
+
isLast: false,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
renderWithProviders(<UrlFeedResolver {...props3} />);
|
|
300
|
+
|
|
301
|
+
expect(mockChildren).toHaveBeenCalledWith(
|
|
302
|
+
expect.objectContaining({
|
|
303
|
+
loadNextData: mockLoadNext,
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should pass correct zappPipesDataProps to children", () => {
|
|
309
|
+
const props = {
|
|
310
|
+
feedUrl: "https://example.com/feed",
|
|
311
|
+
component: { ...componentRequiredKeys } as any,
|
|
312
|
+
children: mockChildren,
|
|
313
|
+
riverId: "test-river",
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
317
|
+
|
|
318
|
+
expect(mockChildren).toHaveBeenCalledWith({
|
|
319
|
+
zappPipesData: {
|
|
320
|
+
url: "test-url",
|
|
321
|
+
loading: false,
|
|
322
|
+
data: { entry: [{ id: "1" }] },
|
|
323
|
+
error: null,
|
|
324
|
+
},
|
|
325
|
+
reloadData: mockReloadData,
|
|
326
|
+
loadNextData: mockLoadNext,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("should apply feed refresh hook", () => {
|
|
331
|
+
const props = {
|
|
332
|
+
feedUrl: "https://example.com/feed",
|
|
333
|
+
component: { ...componentRequiredKeys } as any,
|
|
334
|
+
children: mockChildren,
|
|
335
|
+
riverId: "test-river",
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
339
|
+
|
|
340
|
+
expect(useFeedRefreshModule.useFeedRefresh).toHaveBeenCalledWith({
|
|
341
|
+
reloadData: mockReloadData,
|
|
342
|
+
component: props.component,
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("should clean up listeners on unmount", () => {
|
|
347
|
+
const props = {
|
|
348
|
+
component: {
|
|
349
|
+
data: {
|
|
350
|
+
source: "favorites-source",
|
|
351
|
+
type: "FAVOURITES",
|
|
352
|
+
},
|
|
353
|
+
...componentRequiredKeys,
|
|
354
|
+
} as any,
|
|
355
|
+
children: mockChildren,
|
|
356
|
+
riverId: "test-river",
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const { unmount } = renderWithProviders(<UrlFeedResolver {...props} />);
|
|
360
|
+
|
|
361
|
+
unmount();
|
|
362
|
+
|
|
363
|
+
expect(favoritesListener.removeHandler).toHaveBeenCalledWith(
|
|
364
|
+
"FAVORITES_CHANGED",
|
|
365
|
+
mockReloadData
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -17,9 +17,11 @@ import {
|
|
|
17
17
|
|
|
18
18
|
import { ZappPipesSearchContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
|
|
19
19
|
import { useScreenContext } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
20
|
+
import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
|
|
21
|
+
import { subscribeForKeyChanges } from "@applicaster/zapp-pipes-v2-client";
|
|
20
22
|
|
|
21
23
|
import { isVerticalListOrGrid } from "./utils";
|
|
22
|
-
import {
|
|
24
|
+
import { useFilter } from "./utils/useFilter";
|
|
23
25
|
|
|
24
26
|
type Props = {
|
|
25
27
|
component: ZappUIComponent;
|
|
@@ -204,7 +206,7 @@ export function zappPipesDataConnector(
|
|
|
204
206
|
Component: React.FC<any> | React.ComponentClass<any>
|
|
205
207
|
) {
|
|
206
208
|
return function WrappedWithZappPipesData(props: Props) {
|
|
207
|
-
const { screenData } = useRoute();
|
|
209
|
+
const { pathname, screenData } = useRoute();
|
|
208
210
|
const { plugins } = usePickFromState(["plugins"]);
|
|
209
211
|
|
|
210
212
|
const screenContextData = useScreenContext();
|
|
@@ -286,6 +288,8 @@ export function zappPipesDataConnector(
|
|
|
286
288
|
componentIndex
|
|
287
289
|
);
|
|
288
290
|
|
|
291
|
+
const screenStateStore = useScreenStateStore();
|
|
292
|
+
|
|
289
293
|
useEffect(() => {
|
|
290
294
|
if (dataSourceUrl?.includes("pipesv2://") && reloadData) {
|
|
291
295
|
const addListener = getListenerFromPlugin(dataSourceUrl, plugins);
|
|
@@ -294,9 +298,14 @@ export function zappPipesDataConnector(
|
|
|
294
298
|
return addListener(reloadData);
|
|
295
299
|
}
|
|
296
300
|
} else {
|
|
297
|
-
return
|
|
301
|
+
return subscribeForKeyChanges({
|
|
302
|
+
url: dataSourceUrl,
|
|
303
|
+
pathname,
|
|
304
|
+
screenStateStore,
|
|
305
|
+
callback: reloadData,
|
|
306
|
+
});
|
|
298
307
|
}
|
|
299
|
-
}, [dataSourceUrl, reloadData]);
|
|
308
|
+
}, [dataSourceUrl, reloadData, pathname, screenStateStore]);
|
|
300
309
|
|
|
301
310
|
useEffect(() => {
|
|
302
311
|
if (type === FAVORITES_TYPE && reloadData) {
|
|
@@ -316,12 +325,17 @@ export function zappPipesDataConnector(
|
|
|
316
325
|
[staticFeed?.loading, staticFeed?.data, data?.next, loading, data]
|
|
317
326
|
);
|
|
318
327
|
|
|
328
|
+
const applyItemFilter = useFilter({ url, loading, data, error }, component);
|
|
329
|
+
|
|
319
330
|
const zappPipesDataProps = useMemo(() => {
|
|
320
331
|
const loadNextData =
|
|
321
332
|
!isLast && isVerticalListOrGrid(component) ? undefined : loadNext;
|
|
322
333
|
|
|
323
334
|
return {
|
|
324
|
-
zappPipesData: applyItemLimit(
|
|
335
|
+
zappPipesData: applyItemLimit(
|
|
336
|
+
applyItemFilter(getZappPipesData()),
|
|
337
|
+
component
|
|
338
|
+
),
|
|
325
339
|
reloadData,
|
|
326
340
|
loadNextData,
|
|
327
341
|
};
|
|
@@ -335,6 +349,7 @@ export function zappPipesDataConnector(
|
|
|
335
349
|
data,
|
|
336
350
|
isLast,
|
|
337
351
|
component,
|
|
352
|
+
applyItemFilter,
|
|
338
353
|
]);
|
|
339
354
|
|
|
340
355
|
useFeedRefresh({
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/// <reference types="@applicaster/applicaster-types" />
|
|
2
|
+
/// <reference types="@applicaster/zapp-react-native-ui-components" />
|
|
3
|
+
import React, { useEffect, useMemo } from "react";
|
|
4
|
+
import * as R from "ramda";
|
|
5
|
+
import { Platform } from "react-native";
|
|
6
|
+
import Url from "url";
|
|
7
|
+
import { favoritesListener } from "@applicaster/zapp-react-native-bridge/Favorites";
|
|
8
|
+
import {
|
|
9
|
+
getInflatedDataSourceUrl,
|
|
10
|
+
getSearchContext,
|
|
11
|
+
useFeedLoader,
|
|
12
|
+
useFeedRefresh,
|
|
13
|
+
useRoute,
|
|
14
|
+
} from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
15
|
+
|
|
16
|
+
import { ComponentDataSourceContext, ZappPipesDataProps } from "../types";
|
|
17
|
+
import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
|
|
18
|
+
import { isVerticalListOrGrid } from "../utils";
|
|
19
|
+
import { useFilter } from "../utils/useFilter";
|
|
20
|
+
import { subscribeForKeyChanges } from "@applicaster/zapp-pipes-v2-client";
|
|
21
|
+
|
|
22
|
+
const FAVORITES_TYPE = "FAVOURITES";
|
|
23
|
+
|
|
24
|
+
function getDataSourceUrl({
|
|
25
|
+
component,
|
|
26
|
+
feedUrl,
|
|
27
|
+
screenData,
|
|
28
|
+
entryContext,
|
|
29
|
+
searchContext,
|
|
30
|
+
screenContext,
|
|
31
|
+
}): { dataSourceUrl: string | null; type: string | null } {
|
|
32
|
+
if (feedUrl) {
|
|
33
|
+
return { dataSourceUrl: feedUrl, type: null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (component.data) {
|
|
37
|
+
const {
|
|
38
|
+
data: { source, type, mapping },
|
|
39
|
+
} = component;
|
|
40
|
+
|
|
41
|
+
if (type === FAVORITES_TYPE) {
|
|
42
|
+
return { dataSourceUrl: source || FAVORITES_TYPE, type };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (source) {
|
|
46
|
+
if (type === "APPLICASTER_COLLECTION" && !R.includes("://", source)) {
|
|
47
|
+
return {
|
|
48
|
+
dataSourceUrl: `applicaster://fetchData?type=${type}&collectionId=${source}`,
|
|
49
|
+
type,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (type === "APPLICASTER_CATEGORY" && !R.includes("://", source)) {
|
|
54
|
+
return {
|
|
55
|
+
dataSourceUrl: `applicaster://fetchData?type=${type}&categoryId=${source}&platform=${Platform.OS}`,
|
|
56
|
+
type,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (mapping) {
|
|
61
|
+
const contexts = {
|
|
62
|
+
entry: entryContext,
|
|
63
|
+
screen: screenContext || screenData,
|
|
64
|
+
search: getSearchContext(searchContext, mapping),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const dataSourceUrl = getInflatedDataSourceUrl({
|
|
68
|
+
source,
|
|
69
|
+
mapping,
|
|
70
|
+
contexts,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!dataSourceUrl) {
|
|
74
|
+
return { dataSourceUrl: null, type: null };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { dataSourceUrl, type };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { dataSourceUrl: source, type };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { dataSourceUrl: null, type: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getMethodAndParams(component) {
|
|
88
|
+
const method: "get" | "post" = R.pathOr("get", ["data", "method"], component);
|
|
89
|
+
|
|
90
|
+
const bodyParams: Record<string, unknown> = R.pathOr(
|
|
91
|
+
{},
|
|
92
|
+
["data", "bodyParams"],
|
|
93
|
+
component
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
method,
|
|
98
|
+
bodyParams,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getListenerFromPlugin(
|
|
103
|
+
dataSourceUrl: string,
|
|
104
|
+
plugins: QuickBrickPlugin[]
|
|
105
|
+
): AddDataSourceListener | void {
|
|
106
|
+
const url = Url.parse(dataSourceUrl, false);
|
|
107
|
+
|
|
108
|
+
const addListener = R.compose(
|
|
109
|
+
R.path(["module", "addDataSourceListener"]),
|
|
110
|
+
R.find(R.propEq("identifier", url.host))
|
|
111
|
+
)(plugins);
|
|
112
|
+
|
|
113
|
+
return addListener;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function applyItemLimit(zappPipesData, component) {
|
|
117
|
+
if (!zappPipesData || !zappPipesData.data) {
|
|
118
|
+
return zappPipesData;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { data } = zappPipesData;
|
|
122
|
+
|
|
123
|
+
const {
|
|
124
|
+
rules: { item_limit },
|
|
125
|
+
} = component;
|
|
126
|
+
|
|
127
|
+
if (item_limit && data.entry && data.entry.length) {
|
|
128
|
+
return {
|
|
129
|
+
...zappPipesData,
|
|
130
|
+
data: {
|
|
131
|
+
...data,
|
|
132
|
+
entry: R.slice(
|
|
133
|
+
0,
|
|
134
|
+
Math.min(Number(item_limit), R.length(data.entry)),
|
|
135
|
+
data.entry
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return zappPipesData;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type UrlFeedResolverProps = ComponentDataSourceContext & {
|
|
145
|
+
children: (dataProps: ZappPipesDataProps) => React.ReactNode;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export function UrlFeedResolver({
|
|
149
|
+
children,
|
|
150
|
+
component,
|
|
151
|
+
feedUrl,
|
|
152
|
+
isLast,
|
|
153
|
+
entryContext,
|
|
154
|
+
screenContext,
|
|
155
|
+
searchContext,
|
|
156
|
+
screenData,
|
|
157
|
+
plugins,
|
|
158
|
+
}: UrlFeedResolverProps) {
|
|
159
|
+
const { dataSourceUrl, type } = useMemo(
|
|
160
|
+
() =>
|
|
161
|
+
getDataSourceUrl({
|
|
162
|
+
component,
|
|
163
|
+
feedUrl,
|
|
164
|
+
screenData,
|
|
165
|
+
entryContext,
|
|
166
|
+
searchContext,
|
|
167
|
+
screenContext,
|
|
168
|
+
}),
|
|
169
|
+
[
|
|
170
|
+
component?.id,
|
|
171
|
+
feedUrl,
|
|
172
|
+
entryContext,
|
|
173
|
+
searchContext,
|
|
174
|
+
screenContext,
|
|
175
|
+
screenData?.id,
|
|
176
|
+
]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const shouldClearCache = useMemo(
|
|
180
|
+
() => Boolean(component?.rules?.clear_cache_on_reload),
|
|
181
|
+
[component?.id]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const pipesOptions = useMemo(
|
|
185
|
+
() => ({
|
|
186
|
+
clearCache: type === FAVORITES_TYPE,
|
|
187
|
+
loadLocalFavorites: type === FAVORITES_TYPE,
|
|
188
|
+
silentRefresh: !shouldClearCache,
|
|
189
|
+
...getMethodAndParams(component),
|
|
190
|
+
}),
|
|
191
|
+
[type, shouldClearCache, component]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const { reloadData, loadNext, error, loading, url, data } = useFeedLoader({
|
|
195
|
+
feedUrl: dataSourceUrl ?? "",
|
|
196
|
+
pipesOptions,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const { pathname } = useRoute();
|
|
200
|
+
const screenStateStore = useScreenStateStore();
|
|
201
|
+
|
|
202
|
+
// Setup listeners for data source URL
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (!reloadData) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (dataSourceUrl?.includes("pipesv2://")) {
|
|
209
|
+
(
|
|
210
|
+
getListenerFromPlugin(dataSourceUrl, plugins) as AddDataSourceListener
|
|
211
|
+
)?.(reloadData);
|
|
212
|
+
} else {
|
|
213
|
+
return subscribeForKeyChanges({
|
|
214
|
+
url: dataSourceUrl,
|
|
215
|
+
pathname,
|
|
216
|
+
screenStateStore,
|
|
217
|
+
callback: reloadData,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}, [dataSourceUrl, reloadData, pathname, screenStateStore]);
|
|
221
|
+
|
|
222
|
+
// Setup favorites listener
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (type === FAVORITES_TYPE && reloadData) {
|
|
225
|
+
favoritesListener.on("FAVORITES_CHANGED", reloadData);
|
|
226
|
+
|
|
227
|
+
return () => {
|
|
228
|
+
favoritesListener.removeHandler("FAVORITES_CHANGED", reloadData);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}, [type, reloadData]);
|
|
232
|
+
|
|
233
|
+
// Apply feed refresh hook
|
|
234
|
+
useFeedRefresh({
|
|
235
|
+
reloadData,
|
|
236
|
+
component,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const loadNextData = useMemo(
|
|
240
|
+
() => (!isLast && isVerticalListOrGrid(component) ? undefined : loadNext),
|
|
241
|
+
[isLast, component, loadNext]
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const applyItemFilter = useFilter({ url, loading, data, error }, component);
|
|
245
|
+
|
|
246
|
+
const zappPipesDataProps = useMemo(() => {
|
|
247
|
+
const pipeData = { url, loading, data, error };
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
zappPipesData: applyItemLimit(applyItemFilter(pipeData), component),
|
|
251
|
+
reloadData,
|
|
252
|
+
loadNextData,
|
|
253
|
+
};
|
|
254
|
+
}, [
|
|
255
|
+
url,
|
|
256
|
+
loading,
|
|
257
|
+
data,
|
|
258
|
+
error,
|
|
259
|
+
component,
|
|
260
|
+
reloadData,
|
|
261
|
+
loadNextData,
|
|
262
|
+
applyItemFilter,
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
return <>{children(zappPipesDataProps)}</>;
|
|
266
|
+
}
|