@applicaster/zapp-react-native-utils 16.0.0-alpha.1052711356 → 16.0.0-alpha.1054344748

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 (120) hide show
  1. package/actionsExecutor/ActionExecutor.ts +50 -3
  2. package/actionsExecutor/ActionExecutorContext.tsx +97 -19
  3. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  4. package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
  5. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +3 -0
  6. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  7. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  8. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +133 -0
  9. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +300 -0
  10. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  11. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  12. package/actionsExecutor/actions/__tests__/showToast.test.ts +29 -4
  13. package/actionsExecutor/actions/dismissBottomSheet.ts +2 -1
  14. package/actionsExecutor/actions/goBack.ts +60 -0
  15. package/actionsExecutor/actions/goHome.ts +31 -0
  16. package/actionsExecutor/actions/index.ts +8 -0
  17. package/actionsExecutor/actions/navigateToScreen.ts +18 -4
  18. package/actionsExecutor/actions/openBottomSheet.ts +51 -31
  19. package/actionsExecutor/actions/presentScreen.ts +78 -0
  20. package/actionsExecutor/actions/sendCloudEvent.ts +5 -8
  21. package/actionsExecutor/actions/showAlert.ts +51 -0
  22. package/actionsExecutor/actions/showToast.ts +62 -6
  23. package/actionsExecutor/consts.ts +27 -0
  24. package/actionsExecutor/feedDecorator.ts +6 -6
  25. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  26. package/appUtils/HooksManager/index.ts +9 -0
  27. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +142 -59
  28. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  29. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  30. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
  31. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  32. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  33. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
  34. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  35. package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
  36. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  37. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  38. package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
  39. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  40. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +97 -0
  41. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  42. package/appUtils/playerManager/{conts.ts → const.ts} +33 -0
  43. package/appUtils/playerManager/index.ts +3 -1
  44. package/appUtils/playerManager/player.ts +125 -3
  45. package/appUtils/playerManager/playerFactory.ts +1 -1
  46. package/appUtils/playerManager/playerNative.ts +28 -4
  47. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  48. package/appUtils/playerManager/playerToolbox.ts +192 -0
  49. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  50. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  51. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  52. package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
  53. package/arrayUtils/index.ts +62 -0
  54. package/cellUtils/index.ts +30 -3
  55. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  56. package/configurationUtils/__tests__/modalBlocksParser.test.ts +269 -0
  57. package/configurationUtils/manifestKeyParser.ts +50 -10
  58. package/configurationUtils/modalBlocksParser.ts +214 -0
  59. package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
  60. package/entryActions/aliasPresentation.ts +108 -0
  61. package/entryActions/buildEntryActions.ts +167 -0
  62. package/entryActions/index.ts +15 -0
  63. package/entryActions/types.ts +39 -0
  64. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  65. package/manifestUtils/_internals/index.js +8 -3
  66. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  67. package/manifestUtils/defaultManifestConfigurations/player.js +330 -1
  68. package/manifestUtils/index.js +3 -0
  69. package/manifestUtils/modalBlocks.js +304 -0
  70. package/modalState/ContentViewModel.ts +2 -0
  71. package/modalState/ModalOrchestrator.ts +23 -9
  72. package/modalState/RemoteEditableCollection.ts +77 -31
  73. package/modalState/__tests__/ContentViewModel.test.ts +3 -3
  74. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +35 -0
  75. package/modalState/__tests__/RemoteEditableCollection.test.ts +157 -1
  76. package/modalState/__tests__/index.test.ts +30 -1
  77. package/modalState/__tests__/useBottomSheetContent.test.ts +0 -23
  78. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  79. package/modalState/index.ts +6 -1
  80. package/modalState/store.ts +9 -2
  81. package/modalState/types.ts +56 -4
  82. package/modalState/useBottomSheetContent.ts +37 -26
  83. package/package.json +2 -2
  84. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  85. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  86. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  87. package/playerUtils/contentDataKeys.ts +134 -0
  88. package/playerUtils/index.ts +37 -16
  89. package/playerUtils/isAudioItem.ts +26 -0
  90. package/playerUtils/resolvePlayerActions.ts +71 -0
  91. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  92. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  93. package/pluginUtils/index.ts +29 -0
  94. package/reactHooks/actions/index.ts +67 -2
  95. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  96. package/reactHooks/cell-click/index.ts +21 -14
  97. package/reactHooks/connection/index.tsx +8 -5
  98. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  99. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  100. package/reactHooks/index.ts +2 -0
  101. package/reactHooks/layout/index.ts +1 -1
  102. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  103. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  104. package/reactHooks/navigation/index.ts +2 -0
  105. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  106. package/reactHooks/navigation/useRoute.ts +22 -5
  107. package/reactHooks/usePluginConfiguration.ts +4 -1
  108. package/reactHooks/utils/__tests__/index.test.js +22 -2
  109. package/reactHooks/utils/index.ts +11 -1
  110. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  111. package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
  112. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  113. package/uiActionsRegistrator/index.ts +25 -203
  114. package/uiActionsRegistrator/registry.ts +351 -0
  115. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  116. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  117. package/zappFrameworkUtils/localStorageHelper.ts +10 -2
  118. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  119. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  120. package/playerUtils/getPlayerActionButtons.ts +0 -17
