@applicaster/zapp-react-native-utils 15.1.15 → 15.1.16-alpha.5293677506
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/actions/__tests__/openBottomSheet.inlineItems.test.ts +88 -0
- package/actionsExecutor/actions/openBottomSheet.ts +14 -5
- package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +15 -2
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +40 -32
- package/configurationUtils/modalBlocksParser.ts +14 -0
- package/modalState/ModalOrchestrator.ts +1 -0
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +16 -0
- package/modalState/types.ts +2 -0
- package/package.json +2 -2
|
@@ -77,6 +77,94 @@ describe("openBottomSheetAction inline items", () => {
|
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
it("opens an empty inline collection when dynamic collection options are present", async () => {
|
|
81
|
+
const result = await openBottomSheetAction({
|
|
82
|
+
type: "openBottomSheet",
|
|
83
|
+
options: {
|
|
84
|
+
header: { title: "Queue" },
|
|
85
|
+
content: {
|
|
86
|
+
items: [],
|
|
87
|
+
emptyMessage: "Queued items will be added here",
|
|
88
|
+
dynamicCollection: {
|
|
89
|
+
operations: ["remove"],
|
|
90
|
+
provider: "@{provider/queue}",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
} as any);
|
|
95
|
+
|
|
96
|
+
expect(result).toBe(ActionResult.Success);
|
|
97
|
+
|
|
98
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
99
|
+
|
|
100
|
+
expect(menu.content.items).toEqual([]);
|
|
101
|
+
expect(menu.content.emptyMessage).toBe("Queued items will be added here");
|
|
102
|
+
|
|
103
|
+
expect(menu.content.dynamicCollection).toMatchObject({
|
|
104
|
+
operations: ["remove"],
|
|
105
|
+
provider: "@{provider/queue}",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("accepts empty inline items when dynamic_collection_options live on extensions", async () => {
|
|
110
|
+
const result = await openBottomSheetAction({
|
|
111
|
+
type: "openBottomSheet",
|
|
112
|
+
options: {
|
|
113
|
+
content: {
|
|
114
|
+
items: [],
|
|
115
|
+
extensions: {
|
|
116
|
+
dynamic_collection_options: {
|
|
117
|
+
operations: "remove,reorder",
|
|
118
|
+
provider: "@{provider/offline}",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
} as any);
|
|
124
|
+
|
|
125
|
+
expect(result).toBe(ActionResult.Success);
|
|
126
|
+
|
|
127
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
128
|
+
|
|
129
|
+
expect(menu.content.items).toEqual([]);
|
|
130
|
+
|
|
131
|
+
expect(menu.content.dynamicCollection).toMatchObject({
|
|
132
|
+
operations: ["remove", "reorder"],
|
|
133
|
+
provider: "@{provider/offline}",
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns an error for an empty items array with no source", async () => {
|
|
138
|
+
const result = await openBottomSheetAction({
|
|
139
|
+
type: "openBottomSheet",
|
|
140
|
+
options: {
|
|
141
|
+
content: { items: [] },
|
|
142
|
+
},
|
|
143
|
+
} as any);
|
|
144
|
+
|
|
145
|
+
expect(result).toBe(ActionResult.Error);
|
|
146
|
+
expect(modalOrchestrator.open).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("forwards content.emptyMessage onto the menu", async () => {
|
|
150
|
+
const result = await openBottomSheetAction({
|
|
151
|
+
type: "openBottomSheet",
|
|
152
|
+
options: {
|
|
153
|
+
header: { title: "Queue" },
|
|
154
|
+
content: {
|
|
155
|
+
items: [{ id: "t1", title: "Track 1" }],
|
|
156
|
+
emptyMessage: "Queued items will be added here",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
} as any);
|
|
160
|
+
|
|
161
|
+
expect(result).toBe(ActionResult.Success);
|
|
162
|
+
|
|
163
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
164
|
+
|
|
165
|
+
expect(menu.content.emptyMessage).toBe("Queued items will be added here");
|
|
166
|
+
});
|
|
167
|
+
|
|
80
168
|
it("pushes a new sheet by default even when another sheet is already open", async () => {
|
|
81
169
|
(modalOrchestrator as any).stackSize = 1;
|
|
82
170
|
|
|
@@ -227,7 +227,18 @@ export async function openBottomSheetAction(
|
|
|
227
227
|
|
|
228
228
|
let dynamicCollection: DynamicCollectionOptions | undefined;
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
const rawDynamicCollection =
|
|
231
|
+
content?.dynamicCollection ??
|
|
232
|
+
content?.extensions?.dynamic_collection_options;
|
|
233
|
+
|
|
234
|
+
// An empty items array is a valid starting state for a provider-backed or
|
|
235
|
+
// offline collection. A sheet with neither items nor a source still errors.
|
|
236
|
+
const hasDynamicCollection = Boolean(rawDynamicCollection);
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
Array.isArray(inlineItems) &&
|
|
240
|
+
(inlineItems.length > 0 || hasDynamicCollection)
|
|
241
|
+
) {
|
|
231
242
|
log_info("openBottomSheet: using inline items from action options", {
|
|
232
243
|
count: inlineItems.length,
|
|
233
244
|
});
|
|
@@ -239,10 +250,7 @@ export async function openBottomSheetAction(
|
|
|
239
250
|
entryToMenuItem(entry, behavior?.currentSelection ?? [])
|
|
240
251
|
);
|
|
241
252
|
|
|
242
|
-
dynamicCollection = parseDynamicCollectionOptions(
|
|
243
|
-
content?.dynamicCollection ??
|
|
244
|
-
content?.extensions?.dynamic_collection_options
|
|
245
|
-
);
|
|
253
|
+
dynamicCollection = parseDynamicCollectionOptions(rawDynamicCollection);
|
|
246
254
|
} else if (itemsUrl) {
|
|
247
255
|
log_info(`openBottomSheet: loading items from source: ${itemsUrl}`);
|
|
248
256
|
|
|
@@ -280,6 +288,7 @@ export async function openBottomSheetAction(
|
|
|
280
288
|
dynamicCollection,
|
|
281
289
|
themePluginId,
|
|
282
290
|
styleOverrides: styleOverrides ?? content?.styleOverrides,
|
|
291
|
+
emptyMessage: content?.emptyMessage,
|
|
283
292
|
context,
|
|
284
293
|
},
|
|
285
294
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getUpcomingQueue } from "../getUpcomingQueue";
|
|
2
|
+
|
|
3
|
+
describe("getUpcomingQueue", () => {
|
|
4
|
+
it("returns null when plugin is not provided or has no module", async () => {
|
|
5
|
+
expect(await getUpcomingQueue(undefined)).toBeNull();
|
|
6
|
+
expect(await getUpcomingQueue({} as any)).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns null when QueueManager is not present on module", async () => {
|
|
10
|
+
const plugin = {
|
|
11
|
+
identifier: "queue-action",
|
|
12
|
+
module: {},
|
|
13
|
+
} as any;
|
|
14
|
+
|
|
15
|
+
expect(await getUpcomingQueue(plugin)).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("calls setupQuickBrickQueue when QueueManager is not initialized", async () => {
|
|
19
|
+
const setupQuickBrickQueue = jest.fn();
|
|
20
|
+
const mockUpcoming = [{ id: "item_1" }];
|
|
21
|
+
|
|
22
|
+
const QueueManager = {
|
|
23
|
+
isInitialized: jest
|
|
24
|
+
.fn()
|
|
25
|
+
.mockReturnValueOnce(false)
|
|
26
|
+
.mockReturnValueOnce(true),
|
|
27
|
+
instance: {
|
|
28
|
+
getUpcomingQueue: jest.fn().mockResolvedValue(mockUpcoming),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const plugin = {
|
|
33
|
+
identifier: "queue-action",
|
|
34
|
+
module: {
|
|
35
|
+
QueueManager,
|
|
36
|
+
setupQuickBrickQueue,
|
|
37
|
+
},
|
|
38
|
+
} as any;
|
|
39
|
+
|
|
40
|
+
const result = await getUpcomingQueue(plugin);
|
|
41
|
+
|
|
42
|
+
expect(setupQuickBrickQueue).toHaveBeenCalled();
|
|
43
|
+
expect(result).toEqual(mockUpcoming);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns upcoming items when QueueManager is already initialized", async () => {
|
|
47
|
+
const mockUpcoming = [{ id: "item_1" }, { id: "item_2" }];
|
|
48
|
+
|
|
49
|
+
const QueueManager = {
|
|
50
|
+
isInitialized: jest.fn().mockReturnValue(true),
|
|
51
|
+
instance: {
|
|
52
|
+
getUpcomingQueue: jest.fn().mockResolvedValue(mockUpcoming),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const plugin = {
|
|
57
|
+
identifier: "queue-action",
|
|
58
|
+
module: {
|
|
59
|
+
QueueManager,
|
|
60
|
+
},
|
|
61
|
+
} as any;
|
|
62
|
+
|
|
63
|
+
const result = await getUpcomingQueue(plugin);
|
|
64
|
+
|
|
65
|
+
expect(result).toEqual(mockUpcoming);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
export const getUpcomingQueue = async (
|
|
2
2
|
plugin?: QuickBrickPlugin | ZappPlugin
|
|
3
3
|
): Promise<unknown[] | null> => {
|
|
4
|
-
const
|
|
4
|
+
const module = (plugin as any)?.module;
|
|
5
|
+
const QueueManager = module?.QueueManager;
|
|
6
|
+
const setupQuickBrickQueue = module?.setupQuickBrickQueue;
|
|
5
7
|
|
|
6
|
-
if (!QueueManager
|
|
8
|
+
if (!QueueManager) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
!QueueManager.isInitialized?.() &&
|
|
14
|
+
typeof setupQuickBrickQueue === "function"
|
|
15
|
+
) {
|
|
16
|
+
setupQuickBrickQueue();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!QueueManager.isInitialized?.()) {
|
|
7
20
|
return null;
|
|
8
21
|
}
|
|
9
22
|
|
|
@@ -205,56 +205,64 @@ describe("parseModalBlocksConfiguration", () => {
|
|
|
205
205
|
);
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
-
it("should
|
|
208
|
+
it("should parse section_header title and action_buttons independently of header", () => {
|
|
209
209
|
const result = parseModalBlocksConfiguration(
|
|
210
210
|
{
|
|
211
|
-
"
|
|
212
|
-
|
|
213
|
-
"
|
|
214
|
-
|
|
211
|
+
"section_header.title.font_color": "#ABCDEF",
|
|
212
|
+
"section_header.title.font_size": 13,
|
|
213
|
+
"section_header.title.ios.font_family": "SFProText-Bold",
|
|
214
|
+
"section_header.action_buttons.default.background_color":
|
|
215
|
+
"rgba(62, 62, 62, 1)",
|
|
216
|
+
"section_header.action_buttons.focus.background_color":
|
|
217
|
+
"rgba(78, 78, 78, 1)",
|
|
218
|
+
"section_header.action_buttons.default.font_color": "#FFFFFF",
|
|
219
|
+
"section_header.action_buttons.font_size": 14,
|
|
220
|
+
"section_header.action_buttons.border_radius": 16,
|
|
221
|
+
// Theme modal header — must not leak into sectionHeader
|
|
222
|
+
queue_action_style_header_title_default_fontColor: "#111111",
|
|
223
|
+
queue_action_style_header_title_default_fontSize: 22,
|
|
215
224
|
},
|
|
216
|
-
"
|
|
225
|
+
"queue_action"
|
|
217
226
|
);
|
|
218
227
|
|
|
219
|
-
expect(result.
|
|
228
|
+
expect(result.sectionHeader.title.default).toEqual(
|
|
220
229
|
expect.objectContaining({
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
fontColor: "#ABCDEF",
|
|
231
|
+
fontSize: 13,
|
|
232
|
+
fontFamily: "SFProText-Bold",
|
|
223
233
|
})
|
|
224
234
|
);
|
|
225
|
-
});
|
|
226
235
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
},
|
|
235
|
-
"playlist-experience"
|
|
236
|
+
expect(result.sectionHeader.actionButtons.default).toEqual(
|
|
237
|
+
expect.objectContaining({
|
|
238
|
+
backgroundColor: "rgba(62, 62, 62, 1)",
|
|
239
|
+
fontColor: "#FFFFFF",
|
|
240
|
+
fontSize: 14,
|
|
241
|
+
borderRadius: 16,
|
|
242
|
+
})
|
|
236
243
|
);
|
|
237
244
|
|
|
238
|
-
expect(result.
|
|
245
|
+
expect(result.sectionHeader.actionButtons.focused).toEqual(
|
|
239
246
|
expect.objectContaining({
|
|
240
|
-
|
|
241
|
-
|
|
247
|
+
backgroundColor: "rgba(78, 78, 78, 1)",
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(result.header.title.default).toEqual(
|
|
252
|
+
expect.objectContaining({
|
|
253
|
+
fontColor: "#111111",
|
|
254
|
+
fontSize: 22,
|
|
242
255
|
})
|
|
243
256
|
);
|
|
244
257
|
});
|
|
245
258
|
|
|
246
|
-
it("should
|
|
259
|
+
it("should leave sectionHeader empty when keys are absent", () => {
|
|
247
260
|
const result = parseModalBlocksConfiguration(
|
|
248
|
-
{
|
|
249
|
-
|
|
250
|
-
"https://placehold.co/24x24/FF00FF/FFFFFF/png?text=CN",
|
|
251
|
-
"playlist_experience_assets_flavor1.reorder_handle_asset":
|
|
252
|
-
"https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
|
|
253
|
-
},
|
|
254
|
-
"queue-action"
|
|
261
|
+
{ "item.text_label1.font_color": "#FFFFFF" },
|
|
262
|
+
"playlist-experience"
|
|
255
263
|
);
|
|
256
264
|
|
|
257
|
-
expect(result.
|
|
258
|
-
expect(result.
|
|
265
|
+
expect(result.sectionHeader.title).toEqual({ default: {} });
|
|
266
|
+
expect(result.sectionHeader.actionButtons).toEqual({ default: {} });
|
|
259
267
|
});
|
|
260
268
|
});
|
|
@@ -7,6 +7,14 @@ export type ModalBlocksConfig = {
|
|
|
7
7
|
closeButton: Record<string, any>;
|
|
8
8
|
actionButtons: Record<string, any>;
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Queue Section Header (BB Variant 3) — shared by Next in Queue / Next Up.
|
|
12
|
+
* Separate from Theme modal `header` so top-title styles do not collide.
|
|
13
|
+
*/
|
|
14
|
+
sectionHeader: {
|
|
15
|
+
title: Record<string, any>;
|
|
16
|
+
actionButtons: Record<string, any>;
|
|
17
|
+
};
|
|
10
18
|
action: {
|
|
11
19
|
background: Record<string, any>;
|
|
12
20
|
title: Record<string, any>;
|
|
@@ -63,6 +71,10 @@ export const parseModalBlocksConfiguration = (
|
|
|
63
71
|
closeButton: {},
|
|
64
72
|
actionButtons: {},
|
|
65
73
|
},
|
|
74
|
+
sectionHeader: {
|
|
75
|
+
title: {},
|
|
76
|
+
actionButtons: {},
|
|
77
|
+
},
|
|
66
78
|
action: {
|
|
67
79
|
background: {},
|
|
68
80
|
title: {},
|
|
@@ -148,6 +160,8 @@ export const parseModalBlocksConfiguration = (
|
|
|
148
160
|
"action_buttons",
|
|
149
161
|
]);
|
|
150
162
|
|
|
163
|
+
parseBlock("section_header", ["title", "action_buttons"]);
|
|
164
|
+
|
|
151
165
|
parseBlock("action", [
|
|
152
166
|
"background",
|
|
153
167
|
"title",
|
|
@@ -44,4 +44,20 @@ describe("ModalOrchestrator phase 3", () => {
|
|
|
44
44
|
expect(props.title).toBe("Sleep Timer");
|
|
45
45
|
expect(props.summary).toBe("remaining");
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
it("forwards the queue empty message onto the sheet props", () => {
|
|
49
|
+
modalOrchestrator.open({
|
|
50
|
+
header: { title: "Queue" },
|
|
51
|
+
content: {
|
|
52
|
+
title: "Queue",
|
|
53
|
+
items: [],
|
|
54
|
+
emptyMessage: "Queued items will be added here",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const props = (modalStore.getState().modalState.screen as any)
|
|
59
|
+
?.modalBottomSheetContentProps;
|
|
60
|
+
|
|
61
|
+
expect(props.emptyMessage).toBe("Queued items will be added here");
|
|
62
|
+
});
|
|
47
63
|
});
|
package/modalState/types.ts
CHANGED
|
@@ -118,6 +118,8 @@ export interface Content {
|
|
|
118
118
|
dynamicCollection?: DynamicCollectionOptions;
|
|
119
119
|
themePluginId?: string;
|
|
120
120
|
styleOverrides?: Record<string, any>;
|
|
121
|
+
/** Shown under Next Up when the queue sheet has no upcoming items. */
|
|
122
|
+
emptyMessage?: string;
|
|
121
123
|
context?: Record<string, any>;
|
|
122
124
|
}
|
|
123
125
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.16-alpha.5293677506",
|
|
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": "15.1.
|
|
30
|
+
"@applicaster/applicaster-types": "15.1.16-alpha.5293677506",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|