@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.
Files changed (45) hide show
  1. package/actionsExecutor/ActionExecutor.ts +10 -1
  2. package/actionsExecutor/ActionExecutorContext.tsx +3 -0
  3. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  4. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +88 -0
  5. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  6. package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
  7. package/actionsExecutor/actions/index.ts +2 -0
  8. package/actionsExecutor/actions/openBottomSheet.ts +19 -6
  9. package/actionsExecutor/actions/showAlert.ts +51 -0
  10. package/actionsExecutor/actions/showToast.ts +2 -2
  11. package/actionsExecutor/consts.ts +1 -0
  12. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +4 -11
  13. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  14. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +9 -2
  15. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +15 -2
  16. package/appUtils/playerManager/OverlayObserver/utils.ts +2 -5
  17. package/appUtils/playerManager/__tests__/playerNative.test.ts +76 -0
  18. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  19. package/appUtils/playerManager/const.ts +13 -0
  20. package/appUtils/playerManager/player.ts +10 -0
  21. package/appUtils/playerManager/playerNative.ts +22 -0
  22. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  23. package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
  24. package/arrayUtils/index.ts +26 -43
  25. package/configurationUtils/__tests__/modalBlocksParser.test.ts +78 -69
  26. package/configurationUtils/modalBlocksParser.ts +24 -106
  27. package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
  28. package/entryActions/aliasPresentation.ts +26 -42
  29. package/entryActions/buildEntryActions.ts +1 -0
  30. package/entryActions/index.ts +0 -2
  31. package/modalState/ModalOrchestrator.ts +1 -0
  32. package/modalState/RemoteEditableCollection.ts +9 -1
  33. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +16 -0
  34. package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
  35. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  36. package/modalState/store.ts +9 -2
  37. package/modalState/types.ts +3 -0
  38. package/modalState/useBottomSheetContent.ts +17 -12
  39. package/package.json +2 -2
  40. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  41. package/pluginUtils/index.ts +29 -0
  42. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  43. package/reactHooks/cell-click/index.ts +21 -14
  44. package/reactHooks/navigation/__tests__/index.test.tsx +128 -1
  45. 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
- NativePlayerModule?.command?.(command, playerId, userInfo);
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
  });
@@ -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 | string[] = (item: any) => item?.id
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 rawId = getId(item);
179
- const keys = Array.isArray(rawId) ? rawId : [rawId];
180
-
181
- keys.forEach((key) => {
182
- if (key !== undefined && key !== null && key !== "") {
183
- itemMap.set(String(key), item);
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 && !reorderedSubset.includes(item)) {
197
+ if (item && !orderedIdsSet.has(strId)) {
196
198
  reorderedSubset.push(item);
197
- const rawId = getId(item);
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
- if (isMatched) {
226
- if (subsetIdx < reorderedSubset.length) {
227
- result.push(reorderedSubset[subsetIdx++]);
228
- }
229
- } else {
230
- result.push(item);
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
- return result;
220
+ return item;
221
+ });
239
222
  }
@@ -1,4 +1,7 @@
1
- import { parseModalBlocksConfiguration } from "../modalBlocksParser";
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 button.create_new_playlist_asset onto leadingIcon.asset", () => {
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).toEqual(
164
- expect.objectContaining({
165
- asset: "https://placehold.co/24x24/FF00FF/FFFFFF/png?text=CN",
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
- it("should map item.checkbox_asset and checked_checkbox_asset onto item.checkbox", () => {
171
- const result = parseModalBlocksConfiguration(
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.item.checkbox.default).toEqual(
182
- expect.objectContaining({
183
- asset: "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=CB",
184
- checkedAsset: "https://placehold.co/24x24/FFD60A/000000/png?text=CK",
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 map reorder/remove Studio assets onto item.actionButtons", () => {
209
+ it("should parse section_header title and action_buttons independently of header", () => {
190
210
  const result = parseModalBlocksConfiguration(
191
211
  {
192
- "reorder_playlist_item.remove_from_playlist_asset":
193
- "https://placehold.co/24x24/00FF00/000000/png?text=RM",
194
- "reorder_playlist_item.reorder_handle_asset":
195
- "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
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
- "playlist-experience"
226
+ "queue_action"
198
227
  );
199
228
 
200
- expect(result.item.actionButtons.default).toEqual(
229
+ expect(result.sectionHeader.title.default).toEqual(
201
230
  expect.objectContaining({
202
- removeAsset: "https://placehold.co/24x24/00FF00/000000/png?text=RM",
203
- dragAsset: "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
231
+ fontColor: "#ABCDEF",
232
+ fontSize: 13,
233
+ fontFamily: "SFProText-Bold",
204
234
  })
205
235
  );
206
- });
207
236
 
208
- it("should fall back to flavor1 reorder/remove assets", () => {
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
- removeAsset: "https://placehold.co/24x24/00FF00/000000/png?text=RM",
222
- dragAsset: "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
239
+ backgroundColor: "rgba(62, 62, 62, 1)",
240
+ fontColor: "#FFFFFF",
241
+ fontSize: 14,
242
+ borderRadius: 16,
223
243
  })
224
244
  );
225
- });
226
245
 
227
- it("should fall back to flavor2 reorder/remove assets", () => {
228
- const result = parseModalBlocksConfiguration(
229
- {
230
- "playlist_experience_assets_flavor2.remove_from_playlist_asset":
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.item.actionButtons.default).toEqual(
252
+ expect(result.header.title.default).toEqual(
239
253
  expect.objectContaining({
240
- removeAsset: "https://placehold.co/24x24/00FF00/000000/png?text=RM",
241
- dragAsset: "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH",
254
+ fontColor: "#111111",
255
+ fontSize: 22,
242
256
  })
243
257
  );
244
258
  });
245
259
 
246
- it("should ignore playlist-only asset keys for other theme plugins", () => {
260
+ it("should leave sectionHeader empty when keys are absent", () => {
247
261
  const result = parseModalBlocksConfiguration(
248
- {
249
- "button.create_new_playlist_asset":
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.button.leadingIcon.default?.asset).toBeUndefined();
258
- expect(result.item.actionButtons.default?.dragAsset).toBeUndefined();
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 parseModalBlocksConfiguration = (
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
  };