@@ -0,0 +1,304 @@
1
+ function getModalBlockStyles({ prefix }) {
2
+ return [
3
+ {
4
+ group: true,
5
+ label: "Header Styles",
6
+ folded: true,
7
+ fields: [
8
+ {
9
+ type: "color_picker_rgba",
10
+ key: `${prefix}_style_header_container_default_backgroundColor`,
11
+ label: "Header Background Color",
12
+ initial_value: "rgba(30,30,30,1)",
13
+ },
14
+ {
15
+ type: "color_picker_rgba",
16
+ key: `${prefix}_style_header_container_default_borderBottomColor`,
17
+ label: "Header Border Bottom Color",
18
+ initial_value: "transparent",
19
+ },
20
+ {
21
+ type: "number_input",
22
+ key: `${prefix}_style_header_container_default_borderBottomSize`,
23
+ label: "Header Border Bottom Size",
24
+ initial_value: 0,
25
+ },
26
+ {
27
+ type: "color_picker_rgba",
28
+ key: `${prefix}_style_header_title_default_fontColor`,
29
+ label: "Header Title Color",
30
+ initial_value: "#EFEFEF",
31
+ },
32
+ {
33
+ type: "number_input",
34
+ key: `${prefix}_style_header_title_default_fontSize`,
35
+ label: "Header Title Font Size",
36
+ initial_value: 18,
37
+ },
38
+ {
39
+ type: "number_input",
40
+ key: `${prefix}_style_header_title_default_lineHeight`,
41
+ label: "Header Title Line Height",
42
+ initial_value: 32,
43
+ },
44
+ {
45
+ type: "color_picker_rgba",
46
+ key: `${prefix}_style_header_close_button_default_backgroundColor`,
47
+ label: "Close Button Background Color",
48
+ initial_value: "transparent",
49
+ },
50
+ {
51
+ type: "color_picker_rgba",
52
+ key: `${prefix}_style_header_close_button_hover_backgroundColor`,
53
+ label: "Close Button Hover Background Color",
54
+ initial_value: "rgba(46,46,46,1)",
55
+ },
56
+ {
57
+ type: "uploader",
58
+ key: `${prefix}_style_header_close_button_asset`,
59
+ label: "Close Button Icon Asset",
60
+ },
61
+ {
62
+ type: "color_picker_rgba",
63
+ key: `${prefix}_style_header_action_buttons_default_backgroundColor`,
64
+ label: "Header Action Button Background Color",
65
+ initial_value: "rgba(46,46,46,1)",
66
+ },
67
+ {
68
+ type: "color_picker_rgba",
69
+ key: `${prefix}_style_header_action_buttons_hover_backgroundColor`,
70
+ label: "Header Action Button Hover Color",
71
+ initial_value: "rgba(62,62,62,1)",
72
+ },
73
+ {
74
+ type: "color_picker_rgba",
75
+ key: `${prefix}_style_header_action_buttons_default_fontColor`,
76
+ label: "Header Action Button Title Color",
77
+ initial_value: "#EFEFEF",
78
+ },
79
+ {
80
+ type: "number_input",
81
+ key: `${prefix}_style_header_action_buttons_default_fontSize`,
82
+ label: "Header Action Button Font Size",
83
+ initial_value: 14,
84
+ },
85
+ {
86
+ type: "number_input",
87
+ key: `${prefix}_style_header_action_buttons_default_borderRadius`,
88
+ label: "Header Action Button Radius",
89
+ initial_value: 16,
90
+ },
91
+ ],
92
+ },
93
+ {
94
+ group: true,
95
+ label: "Item Styles",
96
+ folded: true,
97
+ fields: [
98
+ {
99
+ type: "color_picker_rgba",
100
+ key: `${prefix}_style_item_background_default_backgroundColor`,
101
+ label: "Item Background Color",
102
+ initial_value: "transparent",
103
+ },
104
+ {
105
+ type: "color_picker_rgba",
106
+ key: `${prefix}_style_item_background_focus_backgroundColor`,
107
+ label: "Item Focus/Touch Color",
108
+ initial_value: "rgba(62,62,62,1)",
109
+ },
110
+ {
111
+ type: "number_input",
112
+ key: `${prefix}_style_item_background_default_borderRadius`,
113
+ label: "Item Corner Radius",
114
+ initial_value: 12,
115
+ },
116
+ {
117
+ type: "number_input",
118
+ key: `${prefix}_style_item_background_default_paddingTop`,
119
+ label: "Item Padding Top",
120
+ initial_value: 8,
121
+ },
122
+ {
123
+ type: "number_input",
124
+ key: `${prefix}_style_item_background_default_paddingBottom`,
125
+ label: "Item Padding Bottom",
126
+ initial_value: 8,
127
+ },
128
+ {
129
+ type: "number_input",
130
+ key: `${prefix}_style_item_background_default_paddingLeft`,
131
+ label: "Item Padding Left",
132
+ initial_value: 10,
133
+ },
134
+ {
135
+ type: "number_input",
136
+ key: `${prefix}_style_item_background_default_paddingRight`,
137
+ label: "Item Padding Right",
138
+ initial_value: 10,
139
+ },
140
+ {
141
+ type: "number_input",
142
+ key: `${prefix}_style_item_image_default_width`,
143
+ label: "Item Image Width",
144
+ initial_value: 48,
145
+ },
146
+ {
147
+ type: "number_input",
148
+ key: `${prefix}_style_item_image_default_height`,
149
+ label: "Item Image Height",
150
+ initial_value: 48,
151
+ },
152
+ {
153
+ type: "number_input",
154
+ key: `${prefix}_style_item_image_default_borderRadius`,
155
+ label: "Item Image Radius",
156
+ initial_value: 6,
157
+ },
158
+ {
159
+ type: "color_picker_rgba",
160
+ key: `${prefix}_style_item_text_label_1_default_fontColor`,
161
+ label: "Item Title Color",
162
+ initial_value: "#FFFFFF",
163
+ },
164
+ {
165
+ type: "number_input",
166
+ key: `${prefix}_style_item_text_label_1_default_fontSize`,
167
+ label: "Item Title Font Size",
168
+ initial_value: 15,
169
+ },
170
+ {
171
+ type: "color_picker_rgba",
172
+ key: `${prefix}_style_item_text_label_2_default_fontColor`,
173
+ label: "Item Subtitle Color",
174
+ initial_value: "#999999",
175
+ },
176
+ {
177
+ type: "number_input",
178
+ key: `${prefix}_style_item_text_label_2_default_fontSize`,
179
+ label: "Item Subtitle Font Size",
180
+ initial_value: 13,
181
+ },
182
+ ],
183
+ },
184
+ {
185
+ group: true,
186
+ label: "Action Styles",
187
+ folded: true,
188
+ fields: [
189
+ {
190
+ type: "color_picker_rgba",
191
+ key: `${prefix}_style_action_background_default_backgroundColor`,
192
+ label: "Action Background Color",
193
+ initial_value: "transparent",
194
+ },
195
+ {
196
+ type: "color_picker_rgba",
197
+ key: `${prefix}_style_action_background_hover_backgroundColor`,
198
+ label: "Action Hover/Touch Color",
199
+ initial_value: "rgba(46,46,46,1)",
200
+ },
201
+ {
202
+ type: "number_input",
203
+ key: `${prefix}_style_action_background_default_borderRadius`,
204
+ label: "Action Corner Radius",
205
+ initial_value: 12,
206
+ },
207
+ {
208
+ type: "number_input",
209
+ key: `${prefix}_style_action_background_default_paddingTop`,
210
+ label: "Action Padding Top",
211
+ initial_value: 14,
212
+ },
213
+ {
214
+ type: "number_input",
215
+ key: `${prefix}_style_action_background_default_paddingBottom`,
216
+ label: "Action Padding Bottom",
217
+ initial_value: 14,
218
+ },
219
+ {
220
+ type: "color_picker_rgba",
221
+ key: `${prefix}_style_action_title_default_fontColor`,
222
+ label: "Action Title Color",
223
+ initial_value: "#EFEFEF",
224
+ },
225
+ {
226
+ type: "number_input",
227
+ key: `${prefix}_style_action_title_default_fontSize`,
228
+ label: "Action Title Font Size",
229
+ initial_value: 14,
230
+ },
231
+ {
232
+ type: "uploader",
233
+ key: `${prefix}_style_action_trailing_icon_chevron_asset`,
234
+ label: "Action Chevron Asset",
235
+ },
236
+ ],
237
+ },
238
+ {
239
+ group: true,
240
+ label: "Button Styles",
241
+ folded: true,
242
+ fields: [
243
+ {
244
+ type: "color_picker_rgba",
245
+ key: `${prefix}_style_button_background_default_backgroundColor`,
246
+ label: "Button Background Color",
247
+ initial_value: "#FFFFFF",
248
+ },
249
+ {
250
+ type: "color_picker_rgba",
251
+ key: `${prefix}_style_button_background_focus_backgroundColor`,
252
+ label: "Button Focus Color",
253
+ initial_value: "#E0E0E0",
254
+ },
255
+ {
256
+ type: "color_picker_rgba",
257
+ key: `${prefix}_style_button_background_inactive_backgroundColor`,
258
+ label: "Button Inactive Color",
259
+ initial_value: "#999999",
260
+ },
261
+ {
262
+ type: "number_input",
263
+ key: `${prefix}_style_button_background_default_borderRadius`,
264
+ label: "Button Corner Radius",
265
+ initial_value: 12,
266
+ },
267
+ {
268
+ type: "color_picker_rgba",
269
+ key: `${prefix}_style_button_title_default_fontColor`,
270
+ label: "Button Title Color",
271
+ initial_value: "#000000",
272
+ },
273
+ {
274
+ type: "number_input",
275
+ key: `${prefix}_style_button_title_default_fontSize`,
276
+ label: "Button Title Font Size",
277
+ initial_value: 15,
278
+ },
279
+ ],
280
+ },
281
+ ];
282
+ }
283
+
284
+ function getModalLocalizations({ prefix }) {
285
+ return [
286
+ {
287
+ type: "text_input",
288
+ label: "Queue Header Title",
289
+ key: `${prefix}_header_title`,
290
+ initial_value: "Queue",
291
+ },
292
+ {
293
+ type: "text_input",
294
+ label: "Go To Queue Action Label",
295
+ key: `${prefix}_label_go_to_queue`,
296
+ initial_value: "Go To Queue",
297
+ },
298
+ ];
299
+ }
300
+
301
+ module.exports = {
302
+ getModalBlockStyles,
303
+ getModalLocalizations,
304
+ };
@@ -16,6 +16,7 @@ export interface ContentState {
16
16
  role?: string;
17
17
  behavior?: SelectionBehavior;
18
18
  dynamicCollection?: DynamicCollectionOptions;
19
+ themePluginId?: string;
19
20
  }
