@applicaster/zapp-react-native-utils 16.0.0-alpha.2024822855 → 16.0.0-alpha.2105365027
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/actionUtils/index.ts +3 -1
- package/actionsExecutor/ActionExecutor.ts +10 -1
- package/actionsExecutor/ActionExecutorContext.tsx +29 -1
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +3 -0
- package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
- package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
- package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
- package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +201 -1
- package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
- package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
- package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
- package/actionsExecutor/actions/dismissBottomSheet.ts +2 -1
- package/actionsExecutor/actions/goBack.ts +60 -0
- package/actionsExecutor/actions/goHome.ts +31 -0
- package/actionsExecutor/actions/index.ts +8 -0
- package/actionsExecutor/actions/navigateToScreen.ts +29 -45
- package/actionsExecutor/actions/openBottomSheet.ts +35 -16
- package/actionsExecutor/actions/presentScreen.ts +78 -0
- package/actionsExecutor/actions/showAlert.ts +51 -0
- package/actionsExecutor/actions/showToast.ts +2 -2
- package/actionsExecutor/consts.ts +4 -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 +114 -0
- package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
- package/appUtils/playerManager/__tests__/playerToolbox.test.ts +97 -0
- package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
- package/appUtils/playerManager/const.ts +13 -0
- package/appUtils/playerManager/index.ts +7 -0
- package/appUtils/playerManager/player.ts +10 -0
- package/appUtils/playerManager/playerNative.ts +27 -3
- package/appUtils/playerManager/playerNativeCommand.ts +3 -6
- package/appUtils/playerManager/playerToolbox.ts +157 -0
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
- package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
- package/arrayUtils/index.ts +26 -43
- package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
- package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
- package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
- package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
- package/cellUtils/index.ts +74 -19
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +286 -1
- package/configurationUtils/index.ts +77 -73
- package/configurationUtils/manifestKeyParser.ts +12 -1
- package/configurationUtils/modalBlocksParser.ts +78 -4
- package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
- package/entryActions/aliasPresentation.ts +108 -0
- package/entryActions/buildEntryActions.ts +167 -0
- package/entryActions/index.ts +15 -0
- package/entryActions/types.ts +39 -0
- package/focusManager/FocusManager.ts +26 -0
- package/focusManager/__tests__/FocusManager.test.ts +9 -0
- package/localizationUtils/index.ts +12 -6
- package/logger/__tests__/xrayToolbox.test.ts +69 -0
- package/logger/xrayToolbox.ts +67 -0
- package/manifestUtils/_internals/index.js +8 -3
- package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +30 -1
- package/manifestUtils/keys.js +6 -1
- package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
- package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
- package/manifestUtils/mobileAction/groups/defaults.js +2 -1
- package/modalState/ModalOrchestrator.ts +22 -9
- 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__/index.test.ts +30 -1
- package/modalState/__tests__/useModalStoreState.test.ts +142 -0
- package/modalState/index.ts +6 -1
- package/modalState/store.ts +9 -2
- package/modalState/types.ts +31 -2
- package/modalState/useBottomSheetContent.ts +24 -17
- package/navigationUtils/xrayNavigator.ts +12 -0
- package/package.json +2 -2
- package/perfTracker.ts +203 -0
- package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
- package/playerUtils/index.ts +17 -1
- package/playerUtils/resolvePlayerActions.ts +71 -0
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
- package/reactHooks/actions/index.ts +67 -2
- package/reactHooks/cell-click/__tests__/index.test.js +64 -0
- package/reactHooks/cell-click/index.ts +21 -14
- package/reactHooks/connection/index.tsx +8 -5
- package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
- package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
- package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
- package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
- package/reactHooks/navigation/index.ts +2 -0
- package/reactHooks/navigation/usePresentScreen.ts +157 -0
- package/reactHooks/navigation/useRoute.ts +22 -5
- package/reactHooks/usePluginConfiguration.ts +84 -46
- package/riverComponetsMeasurementProvider/index.tsx +8 -1
- package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
- package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
- package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
- package/uiActionsRegistrator/index.ts +25 -203
- package/uiActionsRegistrator/registry.ts +351 -0
- package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
- package/uiActionsRegistrator/usableActionItems.ts +66 -0
- package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
- package/playerUtils/getPlayerActionButtons.ts +0 -17
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const xray = require("@applicaster/quick-brick-xray");
|
|
2
|
+
|
|
3
|
+
const toolbox = { name: "test", description: "", tools: [] };
|
|
4
|
+
|
|
5
|
+
const load = (): typeof import("../xrayToolbox") => {
|
|
6
|
+
let module;
|
|
7
|
+
|
|
8
|
+
jest.isolateModules(() => {
|
|
9
|
+
module = require("../xrayToolbox");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return module;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("registerXrayToolbox", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
delete xray.registerReactToolbox;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("registers once per name", async () => {
|
|
21
|
+
xray.registerReactToolbox = jest.fn().mockResolvedValue(undefined);
|
|
22
|
+
const { registerXrayToolbox } = load();
|
|
23
|
+
|
|
24
|
+
await expect(registerXrayToolbox(toolbox)).resolves.toBe(true);
|
|
25
|
+
await expect(registerXrayToolbox(toolbox)).resolves.toBe(true);
|
|
26
|
+
|
|
27
|
+
expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("resolves false without a toolbox API", async () => {
|
|
31
|
+
await expect(load().registerXrayToolbox(toolbox)).resolves.toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps a failed registration instead of retrying it", async () => {
|
|
35
|
+
xray.registerReactToolbox = jest
|
|
36
|
+
.fn()
|
|
37
|
+
.mockRejectedValue(new Error("XRayToolboxBridge is not available"));
|
|
38
|
+
|
|
39
|
+
const { registerXrayToolbox } = load();
|
|
40
|
+
|
|
41
|
+
await expect(registerXrayToolbox(toolbox)).resolves.toBe(false);
|
|
42
|
+
await expect(registerXrayToolbox(toolbox)).resolves.toBe(false);
|
|
43
|
+
|
|
44
|
+
expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("registers new tools under the same name again (Fast Refresh)", async () => {
|
|
48
|
+
xray.registerReactToolbox = jest.fn().mockResolvedValue(undefined);
|
|
49
|
+
const { registerXrayToolbox } = load();
|
|
50
|
+
|
|
51
|
+
await registerXrayToolbox(toolbox);
|
|
52
|
+
await registerXrayToolbox({ ...toolbox, tools: [] });
|
|
53
|
+
|
|
54
|
+
expect(xray.registerReactToolbox).toHaveBeenCalledTimes(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("stops trying every toolbox once the bridge is missing", async () => {
|
|
58
|
+
xray.registerReactToolbox = jest.fn().mockRejectedValue(new Error("no"));
|
|
59
|
+
const { registerXrayToolbox } = load();
|
|
60
|
+
|
|
61
|
+
await registerXrayToolbox(toolbox);
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
registerXrayToolbox({ ...toolbox, name: "other" })
|
|
65
|
+
).resolves.toBe(false);
|
|
66
|
+
|
|
67
|
+
expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getXray } from ".";
|
|
2
|
+
|
|
3
|
+
/** One tool of an X-Ray React toolbox: an agent calls it by name with JSON arguments. */
|
|
4
|
+
export type XrayToolSpec = {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
parameters?: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
type: "number" | "string";
|
|
10
|
+
description: string;
|
|
11
|
+
}>;
|
|
12
|
+
handler: (args: Record<string, unknown>) => unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type XrayToolbox = {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
tools: XrayToolSpec[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type RegisterReactToolbox = (toolbox: XrayToolbox) => Promise<void>;
|
|
22
|
+
|
|
23
|
+
const registered = new Map<
|
|
24
|
+
string,
|
|
25
|
+
{ tools: XrayToolSpec[]; registration: Promise<boolean> }
|
|
26
|
+
>();
|
|
27
|
+
|
|
28
|
+
/** Set once a registration fails: the build has no X-Ray bridge, which cannot appear later. */
|
|
29
|
+
let bridgeMissing = false;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Registers an X-Ray React toolbox and resolves whether it is registered. The same tools are
|
|
33
|
+
* registered once; new tools under the same name (Fast Refresh re-ran the module) replace them.
|
|
34
|
+
* A build without the X-Ray native bridge (release builds) rejects the first attempt and every
|
|
35
|
+
* later call resolves false at once, so callers on hot paths pay for one attempt only.
|
|
36
|
+
*/
|
|
37
|
+
export const registerXrayToolbox = (toolbox: XrayToolbox): Promise<boolean> => {
|
|
38
|
+
const cached = registered.get(toolbox.name);
|
|
39
|
+
|
|
40
|
+
if (cached?.tools === toolbox.tools) return cached.registration;
|
|
41
|
+
|
|
42
|
+
if (bridgeMissing) return Promise.resolve(false);
|
|
43
|
+
|
|
44
|
+
const register = (
|
|
45
|
+
getXray() as
|
|
46
|
+
| { registerReactToolbox?: RegisterReactToolbox }
|
|
47
|
+
| null
|
|
48
|
+
| undefined
|
|
49
|
+
)?.registerReactToolbox;
|
|
50
|
+
|
|
51
|
+
const registration = register
|
|
52
|
+
? register(toolbox).then(
|
|
53
|
+
() => true,
|
|
54
|
+
() => {
|
|
55
|
+
bridgeMissing = true;
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
: Promise.resolve(false);
|
|
61
|
+
|
|
62
|
+
if (!register) bridgeMissing = true;
|
|
63
|
+
|
|
64
|
+
registered.set(toolbox.name, { tools: toolbox.tools, registration });
|
|
65
|
+
|
|
66
|
+
return registration;
|
|
67
|
+
};
|
|
@@ -135,7 +135,8 @@ function generateFieldsFromDefaults(label, defaults, fields) {
|
|
|
135
135
|
function generateFieldsFromDefaultsWithoutPrefixedLabel(
|
|
136
136
|
label,
|
|
137
137
|
defaults,
|
|
138
|
-
fields
|
|
138
|
+
fields,
|
|
139
|
+
{ withStyle = false } = {}
|
|
139
140
|
) {
|
|
140
141
|
const keyPrefix = toSnakeCase(label);
|
|
141
142
|
|
|
@@ -151,7 +152,9 @@ function generateFieldsFromDefaultsWithoutPrefixedLabel(
|
|
|
151
152
|
const generatedField = {
|
|
152
153
|
...fieldProps,
|
|
153
154
|
label: capitalizeFirstLetter(field.suffix),
|
|
154
|
-
key:
|
|
155
|
+
key: withStyle
|
|
156
|
+
? `${keyPrefix}_style_${toSnakeCase(field.suffix)}`
|
|
157
|
+
: `${keyPrefix}_${toSnakeCase(field.suffix)}`,
|
|
155
158
|
initial_value: R.when(R.is(Array), R.head)(initialValue),
|
|
156
159
|
};
|
|
157
160
|
|
|
@@ -164,7 +167,9 @@ function generateFieldsFromDefaultsWithoutPrefixedLabel(
|
|
|
164
167
|
const section = condition.section ? `${condition.section}/` : "";
|
|
165
168
|
|
|
166
169
|
return {
|
|
167
|
-
key:
|
|
170
|
+
key: withStyle
|
|
171
|
+
? `${section}${keyPrefix}_style_${toSnakeCase(condition.key)}`
|
|
172
|
+
: `${section}${keyPrefix}_${toSnakeCase(condition.key)}`,
|
|
168
173
|
condition_value: condition.value,
|
|
169
174
|
};
|
|
170
175
|
});
|
|
@@ -204,6 +204,20 @@ function getPlayerConfiguration({ platform, version }) {
|
|
|
204
204
|
label_tooltip: "Label for close button accessibility",
|
|
205
205
|
type: "text_input",
|
|
206
206
|
},
|
|
207
|
+
{
|
|
208
|
+
key: "accessibility_overflow_label",
|
|
209
|
+
label: "Accessibility overflow label",
|
|
210
|
+
initial_value: "More actions",
|
|
211
|
+
label_tooltip: "Label for the overflow (…) button accessibility",
|
|
212
|
+
type: "text_input",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
key: "accessibility_overflow_hint",
|
|
216
|
+
label: "Accessibility overflow hint",
|
|
217
|
+
initial_value: "Opens the remaining player actions",
|
|
218
|
+
label_tooltip: "Hint for the overflow (…) button accessibility",
|
|
219
|
+
type: "text_input",
|
|
220
|
+
},
|
|
207
221
|
{
|
|
208
222
|
key: "accessibility_close_hint",
|
|
209
223
|
label: "Accessibility close hint",
|
|
@@ -791,7 +805,7 @@ function getPlayerConfiguration({ platform, version }) {
|
|
|
791
805
|
label: "Player action buttons",
|
|
792
806
|
initial_value: null,
|
|
793
807
|
label_tooltip:
|
|
794
|
-
"
|
|
808
|
+
"Comma-separated action identifiers. The entry_actions token expands from the playing entry's extensions.entry_action. When more than 2 actions resolve, the video and fullscreen audio players show Overflow (…) plus one companion - the first action that did not come from the entry, otherwise the first one - and the Overflow sheet lists every resolved action. A repeated identifier is counted once. No extra plugin is required.",
|
|
795
809
|
type: "text_input",
|
|
796
810
|
},
|
|
797
811
|
{
|
|
@@ -816,6 +830,14 @@ function getPlayerConfiguration({ platform, version }) {
|
|
|
816
830
|
type: "switch",
|
|
817
831
|
label_tooltip:
|
|
818
832
|
"Enable this option to have a floating video player when the app is in the background.",
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
key: "vertical_video_player",
|
|
836
|
+
label: "Vertical video mode",
|
|
837
|
+
initial_value: false,
|
|
838
|
+
type: "switch",
|
|
839
|
+
label_tooltip:
|
|
840
|
+
"Enable this option to use the vertical video player layout.",
|
|
819
841
|
}
|
|
820
842
|
);
|
|
821
843
|
|
|
@@ -1091,6 +1113,13 @@ function getPlayerConfiguration({ platform, version }) {
|
|
|
1091
1113
|
type: "uploader",
|
|
1092
1114
|
label_tooltip: "Override default close badge / icon",
|
|
1093
1115
|
},
|
|
1116
|
+
{
|
|
1117
|
+
key: "overflow",
|
|
1118
|
+
label: "Overflow badge",
|
|
1119
|
+
type: "uploader",
|
|
1120
|
+
label_tooltip:
|
|
1121
|
+
"Override default overflow (…) badge / icon, shown when more action buttons resolve than fit on the player overlay",
|
|
1122
|
+
},
|
|
1094
1123
|
{
|
|
1095
1124
|
key: "lock",
|
|
1096
1125
|
label: "Lock player badge",
|
package/manifestUtils/keys.js
CHANGED
|
@@ -412,6 +412,12 @@ const mobileActionButtonContainerFields = (positionOptions) => [
|
|
|
412
412
|
type: ZAPPIFEST_FIELDS.switch,
|
|
413
413
|
suffix: "buttons enabled",
|
|
414
414
|
},
|
|
415
|
+
{
|
|
416
|
+
type: ZAPPIFEST_FIELDS.switch,
|
|
417
|
+
suffix: "cell navigation enabled",
|
|
418
|
+
label_tooltip:
|
|
419
|
+
"Controls whether tapping on the cell (outside the buttons) triggers its navigation behavior. When disabled, only the configured buttons are interactive. Useful for Home screens (tap anywhere to open) vs. Details screens (only buttons should work).",
|
|
420
|
+
},
|
|
415
421
|
{
|
|
416
422
|
type: ZAPPIFEST_FIELDS.select,
|
|
417
423
|
suffix: "position",
|
|
@@ -472,7 +478,6 @@ const MOBILE_ACTION_BUTTON_FIELDS = [
|
|
|
472
478
|
suffix: "assign action",
|
|
473
479
|
options: [
|
|
474
480
|
{ text: "primary navigation", value: "navigation_action" },
|
|
475
|
-
{ text: "secondary navigation", value: "secondary_navigation" },
|
|
476
481
|
{ text: "favorite", value: "local_storage_favourites_action" },
|
|
477
482
|
{ text: "more", value: "open-modal-bottom-sheet-cell-action" },
|
|
478
483
|
{ text: "add to calendar", value: "add_to_calendar_action" },
|
|
@@ -7,6 +7,7 @@ describe("mobileActionButtonsContainer", () => {
|
|
|
7
7
|
description: "container",
|
|
8
8
|
defaults: {
|
|
9
9
|
buttonsEnabled: false,
|
|
10
|
+
cellNavigationEnabled: false,
|
|
10
11
|
position: ["over_image", "text_label_1", "text_label_2"],
|
|
11
12
|
align: "left",
|
|
12
13
|
overImagePosition: "bottom_left",
|
|
@@ -31,6 +32,26 @@ describe("mobileActionButtonsContainer", () => {
|
|
|
31
32
|
expect(enabledField).toBeTruthy();
|
|
32
33
|
expect(enabledField.type).toBe("switch");
|
|
33
34
|
|
|
35
|
+
const cellNavigationField = result.fields.find(
|
|
36
|
+
(field) =>
|
|
37
|
+
field.key === "mobile_buttons_container_cell_navigation_enabled"
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(cellNavigationField).toMatchObject({
|
|
41
|
+
type: "switch",
|
|
42
|
+
label: "Cell navigation enabled",
|
|
43
|
+
initial_value: false,
|
|
44
|
+
label_tooltip: expect.stringContaining(
|
|
45
|
+
"Controls whether tapping on the cell"
|
|
46
|
+
),
|
|
47
|
+
conditional_fields: [
|
|
48
|
+
{
|
|
49
|
+
key: "styles/mobile_buttons_container_buttons_enabled",
|
|
50
|
+
condition_value: true,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
34
55
|
const positionField = result.fields.find(
|
|
35
56
|
(field) => field.key === "mobile_buttons_container_position"
|
|
36
57
|
);
|
|
@@ -67,7 +67,7 @@ describe("buildMobileActionButtonGroups", () => {
|
|
|
67
67
|
);
|
|
68
68
|
|
|
69
69
|
expect(button2EnabledField.initial_value).toBe(false);
|
|
70
|
-
expect(button2AssignActionField.initial_value).toBe("
|
|
70
|
+
expect(button2AssignActionField.initial_value).toBe("trailer_action");
|
|
71
71
|
|
|
72
72
|
const button3Fields = groups[3].fields;
|
|
73
73
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const DEFAULT_MOBILE_ACTION_BUTTONS_CONTAINER_DEFAULTS = {
|
|
2
2
|
buttonsEnabled: false,
|
|
3
|
+
cellNavigationEnabled: false,
|
|
3
4
|
|
|
4
5
|
align: "left",
|
|
5
6
|
overImagePosition: "bottom_left",
|
|
@@ -63,7 +64,7 @@ const DEFAULT_MOBILE_ACTION_BUTTON_PRESETS = {
|
|
|
63
64
|
},
|
|
64
65
|
2: {
|
|
65
66
|
buttonEnabled: false,
|
|
66
|
-
assignAction: "
|
|
67
|
+
assignAction: "trailer_action",
|
|
67
68
|
},
|
|
68
69
|
3: {
|
|
69
70
|
buttonEnabled: false,
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { dismissModal, modalStore, openBottomSheetModal } from "./store";
|
|
2
|
-
import {
|
|
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.
|
|
@@ -38,6 +44,7 @@ function menuToModalArgs(
|
|
|
38
44
|
title: menu.header?.title ?? menu.content.title,
|
|
39
45
|
summary: menu.header?.subtitle,
|
|
40
46
|
styleOverrides: menu.content.styleOverrides,
|
|
47
|
+
emptyMessage: menu.content.emptyMessage,
|
|
41
48
|
onPress: onItemPress,
|
|
42
49
|
viewModel,
|
|
43
50
|
},
|
|
@@ -136,15 +143,21 @@ class ModalOrchestrator {
|
|
|
136
143
|
private _buildOnPress(
|
|
137
144
|
onItemPress?: (item: MenuItem) => void | Promise<void>
|
|
138
145
|
): (item: MenuItem) => void | Promise<void> {
|
|
139
|
-
return (item: MenuItem) => {
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
this.open(item.action.menu, onItemPress);
|
|
143
|
-
|
|
144
|
-
return;
|
|
146
|
+
return async (item: MenuItem) => {
|
|
147
|
+
if (onItemPress) {
|
|
148
|
+
await onItemPress(item);
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
|
|
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
|
+
}
|
|
148
161
|
};
|
|
149
162
|
}
|
|
150
163
|
|
|
@@ -123,7 +123,14 @@ export class RemoteEditableCollection implements EditableCollection {
|
|
|
123
123
|
|
|
124
124
|
try {
|
|
125
125
|
for (const action of addEvents) {
|
|
126
|
-
|
|
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
|
+
}
|
|
127
134
|
}
|
|
128
135
|
} catch (e) {
|
|
129
136
|
actionFailed = true;
|
|
@@ -219,6 +226,7 @@ export class RemoteEditableCollection implements EditableCollection {
|
|
|
219
226
|
await actionExecutor.handleAction(
|
|
220
227
|
actionToExecute as any,
|
|
221
228
|
{
|
|
229
|
+
...this.content.context,
|
|
222
230
|
entry: item as any,
|
|
223
231
|
} as any
|
|
224
232
|
);
|
|
@@ -44,4 +44,20 @@ describe("ModalOrchestrator phase 3", () => {
|
|
|
44
44
|
expect(props.title).toBe("Sleep Timer");
|
|
45
45
|
expect(props.summary).toBe("remaining");
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
it("forwards the queue empty message onto the sheet props", () => {
|
|
49
|
+
modalOrchestrator.open({
|
|
50
|
+
header: { title: "Queue" },
|
|
51
|
+
content: {
|
|
52
|
+
title: "Queue",
|
|
53
|
+
items: [],
|
|
54
|
+
emptyMessage: "Queued items will be added here",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const props = (modalStore.getState().modalState.screen as any)
|
|
59
|
+
?.modalBottomSheetContentProps;
|
|
60
|
+
|
|
61
|
+
expect(props.emptyMessage).toBe("Queued items will be added here");
|
|
62
|
+
});
|
|
47
63
|
});
|
|
@@ -34,16 +34,26 @@ const itemPlain: MenuItem = { id: "2", title: "B" };
|
|
|
34
34
|
function makeCollection(
|
|
35
35
|
items: MenuItem[],
|
|
36
36
|
refetch = jest.fn().mockResolvedValue(undefined),
|
|
37
|
-
operations = ["remove", "reorder"]
|
|
37
|
+
operations = ["remove", "reorder"],
|
|
38
|
+
context?: Record<string, unknown>
|
|
38
39
|
) {
|
|
39
40
|
const content = {
|
|
40
41
|
items,
|
|
42
|
+
context,
|
|
41
43
|
dynamicCollection: { operations, postUrl: "https://x/cloud-events" },
|
|
42
44
|
} as unknown as Content;
|
|
43
45
|
|
|
44
46
|
return { col: new RemoteEditableCollection(content, refetch), refetch };
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
const openingContext = {
|
|
50
|
+
component: {
|
|
51
|
+
id: "list-1",
|
|
52
|
+
data: { source: "https://example.com/user/collections/abc" },
|
|
53
|
+
},
|
|
54
|
+
screenData: { id: "playlist-screen" },
|
|
55
|
+
};
|
|
56
|
+
|
|
47
57
|
describe("RemoteEditableCollection", () => {
|
|
48
58
|
beforeEach(() => jest.clearAllMocks());
|
|
49
59
|
|
|
@@ -118,6 +128,24 @@ describe("RemoteEditableCollection", () => {
|
|
|
118
128
|
expect(refetch).toHaveBeenCalledTimes(1);
|
|
119
129
|
expect(currentItems(col)).toEqual([itemWithRemove]);
|
|
120
130
|
});
|
|
131
|
+
|
|
132
|
+
it("forwards stored sheet context plus the removed item as entry", async () => {
|
|
133
|
+
handleAction.mockResolvedValue("success");
|
|
134
|
+
|
|
135
|
+
const { col } = makeCollection(
|
|
136
|
+
[itemWithRemove, itemPlain],
|
|
137
|
+
jest.fn().mockResolvedValue(undefined),
|
|
138
|
+
["remove", "reorder"],
|
|
139
|
+
openingContext
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await col.remove(0);
|
|
143
|
+
|
|
144
|
+
expect(handleAction).toHaveBeenCalledWith(removeAction, {
|
|
145
|
+
...openingContext,
|
|
146
|
+
entry: itemWithRemove,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
121
149
|
});
|
|
122
150
|
|
|
123
151
|
describe("move", () => {
|
|
@@ -164,6 +192,35 @@ describe("RemoteEditableCollection", () => {
|
|
|
164
192
|
expect(refetch).toHaveBeenCalledTimes(1);
|
|
165
193
|
});
|
|
166
194
|
|
|
195
|
+
it("forwards stored sheet context plus the moved item as entry", async () => {
|
|
196
|
+
handleAction.mockResolvedValue("success");
|
|
197
|
+
|
|
198
|
+
const itemA: MenuItem = {
|
|
199
|
+
id: "2",
|
|
200
|
+
title: "B",
|
|
201
|
+
entryActions: [
|
|
202
|
+
{ button: { alias: "reorder_item" }, actions: [removeAction] },
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const { col } = makeCollection(
|
|
207
|
+
[itemA],
|
|
208
|
+
jest.fn().mockResolvedValue(undefined),
|
|
209
|
+
["remove", "reorder"],
|
|
210
|
+
openingContext
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
await col.move(["2"]);
|
|
214
|
+
|
|
215
|
+
expect(handleAction).toHaveBeenCalledWith(
|
|
216
|
+
expect.objectContaining({ type: "sendCloudEvent" }),
|
|
217
|
+
{
|
|
218
|
+
...openingContext,
|
|
219
|
+
entry: itemA,
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
167
224
|
it("rolls back on failure", async () => {
|
|
168
225
|
handleAction.mockRejectedValue(new Error("nope"));
|
|
169
226
|
|
|
@@ -322,5 +379,37 @@ describe("RemoteEditableCollection", () => {
|
|
|
322
379
|
expect(handleAction).toHaveBeenCalledWith(addAction);
|
|
323
380
|
expect(refetch).toHaveBeenCalledTimes(1);
|
|
324
381
|
});
|
|
382
|
+
|
|
383
|
+
it("forwards stored sheet context into events.add actions", async () => {
|
|
384
|
+
handleAction.mockResolvedValue("success");
|
|
385
|
+
|
|
386
|
+
const addAction = {
|
|
387
|
+
type: "showTextInput",
|
|
388
|
+
options: {
|
|
389
|
+
headerTitle: "Create Playlist",
|
|
390
|
+
buttonLabel: "Create",
|
|
391
|
+
actions: [{ type: "sendCloudEvent" }],
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const content = {
|
|
396
|
+
items: [],
|
|
397
|
+
context: openingContext,
|
|
398
|
+
dynamicCollection: {
|
|
399
|
+
operations: ["add"],
|
|
400
|
+
postUrl: "https://x/cloud-events",
|
|
401
|
+
events: {
|
|
402
|
+
add: [addAction],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
} as any;
|
|
406
|
+
|
|
407
|
+
const refetch = jest.fn().mockResolvedValue(undefined);
|
|
408
|
+
const col = new RemoteEditableCollection(content, refetch);
|
|
409
|
+
|
|
410
|
+
await col.add();
|
|
411
|
+
|
|
412
|
+
expect(handleAction).toHaveBeenCalledWith(addAction, openingContext);
|
|
413
|
+
});
|
|
325
414
|
});
|
|
326
415
|
});
|
|
@@ -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
|
});
|