@applicaster/zapp-react-native-utils 16.0.0-alpha.1593888345 → 16.0.0-alpha.1701179921
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/ActionExecutor.ts +10 -1
- package/actionsExecutor/ActionExecutorContext.tsx +3 -0
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +88 -0
- package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
- package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
- package/actionsExecutor/actions/index.ts +2 -0
- package/actionsExecutor/actions/openBottomSheet.ts +19 -6
- package/actionsExecutor/actions/showAlert.ts +51 -0
- package/actionsExecutor/actions/showToast.ts +2 -2
- package/actionsExecutor/consts.ts +1 -0
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +4 -11
- package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
- package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +9 -2
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +15 -2
- package/appUtils/playerManager/OverlayObserver/utils.ts +2 -5
- package/appUtils/playerManager/__tests__/playerNative.test.ts +76 -0
- package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
- package/appUtils/playerManager/const.ts +13 -0
- package/appUtils/playerManager/player.ts +10 -0
- package/appUtils/playerManager/playerNative.ts +22 -0
- package/appUtils/playerManager/playerNativeCommand.ts +3 -6
- package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
- package/arrayUtils/index.ts +26 -43
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +78 -69
- package/configurationUtils/modalBlocksParser.ts +24 -106
- package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
- package/entryActions/aliasPresentation.ts +26 -42
- package/entryActions/buildEntryActions.ts +1 -0
- package/entryActions/index.ts +0 -2
- package/modalState/ModalOrchestrator.ts +1 -0
- package/modalState/RemoteEditableCollection.ts +9 -1
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +16 -0
- package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
- package/modalState/__tests__/useModalStoreState.test.ts +142 -0
- package/modalState/store.ts +9 -2
- package/modalState/types.ts +3 -0
- package/modalState/useBottomSheetContent.ts +17 -12
- package/package.json +2 -2
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/reactHooks/cell-click/__tests__/index.test.js +64 -0
- package/reactHooks/cell-click/index.ts +21 -14
- package/reactHooks/navigation/__tests__/index.test.tsx +128 -1
- package/reactHooks/navigation/useRoute.ts +8 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { NativeModules } from "react-native";
|
|
2
|
+
import {
|
|
3
|
+
PlayerNativeCommandTypes,
|
|
4
|
+
PlayerNativeSendCommand,
|
|
5
|
+
} from "../playerNativeCommand";
|
|
6
|
+
|
|
7
|
+
describe("PlayerNativeSendCommand", () => {
|
|
8
|
+
const reactUtils = require("../../../reactUtils");
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("calls NativeModules.PlayerModule.command on Apple platform with playerId", () => {
|
|
15
|
+
jest.spyOn(reactUtils, "isApplePlatform").mockReturnValue(true);
|
|
16
|
+
|
|
17
|
+
NativeModules.PlayerModule = {
|
|
18
|
+
command: jest.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
PlayerNativeSendCommand(
|
|
22
|
+
PlayerNativeCommandTypes.setPlayerControls,
|
|
23
|
+
"player-123",
|
|
24
|
+
{ transport: ["play"] }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(NativeModules.PlayerModule.command).toHaveBeenCalledWith(
|
|
28
|
+
"setPlayerControls",
|
|
29
|
+
"player-123",
|
|
30
|
+
{ transport: ["play"] }
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not call NativeModules.PlayerModule.command on non-Apple platform", () => {
|
|
35
|
+
jest.spyOn(reactUtils, "isApplePlatform").mockReturnValue(false);
|
|
36
|
+
|
|
37
|
+
NativeModules.PlayerModule = {
|
|
38
|
+
command: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
PlayerNativeSendCommand(
|
|
42
|
+
PlayerNativeCommandTypes.setPlayerControls,
|
|
43
|
+
"player-123",
|
|
44
|
+
{ transport: ["play"] }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(NativeModules.PlayerModule.command).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does nothing when playerId or command is missing", () => {
|
|
51
|
+
jest.spyOn(reactUtils, "isApplePlatform").mockReturnValue(true);
|
|
52
|
+
|
|
53
|
+
NativeModules.PlayerModule = {
|
|
54
|
+
command: jest.fn(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
PlayerNativeSendCommand("", "player-123");
|
|
58
|
+
PlayerNativeSendCommand("setPlayerControls", "");
|
|
59
|
+
|
|
60
|
+
expect(NativeModules.PlayerModule.command).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -59,6 +59,19 @@ export enum SharedPlayerCallBacksKeys {
|
|
|
59
59
|
// PIP
|
|
60
60
|
OnPlayerDetached = "onPlayerDetached",
|
|
61
61
|
OnPlayerAttached = "onPlayerAttached",
|
|
62
|
+
|
|
63
|
+
OnRemoteCommand = "onRemoteCommand",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Ids native sends in onRemoteCommand (lock screen / Control Center / CarPlay buttons)
|
|
67
|
+
export enum RemoteCommand {
|
|
68
|
+
Play = "play",
|
|
69
|
+
Pause = "pause",
|
|
70
|
+
Stop = "stop",
|
|
71
|
+
NextTrack = "nextTrack",
|
|
72
|
+
PreviousTrack = "previousTrack",
|
|
73
|
+
SkipForward = "skipForward",
|
|
74
|
+
SkipBackward = "skipBackward",
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
export enum PlayerRole {
|
|
@@ -746,6 +746,13 @@ export class Player {
|
|
|
746
746
|
});
|
|
747
747
|
};
|
|
748
748
|
|
|
749
|
+
onRemoteCommand = (event: QuickBrickPlayer.RemoteCommandEvent) => {
|
|
750
|
+
this.invokeListenersUpdate({
|
|
751
|
+
callbackName: SharedPlayerCallBacksKeys.OnRemoteCommand,
|
|
752
|
+
event,
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
|
|
749
756
|
getTracksState = (): QuickBrickPlayer.TracksState =>
|
|
750
757
|
this.playerState.trackState;
|
|
751
758
|
|
|
@@ -823,6 +830,8 @@ export class Player {
|
|
|
823
830
|
// PIP
|
|
824
831
|
onPlayerDetached: this.onPlayerDetached,
|
|
825
832
|
onPlayerAttached: this.onPlayerAttached,
|
|
833
|
+
|
|
834
|
+
onRemoteCommand: this.onRemoteCommand,
|
|
826
835
|
});
|
|
827
836
|
|
|
828
837
|
play = () => {};
|
|
@@ -832,6 +841,7 @@ export class Player {
|
|
|
832
841
|
setPlaybackRate = (_rate) => {};
|
|
833
842
|
startSleepTimer = (_timestamp) => {};
|
|
834
843
|
cancelSleepTimer = () => {};
|
|
844
|
+
command = (_command: string, _userInfo?: Record<string, any>) => {};
|
|
835
845
|
|
|
836
846
|
getPluginConfiguration = () => null;
|
|
837
847
|
appStateChange = (_appState, _previousAppState) => {};
|
|
@@ -6,6 +6,7 @@ import { findNodeHandle } from "react-native";
|
|
|
6
6
|
import { PlayerRole } from "./const";
|
|
7
7
|
import { toBooleanWithDefaultFalse } from "../../booleanUtils";
|
|
8
8
|
import { RNPlayerManager } from "./nativePlayerManager";
|
|
9
|
+
import { isApplePlatform } from "../../reactUtils";
|
|
9
10
|
|
|
10
11
|
const logger = createLogger({
|
|
11
12
|
category: "PlayerNativeController",
|
|
@@ -27,6 +28,7 @@ interface PlayerMethods {
|
|
|
27
28
|
appStateChange?: (appState: string, previousAppState: string) => void;
|
|
28
29
|
closeNativePlayer?: () => void;
|
|
29
30
|
togglePlayPause?: () => void;
|
|
31
|
+
command?: (command: string, userInfo?: Record<string, any>) => void;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
type PlayerComponent = ReactComponent<PlayerRefProps> & PlayerMethods;
|
|
@@ -52,6 +54,7 @@ const PlayerModuleFuncNames = {
|
|
|
52
54
|
startSleepTimer: "startSleepTimer",
|
|
53
55
|
cancelSleepTimer: "cancelSleepTimer",
|
|
54
56
|
getSleepTimerEnd: "getSleepTimerEnd",
|
|
57
|
+
command: "command",
|
|
55
58
|
};
|
|
56
59
|
|
|
57
60
|
export class PlayerNative extends Player {
|
|
@@ -206,6 +209,25 @@ export class PlayerNative extends Player {
|
|
|
206
209
|
}
|
|
207
210
|
};
|
|
208
211
|
|
|
212
|
+
command = (command: string, userInfo?: Record<string, any>) => {
|
|
213
|
+
if (isApplePlatform()) {
|
|
214
|
+
const module = this.getPlayerModule();
|
|
215
|
+
module?.command?.(command, this.playerId, userInfo);
|
|
216
|
+
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (
|
|
221
|
+
!this.invokeNativeFunction(
|
|
222
|
+
PlayerModuleFuncNames.command,
|
|
223
|
+
command,
|
|
224
|
+
userInfo
|
|
225
|
+
)
|
|
226
|
+
) {
|
|
227
|
+
this.currentPlayerComponent()?.command?.(command, userInfo);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
209
231
|
disableBufferAnimation = (): boolean => false;
|
|
210
232
|
|
|
211
233
|
selectTrack = (
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { isApplePlatform } from "../../reactUtils";
|
|
2
2
|
import { NativeModules } from "react-native";
|
|
3
3
|
|
|
4
|
-
const NativePlayerModule = isApplePlatform()
|
|
5
|
-
? NativeModules.PlayerModule
|
|
6
|
-
: NativeModules.QuickBrickDefaultPlayerModule;
|
|
7
|
-
|
|
8
4
|
const PlayerNativeCommandTypes = {
|
|
9
5
|
pause: "pause",
|
|
10
6
|
clearPlayerData: "clearPlayerData",
|
|
11
7
|
removeViewByNativeId: "removeViewByNativeId",
|
|
8
|
+
setPlayerControls: "setPlayerControls",
|
|
12
9
|
};
|
|
13
10
|
|
|
14
11
|
const PlayerNativeSendCommand = (
|
|
@@ -16,8 +13,8 @@ const PlayerNativeSendCommand = (
|
|
|
16
13
|
playerId: string,
|
|
17
14
|
userInfo?: Record<string, any>
|
|
18
15
|
) => {
|
|
19
|
-
if (playerId && command) {
|
|
20
|
-
|
|
16
|
+
if (isApplePlatform() && playerId && command) {
|
|
17
|
+
NativeModules.PlayerModule?.command?.(command, playerId, userInfo);
|
|
21
18
|
}
|
|
22
19
|
};
|
|
23
20
|
|
|
@@ -47,4 +47,17 @@ describe("reorderByIds", () => {
|
|
|
47
47
|
|
|
48
48
|
expect(result.map((i) => i.key)).toEqual(["c", "b", "a"]);
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
it("reorders duplicate items when using unique slot identifiers via getId", () => {
|
|
52
|
+
const duplicateItems = [
|
|
53
|
+
{ id: "x", slot: "s1", name: "First X" },
|
|
54
|
+
{ id: "y", slot: "s2", name: "Y" },
|
|
55
|
+
{ id: "x", slot: "s3", name: "Second X" },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const result = reorderByIds(duplicateItems, ["s3", "s1"], (i) => i.slot);
|
|
59
|
+
|
|
60
|
+
expect(result).toHaveLength(3);
|
|
61
|
+
expect(result.map((i) => i.name)).toEqual(["Second X", "Y", "First X"]);
|
|
62
|
+
});
|
|
50
63
|
});
|
package/arrayUtils/index.ts
CHANGED
|
@@ -167,7 +167,7 @@ export const anyTruthy = (xs: boolean[]) => xs.some(Boolean);
|
|
|
167
167
|
export function reorderByIds<T>(
|
|
168
168
|
items: T[],
|
|
169
169
|
orderedIds: string[],
|
|
170
|
-
getId: (item: T) => string
|
|
170
|
+
getId: (item: T) => string = (item: any) => item?.id
|
|
171
171
|
): T[] {
|
|
172
172
|
if (!items || items.length === 0) return [];
|
|
173
173
|
if (!orderedIds || orderedIds.length === 0) return [...items];
|
|
@@ -175,14 +175,16 @@ export function reorderByIds<T>(
|
|
|
175
175
|
const itemMap = new Map<string, T>();
|
|
176
176
|
|
|
177
177
|
items.forEach((item) => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
const id = getId(item);
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
id !== undefined &&
|
|
182
|
+
id !== null &&
|
|
183
|
+
id !== "" &&
|
|
184
|
+
!itemMap.has(String(id))
|
|
185
|
+
) {
|
|
186
|
+
itemMap.set(String(id), item);
|
|
187
|
+
}
|
|
186
188
|
});
|
|
187
189
|
|
|
188
190
|
const reorderedSubset: T[] = [];
|
|
@@ -192,48 +194,29 @@ export function reorderByIds<T>(
|
|
|
192
194
|
const strId = String(id);
|
|
193
195
|
const item = itemMap.get(strId);
|
|
194
196
|
|
|
195
|
-
if (item && !
|
|
197
|
+
if (item && !orderedIdsSet.has(strId)) {
|
|
196
198
|
reorderedSubset.push(item);
|
|
197
|
-
|
|
198
|
-
const keys = Array.isArray(rawId) ? rawId : [rawId];
|
|
199
|
-
|
|
200
|
-
keys.forEach((k) => {
|
|
201
|
-
if (k !== undefined && k !== null && k !== "") {
|
|
202
|
-
orderedIdsSet.add(String(k));
|
|
203
|
-
}
|
|
204
|
-
});
|
|
199
|
+
orderedIdsSet.add(strId);
|
|
205
200
|
}
|
|
206
201
|
});
|
|
207
202
|
|
|
208
203
|
if (reorderedSubset.length === 0) return [...items];
|
|
209
204
|
|
|
210
205
|
let subsetIdx = 0;
|
|
211
|
-
const result: T[] = [];
|
|
212
|
-
|
|
213
|
-
items.forEach((item) => {
|
|
214
|
-
const rawId = getId(item);
|
|
215
|
-
const keys = Array.isArray(rawId) ? rawId : [rawId];
|
|
216
|
-
|
|
217
|
-
const isMatched = keys.some(
|
|
218
|
-
(k) =>
|
|
219
|
-
k !== undefined &&
|
|
220
|
-
k !== null &&
|
|
221
|
-
k !== "" &&
|
|
222
|
-
orderedIdsSet.has(String(k))
|
|
223
|
-
);
|
|
224
206
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
207
|
+
return items.map((item) => {
|
|
208
|
+
const id = getId(item);
|
|
209
|
+
|
|
210
|
+
if (
|
|
211
|
+
id !== undefined &&
|
|
212
|
+
id !== null &&
|
|
213
|
+
id !== "" &&
|
|
214
|
+
orderedIdsSet.has(String(id)) &&
|
|
215
|
+
subsetIdx < reorderedSubset.length
|
|
216
|
+
) {
|
|
217
|
+
return reorderedSubset[subsetIdx++];
|
|
231
218
|
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
while (subsetIdx < reorderedSubset.length) {
|
|
235
|
-
result.push(reorderedSubset[subsetIdx++]);
|
|
236
|
-
}
|
|
237
219
|
|
|
238
|
-
|
|
220
|
+
return item;
|
|
221
|
+
});
|
|
239
222
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getEmptyModalBlocksStructure,
|
|
3
|
+
parseModalBlocksConfiguration,
|
|
4
|
+
} from "../modalBlocksParser";
|
|
2
5
|
|
|
3
6
|
// Mock the platform selection logic so it's consistent
|
|
4
7
|
jest.mock("../../reactUtils", () => ({
|
|
@@ -151,110 +154,116 @@ describe("parseModalBlocksConfiguration", () => {
|
|
|
151
154
|
);
|
|
152
155
|
});
|
|
153
156
|
|
|
154
|
-
it("should map
|
|
157
|
+
it("should ignore keys that do not map to a known block sub-component", () => {
|
|
155
158
|
const result = parseModalBlocksConfiguration(
|
|
156
159
|
{
|
|
157
160
|
"button.create_new_playlist_asset":
|
|
158
161
|
"https://placehold.co/24x24/FF00FF/FFFFFF/png?text=CN",
|
|
162
|
+
"item.checkbox_asset":
|
|
163
|
+
"https://placehold.co/24x24/FF3B30/FFFFFF/png?text=CB",
|
|
164
|
+
"reorder_playlist_item.reorder_handle_asset":
|
|
165
|
+
"https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
|
|
159
166
|
},
|
|
160
167
|
"playlist-experience"
|
|
161
168
|
);
|
|
162
169
|
|
|
163
|
-
expect(result.button.leadingIcon.default).
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
})
|
|
167
|
-
);
|
|
170
|
+
expect(result.button.leadingIcon.default?.asset).toBeUndefined();
|
|
171
|
+
expect(result.item.checkbox.default).toBeUndefined();
|
|
172
|
+
expect(result.item.actionButtons.default).toBeUndefined();
|
|
168
173
|
});
|
|
174
|
+
});
|
|
169
175
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"item.checkbox_asset":
|
|
174
|
-
"https://placehold.co/24x24/FF3B30/FFFFFF/png?text=CB",
|
|
175
|
-
"item.checked_checkbox_asset":
|
|
176
|
-
"https://placehold.co/24x24/FFD60A/000000/png?text=CK",
|
|
177
|
-
},
|
|
178
|
-
"playlist-experience"
|
|
179
|
-
);
|
|
176
|
+
describe("getEmptyModalBlocksStructure", () => {
|
|
177
|
+
it("should return every block with empty containers and no state entries", () => {
|
|
178
|
+
const result = getEmptyModalBlocksStructure();
|
|
180
179
|
|
|
181
|
-
expect(result.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
expect(result.header.container).toEqual({});
|
|
181
|
+
expect(result.item.background).toEqual({});
|
|
182
|
+
expect(result.button.background).toEqual({});
|
|
183
|
+
expect(result.action.background).toEqual({});
|
|
184
|
+
|
|
185
|
+
expect(result.button.leadingIcon.default).toBeUndefined();
|
|
186
|
+
expect(result.item.checkbox.default).toBeUndefined();
|
|
187
|
+
expect(result.item.actionButtons.default).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should return a fresh object on every call", () => {
|
|
191
|
+
const first = getEmptyModalBlocksStructure();
|
|
192
|
+
const second = getEmptyModalBlocksStructure();
|
|
193
|
+
|
|
194
|
+
expect(first).not.toBe(second);
|
|
195
|
+
expect(first.header).not.toBe(second.header);
|
|
196
|
+
expect(first).toEqual(second);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should be what the parser returns when config or componentName is missing", () => {
|
|
200
|
+
expect(
|
|
201
|
+
parseModalBlocksConfiguration(undefined as any, "queue_action")
|
|
202
|
+
).toEqual(getEmptyModalBlocksStructure());
|
|
203
|
+
|
|
204
|
+
expect(parseModalBlocksConfiguration({ foo: "bar" }, "")).toEqual(
|
|
205
|
+
getEmptyModalBlocksStructure()
|
|
186
206
|
);
|
|
187
207
|
});
|
|
188
208
|
|
|
189
|
-
it("should
|
|
209
|
+
it("should parse section_header title and action_buttons independently of header", () => {
|
|
190
210
|
const result = parseModalBlocksConfiguration(
|
|
191
211
|
{
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
"
|
|
195
|
-
|
|
212
|
+
"section_header.title.font_color": "#ABCDEF",
|
|
213
|
+
"section_header.title.font_size": 13,
|
|
214
|
+
"section_header.title.ios.font_family": "SFProText-Bold",
|
|
215
|
+
"section_header.action_buttons.default.background_color":
|
|
216
|
+
"rgba(62, 62, 62, 1)",
|
|
217
|
+
"section_header.action_buttons.focus.background_color":
|
|
218
|
+
"rgba(78, 78, 78, 1)",
|
|
219
|
+
"section_header.action_buttons.default.font_color": "#FFFFFF",
|
|
220
|
+
"section_header.action_buttons.font_size": 14,
|
|
221
|
+
"section_header.action_buttons.border_radius": 16,
|
|
222
|
+
// Theme modal header — must not leak into sectionHeader
|
|
223
|
+
queue_action_style_header_title_default_fontColor: "#111111",
|
|
224
|
+
queue_action_style_header_title_default_fontSize: 22,
|
|
196
225
|
},
|
|
197
|
-
"
|
|
226
|
+
"queue_action"
|
|
198
227
|
);
|
|
199
228
|
|
|
200
|
-
expect(result.
|
|
229
|
+
expect(result.sectionHeader.title.default).toEqual(
|
|
201
230
|
expect.objectContaining({
|
|
202
|
-
|
|
203
|
-
|
|
231
|
+
fontColor: "#ABCDEF",
|
|
232
|
+
fontSize: 13,
|
|
233
|
+
fontFamily: "SFProText-Bold",
|
|
204
234
|
})
|
|
205
235
|
);
|
|
206
|
-
});
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
const result = parseModalBlocksConfiguration(
|
|
210
|
-
{
|
|
211
|
-
"playlist_experience_assets_flavor1.remove_from_playlist_asset":
|
|
212
|
-
"https://placehold.co/24x24/00FF00/000000/png?text=RM",
|
|
213
|
-
"playlist_experience_assets_flavor1.reorder_handle_asset":
|
|
214
|
-
"https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
|
|
215
|
-
},
|
|
216
|
-
"playlist-experience"
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
expect(result.item.actionButtons.default).toEqual(
|
|
237
|
+
expect(result.sectionHeader.actionButtons.default).toEqual(
|
|
220
238
|
expect.objectContaining({
|
|
221
|
-
|
|
222
|
-
|
|
239
|
+
backgroundColor: "rgba(62, 62, 62, 1)",
|
|
240
|
+
fontColor: "#FFFFFF",
|
|
241
|
+
fontSize: 14,
|
|
242
|
+
borderRadius: 16,
|
|
223
243
|
})
|
|
224
244
|
);
|
|
225
|
-
});
|
|
226
245
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
"https://placehold.co/24x24/00FF00/000000/png?text=RM",
|
|
232
|
-
"playlist_experience_assets_flavor2.reorder_handle_asset":
|
|
233
|
-
"https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
|
|
234
|
-
},
|
|
235
|
-
"playlist-experience"
|
|
246
|
+
expect(result.sectionHeader.actionButtons.focused).toEqual(
|
|
247
|
+
expect.objectContaining({
|
|
248
|
+
backgroundColor: "rgba(78, 78, 78, 1)",
|
|
249
|
+
})
|
|
236
250
|
);
|
|
237
251
|
|
|
238
|
-
expect(result.
|
|
252
|
+
expect(result.header.title.default).toEqual(
|
|
239
253
|
expect.objectContaining({
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
fontColor: "#111111",
|
|
255
|
+
fontSize: 22,
|
|
242
256
|
})
|
|
243
257
|
);
|
|
244
258
|
});
|
|
245
259
|
|
|
246
|
-
it("should
|
|
260
|
+
it("should leave sectionHeader empty when keys are absent", () => {
|
|
247
261
|
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"
|
|
262
|
+
{ "item.text_label1.font_color": "#FFFFFF" },
|
|
263
|
+
"playlist-experience"
|
|
255
264
|
);
|
|
256
265
|
|
|
257
|
-
expect(result.
|
|
258
|
-
expect(result.
|
|
266
|
+
expect(result.sectionHeader.title).toEqual({ default: {} });
|
|
267
|
+
expect(result.sectionHeader.actionButtons).toEqual({ default: {} });
|
|
259
268
|
});
|
|
260
269
|
});
|
|
@@ -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>;
|
|
@@ -52,10 +60,7 @@ export type ModalBlocksConfig = {
|
|
|
52
60
|
};
|
|
53
61
|
};
|
|
54
62
|
|
|
55
|
-
export const
|
|
56
|
-
configuration: Record<string, any>,
|
|
57
|
-
componentName: string
|
|
58
|
-
): ModalBlocksConfig => {
|
|
63
|
+
export const getEmptyModalBlocksStructure = (): ModalBlocksConfig => {
|
|
59
64
|
const result: ModalBlocksConfig = {
|
|
60
65
|
header: {
|
|
61
66
|
container: {},
|
|
@@ -63,6 +68,10 @@ export const parseModalBlocksConfiguration = (
|
|
|
63
68
|
closeButton: {},
|
|
64
69
|
actionButtons: {},
|
|
65
70
|
},
|
|
71
|
+
sectionHeader: {
|
|
72
|
+
title: {},
|
|
73
|
+
actionButtons: {},
|
|
74
|
+
},
|
|
66
75
|
action: {
|
|
67
76
|
background: {},
|
|
68
77
|
title: {},
|
|
@@ -106,6 +115,15 @@ export const parseModalBlocksConfiguration = (
|
|
|
106
115
|
},
|
|
107
116
|
};
|
|
108
117
|
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const parseModalBlocksConfiguration = (
|
|
122
|
+
configuration: Record<string, any>,
|
|
123
|
+
componentName: string
|
|
124
|
+
): ModalBlocksConfig => {
|
|
125
|
+
const result = getEmptyModalBlocksStructure();
|
|
126
|
+
|
|
109
127
|
if (!configuration || !componentName) {
|
|
110
128
|
return result;
|
|
111
129
|
}
|
|
@@ -148,6 +166,8 @@ export const parseModalBlocksConfiguration = (
|
|
|
148
166
|
"action_buttons",
|
|
149
167
|
]);
|
|
150
168
|
|
|
169
|
+
parseBlock("section_header", ["title", "action_buttons"]);
|
|
170
|
+
|
|
151
171
|
parseBlock("action", [
|
|
152
172
|
"background",
|
|
153
173
|
"title",
|
|
@@ -190,107 +210,5 @@ export const parseModalBlocksConfiguration = (
|
|
|
190
210
|
|
|
191
211
|
parseBlock("button", ["background", "title", "icons", "leading_icon"]);
|
|
192
212
|
|
|
193
|
-
// Playlist-experience Studio keys live in this shared parser because modal
|
|
194
|
-
// blocks are themed by themePluginId. Gate them so other plugins are not
|
|
195
|
-
// affected by playlist-only asset names.
|
|
196
|
-
const isPlaylistExperience =
|
|
197
|
-
normalizedComponentName === "playlist_experience";
|
|
198
|
-
|
|
199
|
-
// Studio uploads Create New icon as button.create_new_playlist_asset (not
|
|
200
|
-
// button.leading_icon.asset). Map it onto leadingIcon so AudioPlayerButton
|
|
201
|
-
// can override the hardcoded action-type asset.
|
|
202
|
-
const createNewPlaylistAsset = isPlaylistExperience
|
|
203
|
-
? configuration["button.create_new_playlist_asset"] ||
|
|
204
|
-
configuration[
|
|
205
|
-
`${normalizedComponentName}_button_create_new_playlist_asset`
|
|
206
|
-
]
|
|
207
|
-
: undefined;
|
|
208
|
-
|
|
209
|
-
if (createNewPlaylistAsset) {
|
|
210
|
-
if (!result.button.leadingIcon.default) {
|
|
211
|
-
result.button.leadingIcon.default = {};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
result.button.leadingIcon.default.asset = createNewPlaylistAsset;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Studio checkbox assets are top-level item.* keys (not a parseBlock
|
|
218
|
-
// sub-component). Map onto item.checkbox so multiSelect can render them.
|
|
219
|
-
const checkboxAsset =
|
|
220
|
-
configuration["item.checkbox_asset"] ||
|
|
221
|
-
configuration[`${normalizedComponentName}_item_checkbox_asset`];
|
|
222
|
-
|
|
223
|
-
const checkedCheckboxAsset =
|
|
224
|
-
configuration["item.checked_checkbox_asset"] ||
|
|
225
|
-
configuration[`${normalizedComponentName}_item_checked_checkbox_asset`];
|
|
226
|
-
|
|
227
|
-
if (checkboxAsset || checkedCheckboxAsset) {
|
|
228
|
-
if (!result.item.checkbox.default) {
|
|
229
|
-
result.item.checkbox.default = {};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (checkboxAsset) {
|
|
233
|
-
result.item.checkbox.default.asset = checkboxAsset;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (checkedCheckboxAsset) {
|
|
237
|
-
result.item.checkbox.default.checkedAsset = checkedCheckboxAsset;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Edit Order row icons: prefer reorder_playlist_item.*, then flavor1, then
|
|
242
|
-
// flavor2 playlist_experience_assets_* (playlist-experience theme only).
|
|
243
|
-
const removeAsset = isPlaylistExperience
|
|
244
|
-
? configuration["reorder_playlist_item.remove_from_playlist_asset"] ||
|
|
245
|
-
configuration[
|
|
246
|
-
`${normalizedComponentName}_reorder_playlist_item_remove_from_playlist_asset`
|
|
247
|
-
] ||
|
|
248
|
-
configuration[
|
|
249
|
-
"playlist_experience_assets_flavor1.remove_from_playlist_asset"
|
|
250
|
-
] ||
|
|
251
|
-
configuration[
|
|
252
|
-
`${normalizedComponentName}_assets_flavor1_remove_from_playlist_asset`
|
|
253
|
-
] ||
|
|
254
|
-
configuration[
|
|
255
|
-
"playlist_experience_assets_flavor2.remove_from_playlist_asset"
|
|
256
|
-
] ||
|
|
257
|
-
configuration[
|
|
258
|
-
`${normalizedComponentName}_assets_flavor2_remove_from_playlist_asset`
|
|
259
|
-
]
|
|
260
|
-
: undefined;
|
|
261
|
-
|
|
262
|
-
const dragAsset = isPlaylistExperience
|
|
263
|
-
? configuration["reorder_playlist_item.reorder_handle_asset"] ||
|
|
264
|
-
configuration[
|
|
265
|
-
`${normalizedComponentName}_reorder_playlist_item_reorder_handle_asset`
|
|
266
|
-
] ||
|
|
267
|
-
configuration[
|
|
268
|
-
"playlist_experience_assets_flavor1.reorder_handle_asset"
|
|
269
|
-
] ||
|
|
270
|
-
configuration[
|
|
271
|
-
`${normalizedComponentName}_assets_flavor1_reorder_handle_asset`
|
|
272
|
-
] ||
|
|
273
|
-
configuration[
|
|
274
|
-
"playlist_experience_assets_flavor2.reorder_handle_asset"
|
|
275
|
-
] ||
|
|
276
|
-
configuration[
|
|
277
|
-
`${normalizedComponentName}_assets_flavor2_reorder_handle_asset`
|
|
278
|
-
]
|
|
279
|
-
: undefined;
|
|
280
|
-
|
|
281
|
-
if (removeAsset || dragAsset) {
|
|
282
|
-
if (!result.item.actionButtons.default) {
|
|
283
|
-
result.item.actionButtons.default = {};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (removeAsset) {
|
|
287
|
-
result.item.actionButtons.default.removeAsset = removeAsset;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (dragAsset) {
|
|
291
|
-
result.item.actionButtons.default.dragAsset = dragAsset;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
213
|
return result;
|
|
296
214
|
};
|