@applicaster/zapp-react-native-utils 16.0.0-rc.79 → 16.0.0-rc.80

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.
@@ -4,6 +4,9 @@ import { ActionResult } from "../../ActionExecutor";
4
4
 
5
5
  jest.mock("../../../modalState", () => ({
6
6
  dismissModal: jest.fn(),
7
+ modalOrchestrator: {
8
+ close: jest.fn(),
9
+ },
7
10
  }));
8
11
 
9
12
  describe("dismissBottomSheetAction", () => {
@@ -1,5 +1,5 @@
1
1
  import { openBottomSheetAction } from "../openBottomSheet";
2
- import { ActionResult } from "../../ActionExecutor";
2
+ import { actionExecutor, ActionResult } from "../../ActionExecutor";
3
3
  import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
4
4
 
5
5
  jest.mock("../../../modalState/store", () => ({
@@ -97,4 +97,116 @@ describe("openBottomSheetAction inline items", () => {
97
97
  expect(modalOrchestrator.replace).toHaveBeenCalledTimes(1);
98
98
  expect(modalOrchestrator.open).not.toHaveBeenCalled();
99
99
  });
100
+
101
+ it("executes all actions in tap_actions when onItemPress is called", async () => {
102
+ const multiActionSheet = {
103
+ type: "openBottomSheet",
104
+ options: {
105
+ header: { title: "Target Playlists" },
106
+ content: {
107
+ role: "collection_selector",
108
+ behavior: { selectMode: "none", currentSelection: [] },
109
+ items: [
110
+ {
111
+ id: "playlist-1",
112
+ title: "My Playlist",
113
+ extensions: {
114
+ tap_actions: {
115
+ actions: [
116
+ {
117
+ type: "sendCloudEvent",
118
+ options: { subject: "add_collection_to_collection" },
119
+ },
120
+ {
121
+ type: "showToast",
122
+ options: { message: "Added to playlist" },
123
+ },
124
+ { type: "dismissBottomSheet", options: {} },
125
+ ],
126
+ },
127
+ },
128
+ },
129
+ ],
130
+ },
131
+ },
132
+ } as any;
133
+
134
+ await openBottomSheetAction(multiActionSheet);
135
+
136
+ const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
137
+ .calls[0];
138
+
139
+ expect(menu.content.items[0].actions).toHaveLength(3);
140
+
141
+ const handleActionsSpy = jest
142
+ .spyOn(actionExecutor, "handleActions")
143
+ .mockResolvedValue(ActionResult.Success);
144
+
145
+ await onItemPress(menu.content.items[0]);
146
+
147
+ expect(handleActionsSpy).toHaveBeenCalledWith(
148
+ [
149
+ {
150
+ type: "sendCloudEvent",
151
+ options: { subject: "add_collection_to_collection" },
152
+ },
153
+ {
154
+ type: "showToast",
155
+ options: { message: "Added to playlist" },
156
+ },
157
+ { type: "dismissBottomSheet", options: {} },
158
+ ],
159
+ expect.objectContaining({
160
+ entry: menu.content.items[0],
161
+ })
162
+ );
163
+
164
+ handleActionsSpy.mockRestore();
165
+ });
166
+
167
+ it("executes preceding actions and excludes presentSubMenu from actionExecutor", async () => {
168
+ const subMenuSheet = {
169
+ type: "openBottomSheet",
170
+ options: {
171
+ header: { title: "Menu" },
172
+ content: {
173
+ items: [
174
+ {
175
+ id: "item-with-submenu",
176
+ title: "More Options",
177
+ extensions: {
178
+ tap_actions: {
179
+ actions: [
180
+ { type: "sendAnalytics", options: { event: "click_more" } },
181
+ {
182
+ type: "presentSubMenu",
183
+ menu: { content: { title: "Sub Options", items: [] } },
184
+ },
185
+ ],
186
+ },
187
+ },
188
+ },
189
+ ],
190
+ },
191
+ },
192
+ } as any;
193
+
194
+ await openBottomSheetAction(subMenuSheet);
195
+
196
+ const [menu, onItemPress] = (modalOrchestrator.open as jest.Mock).mock
197
+ .calls[0];
198
+
199
+ const handleActionsSpy = jest
200
+ .spyOn(actionExecutor, "handleActions")
201
+ .mockResolvedValue(ActionResult.Success);
202
+
203
+ await onItemPress(menu.content.items[0]);
204
+
205
+ expect(handleActionsSpy).toHaveBeenCalledWith(
206
+ [{ type: "sendAnalytics", options: { event: "click_more" } }],
207
+ expect.objectContaining({ entry: menu.content.items[0] })
208
+ );
209
+
210
+ handleActionsSpy.mockRestore();
211
+ });
100
212
  });
@@ -1,5 +1,5 @@
1
1
  /// <reference types="@applicaster/applicaster-types" />
2
- import { dismissModal } from "../../modalState";
2
+ import { dismissModal, modalOrchestrator } from "../../modalState";
3
3
  import { ActionResult } from "../ActionExecutor";
4
4
  import { ActionHandler } from "../types";
5
5
  import { createLogger } from "../../logger";
@@ -16,6 +16,7 @@ export const dismissBottomSheetAction: ActionHandler<
16
16
  any
17
17
  > = async (): Promise<ActionResult> => {
18
18
  log_info("handleAction: dismissBottomSheet");
19
+ modalOrchestrator.close();
19
20
  dismissModal();
20
21
 
21
22
  return ActionResult.Success;
@@ -8,6 +8,7 @@ import { modalOrchestrator } from "../../modalState/ModalOrchestrator";
8
8
  import { openBottomSheetModal } from "../../modalState/store";
9
9
  import {
10
10
  DynamicCollectionOptions,
11
+ getMenuItemActions,
11
12
  isPresentSubMenuAction,
12
13
  Menu,
13
14
  MenuItem,
@@ -91,6 +92,7 @@ export function entryToMenuItem(
91
92
  icon: firstImage?.src,
92
93
  isSelected: currentSelection.includes(itemId),
93
94
  action: primaryAction,
95
+ actions: tapActions,
94
96
  entryActions: entry.extensions?.entry_action || [],
95
97
  };
96
98
  }
@@ -288,18 +290,21 @@ export async function openBottomSheetAction(
288
290
  * reference it.
289
291
  */
290
292
  const onItemPress = async (item: MenuItem): Promise<void> => {
291
- if (!item.action || isPresentSubMenuAction(item.action)) {
292
- // Submenu navigation is handled internally by the orchestrator
293
- return;
294
- }
293
+ const actions = getMenuItemActions(item);
295
294
 
296
- await actionExecutor.handleAction(
297
- item.action as ActionType,
298
- {
299
- ...context,
300
- entry: item as any,
301
- } as any
295
+ const executableActions = actions.filter(
296
+ (action) => !isPresentSubMenuAction(action)
302
297
  );
298
+
299
+ if (executableActions.length > 0) {
300
+ await actionExecutor.handleActions(
301
+ executableActions as ActionType[],
302
+ {
303
+ ...context,
304
+ entry: item as any,
305
+ } as any
306
+ );
307
+ }
303
308
  };
304
309
 
305
310
  if (action.options?.replace) {
@@ -1,10 +1,16 @@
1
1
  import { dismissModal, modalStore, openBottomSheetModal } from "./store";
2
- import { isPresentSubMenuAction, Menu, MenuItem } from "./types";
2
+ import {
3
+ getMenuItemActions,
4
+ isPresentSubMenuAction,
5
+ Menu,
6
+ MenuItem,
7
+ PresentSubMenuAction,
8
+ } from "./types";
3
9
  import { ContentViewModel } from "./ContentViewModel";
4
10
 
5
11
  export type { Menu, MenuItem };
6
12
 
7
- export { isPresentSubMenuAction };
13
+ export { isPresentSubMenuAction, getMenuItemActions };
8
14
 
9
15
  /**
10
16
  * Converts a Menu model into the props expected by BottomSheetModalContent.
@@ -136,15 +142,21 @@ class ModalOrchestrator {
136
142
  private _buildOnPress(
137
143
  onItemPress?: (item: MenuItem) => void | Promise<void>
138
144
  ): (item: MenuItem) => void | Promise<void> {
139
- return (item: MenuItem) => {
140
- if (isPresentSubMenuAction(item.action)) {
141
- // Navigate deeper into the sub-menu
142
- this.open(item.action.menu, onItemPress);
143
-
144
- return;
145
+ return async (item: MenuItem) => {
146
+ if (onItemPress) {
147
+ await onItemPress(item);
145
148
  }
146
149
 
147
- return onItemPress?.(item);
150
+ const actions = getMenuItemActions(item);
151
+
152
+ const subMenuAction = actions.find(isPresentSubMenuAction) as
153
+ | PresentSubMenuAction
154
+ | undefined;
155
+
156
+ if (subMenuAction) {
157
+ // Navigate deeper into the sub-menu
158
+ this.open(subMenuAction.menu, onItemPress);
159
+ }
148
160
  };
149
161
  }
150
162
 
@@ -1,4 +1,4 @@
1
- import { modalStore } from "../";
1
+ import { modalStore, getMenuItemActions } from "../";
2
2
 
3
3
  describe("Modal State", () => {
4
4
  it("should have initial state", () => {
@@ -48,4 +48,33 @@ describe("Modal State", () => {
48
48
  });
49
49
  });
50
50
  });
51
+
52
+ describe("getMenuItemActions", () => {
53
+ it("returns actions array when actions is provided", () => {
54
+ const actions = [{ type: "action1" }, { type: "action2" }];
55
+ const item = { id: "1", title: "Item", actions };
56
+
57
+ expect(getMenuItemActions(item)).toEqual(actions);
58
+ });
59
+
60
+ it("returns [action] when single action is provided", () => {
61
+ const action = { type: "action1" };
62
+ const item = { id: "1", title: "Item", action };
63
+
64
+ expect(getMenuItemActions(item)).toEqual([action]);
65
+ });
66
+
67
+ it("returns action when action is passed as an array", () => {
68
+ const actions = [{ type: "action1" }];
69
+ const item = { id: "1", title: "Item", action: actions as any };
70
+
71
+ expect(getMenuItemActions(item)).toEqual(actions);
72
+ });
73
+
74
+ it("returns empty array when no action or actions are provided", () => {
75
+ expect(getMenuItemActions({ id: "1", title: "Item" })).toEqual([]);
76
+ expect(getMenuItemActions(null)).toEqual([]);
77
+ expect(getMenuItemActions(undefined)).toEqual([]);
78
+ });
79
+ });
51
80
  });
@@ -25,6 +25,11 @@ export type {
25
25
  SelectionBehavior,
26
26
  } from "./types";
27
27
 
28
- export { isPresentSubMenuAction, OPERATIONS, hasOperation } from "./types";
28
+ export {
29
+ isPresentSubMenuAction,
30
+ getMenuItemActions,
31
+ OPERATIONS,
32
+ hasOperation,
33
+ } from "./types";
29
34
 
30
35
  export { useBottomSheetContent } from "./useBottomSheetContent";
@@ -37,8 +37,8 @@ export interface MenuItem {
37
37
  summary?: string;
38
38
  icon?: string;
39
39
  isSelected?: boolean;
40
- // When item represents an audio item, clicking on it will start playback
41
- // and secondary action will allow to add it to queue or playlist
40
+ actions?: (Action | PresentSubMenuAction)[];
41
+ /** @deprecated Use `actions` array instead. Maintained for backwards compatibility. */
42
42
  action?: Action | PresentSubMenuAction;
43
43
  secondaryAction?: SecondaryAction;
44
44
  trailingButton?: any;
@@ -131,3 +131,29 @@ export function isPresentSubMenuAction(
131
131
  ): action is PresentSubMenuAction {
132
132
  return !!action && typeof action === "object" && "menu" in action;
133
133
  }
134
+
135
+ /**
136
+ * Normalizes an item's action/actions into an array of Action items.
137
+ * Supports canonical `actions: Action[]` and backwards-compatible `action: Action | Action[]`.
138
+ */
139
+ export function getMenuItemActions(
140
+ item?: Partial<MenuItem> | null
141
+ ): (Action | PresentSubMenuAction)[] {
142
+ if (!item) {
143
+ return [];
144
+ }
145
+
146
+ if (Array.isArray(item.actions) && item.actions.length > 0) {
147
+ return item.actions;
148
+ }
149
+
150
+ if (Array.isArray((item as any).action)) {
151
+ return (item as any).action;
152
+ }
153
+
154
+ if (item.action) {
155
+ return [item.action];
156
+ }
157
+
158
+ return [];
159
+ }
@@ -1,14 +1,15 @@
1
1
  import React, { useEffect, useState } from "react";
2
2
  import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
3
3
 
4
- import {
5
- isPresentSubMenuAction,
6
- MenuItem,
7
- modalOrchestrator,
8
- } from "./ModalOrchestrator";
4
+ import { MenuItem, modalOrchestrator } from "./ModalOrchestrator";
9
5
 
10
6
  import { ContentViewModel } from "./ContentViewModel";
11
- import type { DynamicCollectionOptions, SelectionBehavior } from "./types";
7
+ import {
8
+ DynamicCollectionOptions,
9
+ getMenuItemActions,
10
+ isPresentSubMenuAction,
11
+ SelectionBehavior,
12
+ } from "./types";
12
13
  import type { EditableCollection } from "./EditableCollection";
13
14
 
14
15
  type Props = {
@@ -65,7 +66,8 @@ export const useBottomSheetContent = (props: Props): Return => {
65
66
  * so that checkmarks (isSelected) reflect the updated server state.
66
67
  */
67
68
  const handleItemPress = async (item: MenuItem) => {
68
- const isSubMenu = item.action && isPresentSubMenuAction(item.action);
69
+ const actions = getMenuItemActions(item);
70
+ const isSubMenu = actions.some(isPresentSubMenuAction);
69
71
 
70
72
  await onPress(item);
71
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "16.0.0-rc.79",
3
+ "version": "16.0.0-rc.80",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "16.0.0-rc.79",
30
+ "@applicaster/applicaster-types": "16.0.0-rc.80",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",