20
21
 
21
22
  export class ContentViewModel {
@@ -34,6 +35,7 @@ export class ContentViewModel {
34
35
  role: content.role,
35
36
  behavior: content.behavior,
36
37
  dynamicCollection: content.dynamicCollection,
38
+ themePluginId: content.themePluginId,
37
39
  });
38
40
 
39
41
  this.initEditableCollection(content);
@@ -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.
@@ -37,6 +43,8 @@ function menuToModalArgs(
37
43
  itemsUrl: menu.content.itemsUrl,
38
44
  title: menu.header?.title ?? menu.content.title,
39
45
  summary: menu.header?.subtitle,
46
+ styleOverrides: menu.content.styleOverrides,
47
+ emptyMessage: menu.content.emptyMessage,
40
48
  onPress: onItemPress,
41
49
  viewModel,
42
50
  },
@@ -135,15 +143,21 @@ class ModalOrchestrator {
135
143
  private _buildOnPress(
136
144
  onItemPress?: (item: MenuItem) => void | Promise<void>
137
145
  ): (item: MenuItem) => void | Promise<void> {
138
- return (item: MenuItem) => {
139
- if (isPresentSubMenuAction(item.action)) {
140
- // Navigate deeper into the sub-menu
141
- this.open(item.action.menu, onItemPress);
142
-
143
- return;
146
+ return async (item: MenuItem) => {
147
+ if (onItemPress) {
148
+ await onItemPress(item);
144
149
  }
145
150
 
146
- return onItemPress?.(item);
151
+ const actions = getMenuItemActions(item);
152
+
153
+ const subMenuAction = actions.find(isPresentSubMenuAction) as
154
+ | PresentSubMenuAction
155
+ | undefined;
156
+
157
+ if (subMenuAction) {
158
+ // Navigate deeper into the sub-menu
159
+ this.open(subMenuAction.menu, onItemPress);
160
+ }
147
161
  };
148
162
  }
149
163
 
@@ -1,10 +1,11 @@
1
1
  import { BehaviorSubject, Observable } from "rxjs";
2
2
  import { Content, MenuItem, Operation } from "./types";
3
3
  import { EditableCollection } from "./EditableCollection";
4
+ import { reorderByIds } from "../arrayUtils";
4
5
  import { actionExecutor } from "../actionsExecutor/ActionExecutor";
5
6
  import { createLogger } from "../logger";
6
7
 
7
- const { log_warning, log_error } = createLogger({
8
+ const { log_debug, log_warning, log_error } = createLogger({
8
9
  subsystem: "modalState",
9
10
  category: "RemoteEditableCollection",
10
11
  });
@@ -15,6 +16,7 @@ export class RemoteEditableCollection implements EditableCollection {
15
16
  private itemsSubject: BehaviorSubject<MenuItem[]>;
16
17
  private refetch: () => Promise<void>;
17
18
  private content: Content;
19
+ private pendingMutationsCount = 0;
18
20
 
19
21
  constructor(content: Content, refetch: () => Promise<void>) {
20
22
  this.content = content;
@@ -29,6 +31,14 @@ export class RemoteEditableCollection implements EditableCollection {
29
31
  * a successful mutation reloads the feed). Overrides optimistic state.
30
32
  */
31
33
  sync(items: MenuItem[]): void {
34
+ if (this.pendingMutationsCount > 0) {
35
+ log_debug(
36
+ `sync: Suppressing server sync because ${this.pendingMutationsCount} local mutation(s) are in-flight`
37
+ );
38
+
39
+ return;
40
+ }
41
+
32
42
  this.itemsSubject.next(items);
33
43
  }
34
44
 
@@ -39,16 +49,26 @@ export class RemoteEditableCollection implements EditableCollection {
39
49
 
40
50
  if (!item) return;
41
51
 
52
+ this.pendingMutationsCount++;
53
+
42
54
  // Optimistic update
43
55
  const newItems = items.filter((_, i) => i !== index);
44
56
  this.itemsSubject.next(newItems);
45
57
 
58
+ let actionFailed = false;
59
+
46
60
  try {
47
61
  await this.executeActionAlias(item, "remove_item");
48
- await this.refetch();
49
62
  } catch (e) {
63
+ actionFailed = true;
50
64
  log_error("Remove failed, rolling back", { error: e });
51
65
  this.itemsSubject.next(snapshot);
66
+ } finally {
67
+ this.pendingMutationsCount = Math.max(0, this.pendingMutationsCount - 1);
68
+ }
69
+
70
+ if (!actionFailed) {
71
+ await this.refetch();
52
72
  }
53
73
  }
54
74
 
@@ -57,32 +77,31 @@ export class RemoteEditableCollection implements EditableCollection {
57
77
  const snapshot = [...items];
58
78
 
59
79
  if (itemIds && itemIds.length > 0) {
60
- const itemMap = new Map(items.map((item) => [item.id, item]));
61
- const newItems: MenuItem[] = [];
62
-
63
- itemIds.forEach((id) => {
64
- const found = itemMap.get(id);
65
- if (found) newItems.push(found);
66
- });
67
-
68
- items.forEach((item) => {
69
- if (!newItems.includes(item)) newItems.push(item);
70
- });
71
-
80
+ this.pendingMutationsCount++;
81
+ const newItems = reorderByIds(items, itemIds);
72
82
  this.itemsSubject.next(newItems);
73
83
 
84
+ let actionFailed = false;
85
+
74
86
  try {
75
87
  const targetItem = newItems[0] || items[0];
76
88
 
77
89
  await this.executeActionAlias(targetItem, "reorder_item", {
78
90
  itemIds,
79
91
  });
80
-
81
- await this.refetch();
82
92
  } catch (e) {
83
- console.error("[RemoteEditableCollection] move failed:", e);
93
+ actionFailed = true;
84
94
  log_error("Move failed, rolling back", { error: e });
85
95
  this.itemsSubject.next(snapshot);
96
+ } finally {
97
+ this.pendingMutationsCount = Math.max(
98
+ 0,
99
+ this.pendingMutationsCount - 1
100
+ );
101
+ }
102
+
103
+ if (!actionFailed) {
104
+ await this.refetch();
86
105
  }
87
106
  }
88
107
  }
@@ -99,15 +118,30 @@ export class RemoteEditableCollection implements EditableCollection {
99
118
  return;
100
119
  }
101
120
 
121
+ this.pendingMutationsCount++;
122
+ let actionFailed = false;
123
+
102
124
  try {
103
125
  for (const action of addEvents) {
104
- await actionExecutor.handleAction(action as any);
126
+ if (this.content.context) {
127
+ await actionExecutor.handleAction(
128
+ action as any,
129
+ this.content.context
130
+ );
131
+ } else {
132
+ await actionExecutor.handleAction(action as any);
133
+ }
105
134
  }
106
-
107
- await this.refetch();
108
135
  } catch (e) {
136
+ actionFailed = true;
109
137
  log_error("Add/create collection failed", { error: e });
110
138
  this.itemsSubject.next(snapshot);
139
+ } finally {
140
+ this.pendingMutationsCount = Math.max(0, this.pendingMutationsCount - 1);
141
+ }
142
+
143
+ if (!actionFailed) {
144
+ await this.refetch();
111
145
  }
112
146
  }
113
147
 
@@ -115,32 +149,46 @@ export class RemoteEditableCollection implements EditableCollection {
115
149
  const items = this.itemsSubject.value;
116
150
  const snapshot = [...items];
117
151
 
152
+ this.pendingMutationsCount++;
118
153
  this.itemsSubject.next([]);
119
154
 
155
+ let actionFailed = false;
156
+
120
157
  try {
121
158
  const firstItem = items[0];
122
159
 
123
160
  if (firstItem) {
124
- await this.executeActionAlias(firstItem, "clear_collection");
161
+ await this.executeActionAlias(firstItem, [
162
+ "delete_collection",
163
+ "clear_collection",
164
+ ]);
125
165
  }
126
-
127
- await this.refetch();
128
166
  } catch (e) {
167
+ actionFailed = true;
129
168
  log_error("deleteCollection failed, rolling back", { error: e });
130
169
  this.itemsSubject.next(snapshot);
170
+ } finally {
171
+ this.pendingMutationsCount = Math.max(0, this.pendingMutationsCount - 1);
172
+ }
173
+
174
+ if (!actionFailed) {
175
+ await this.refetch();
131
176
  }
132
177
  }
133
178
 
134
179
  private async executeActionAlias(
135
180
  item: MenuItem,
136
- alias: string,
181
+ alias: string | string[],
137
182
  data?: Record<string, unknown>
138
183
  ): Promise<void> {
139
184
  const entryActions = item.entryActions || [];
185
+ const aliases = Array.isArray(alias) ? alias : [alias];
140
186
 
141
- const actionGroup = entryActions.find((ea) => ea.button?.alias === alias);
187
+ const actionGroup = entryActions.find(
188
+ (ea) => ea.button?.alias && aliases.includes(ea.button.alias)
189
+ );
142
190
 
143
- console.log("[RemoteEditableCollection] executeActionAlias:", {
191
+ log_debug("executeActionAlias:", {
144
192
  alias,
145
193
  itemId: item.id,
146
194
  entryActionsCount: entryActions.length,
@@ -150,7 +198,7 @@ export class RemoteEditableCollection implements EditableCollection {
150
198
  });
151
199
 
152
200
  if (!actionGroup || !actionGroup.actions) {
153
- log_warning(`No entry action found for alias "${alias}"`, {
201
+ log_warning(`No entry action found for alias "${aliases.join(", ")}"`, {
154
202
  itemId: item.id,
155
203
  });
156
204
 
@@ -173,14 +221,12 @@ export class RemoteEditableCollection implements EditableCollection {
173
221
  }
174
222
  : action;
175
223
 
176
- console.log(
177
- "[RemoteEditableCollection] handling action:",
178
- actionToExecute
179
- );
224
+ log_debug("handling action:", actionToExecute);
180
225
 
181
226
  await actionExecutor.handleAction(
182
227
  actionToExecute as any,
183
228
  {
229
+ ...this.content.context,
184
230
  entry: item as any,
185
231
  } as any
186
232
  );
@@ -18,7 +18,7 @@ describe("ContentViewModel", () => {
18
18
 
19
19
  const viewModel = new ContentViewModel(content);
20
20
 
21
- expect(viewModel.getState()).toEqual({
21
+ expect(viewModel.getState()).toMatchObject({
22
22
  items: content.items,
23
23
  isLoading: false,
24
24
  role: undefined,
@@ -71,7 +71,7 @@ describe("ContentViewModel", () => {
71
71
  expect.stringMatching(/^http:\/\/example\.com\/items\?_t=\d+$/)
72
72
  );
73
73
 
74
- expect(viewModel.getState()).toEqual({
74
+ expect(viewModel.getState()).toMatchObject({
75
75
  items: mockItems,
76
76
  isLoading: false,
77
77
  role: "collection_selector",
@@ -154,7 +154,7 @@ describe("ContentViewModel", () => {
154
154
 
155
155
  await new Promise(process.nextTick);
156
156
 
157
- expect(viewModel.getState()).toEqual({
157
+ expect(viewModel.getState()).toMatchObject({
158
158
  items: [],
159
159
  isLoading: false,
160
160
  error: "Network Error",
@@ -25,4 +25,39 @@ describe("ModalOrchestrator phase 3", () => {
25
25
  expect(props.viewModel.getState().role).toBe("collection_selector");
26
26
  expect(props.title).toBe("Playlists");
27
27
  });
28
+
29
+ it("forwards header title and subtitle as summary", () => {
30
+ modalOrchestrator.open({
31
+ header: {
32
+ title: "Sleep Timer",
33
+ subtitle: "remaining",
34
+ },
35
+ content: {
36
+ title: "Sleep Timer",
37
+ items: [{ id: "5", title: "5 minutes" }],
38
+ },
39
+ });
40
+
41
+ const props = (modalStore.getState().modalState.screen as any)
42
+ ?.modalBottomSheetContentProps;
43
+
44
+ expect(props.title).toBe("Sleep Timer");
45
+ expect(props.summary).toBe("remaining");
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
+ });
28
63
  });