@applicaster/zapp-react-native-utils 16.0.0-alpha.1263110541 → 16.0.0-alpha.1334507601
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actionsExecutor/ActionExecutor.ts +10 -1
- package/actionsExecutor/ActionExecutorContext.tsx +13 -1
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +3 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +113 -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/index.ts +4 -0
- package/actionsExecutor/actions/openBottomSheet.ts +20 -11
- 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 +2 -0
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +102 -1
- package/configurationUtils/manifestKeyParser.ts +12 -1
- package/configurationUtils/modalBlocksParser.ts +26 -4
- package/entryActions/__tests__/buildEntryActions.test.ts +97 -18
- package/entryActions/aliasPresentation.ts +26 -42
- package/entryActions/buildEntryActions.ts +1 -0
- package/entryActions/index.ts +0 -2
- package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
- package/modalState/ModalOrchestrator.ts +21 -9
- package/modalState/RemoteEditableCollection.ts +9 -1
- 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 +29 -2
- package/modalState/useBottomSheetContent.ts +24 -17
- package/package.json +2 -2
- package/reactHooks/cell-click/__tests__/index.test.js +64 -0
- package/reactHooks/cell-click/index.ts +21 -14
- package/reactHooks/navigation/__tests__/index.test.tsx +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
|
@@ -56,7 +56,6 @@ export class ActionExecutor {
|
|
|
56
56
|
context?: ActionExecutionContext
|
|
57
57
|
): Promise<ActionResult> => {
|
|
58
58
|
for (const action of actionTypes) {
|
|
59
|
-
// TODO: Handle action result error
|
|
60
59
|
const result = await this.handleAction(action, context);
|
|
61
60
|
|
|
62
61
|
if (result === ActionResult.Cancel) {
|
|
@@ -64,6 +63,16 @@ export class ActionExecutor {
|
|
|
64
63
|
|
|
65
64
|
return result;
|
|
66
65
|
}
|
|
66
|
+
|
|
67
|
+
if (result === ActionResult.Error) {
|
|
68
|
+
// Everything after a failed step was written for a state that never
|
|
69
|
+
// happened — the event was refused, the screen never opened — so
|
|
70
|
+
// running it is how a chain ends up telling the user their mail was
|
|
71
|
+
// sent when it was not.
|
|
72
|
+
log_error(`handleActions: action: ${action.type} failed`, { action });
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
return ActionResult.Success;
|
|
@@ -9,7 +9,7 @@ import { ActionExecutionContext, ActionHandler } from "./types";
|
|
|
9
9
|
import { batchRemoveAllFromNamespaceForStorage } from "../zappFrameworkUtils/localStorageHelper";
|
|
10
10
|
import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
|
|
11
11
|
|
|
12
|
-
import { useNavigation, useRivers } from "../reactHooks";
|
|
12
|
+
import { useNavigation, usePresentScreen, useRivers } from "../reactHooks";
|
|
13
13
|
import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
|
|
14
14
|
import { useSubscriberFor } from "../reactHooks/useSubscriberFor";
|
|
15
15
|
import { APP_EVENTS } from "../appUtils/events";
|
|
@@ -18,6 +18,7 @@ import { createLogger } from "../logger";
|
|
|
18
18
|
import {
|
|
19
19
|
appRestartAction,
|
|
20
20
|
confirmDialogAction,
|
|
21
|
+
showAlertAction,
|
|
21
22
|
createGoBackAction,
|
|
22
23
|
createGoHomeAction,
|
|
23
24
|
createNavigateToScreenAction,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
localStorageRemoveAction,
|
|
26
27
|
localStorageSetAction,
|
|
27
28
|
localStorageToggleFlagAction,
|
|
29
|
+
createPresentScreenAction,
|
|
28
30
|
openBottomSheetAction,
|
|
29
31
|
refreshComponentAction,
|
|
30
32
|
screenSetVariableAction,
|
|
@@ -109,6 +111,8 @@ const prepareDefaultActions = (actionExecutor) => {
|
|
|
109
111
|
confirmDialogAction
|
|
110
112
|
);
|
|
111
113
|
|
|
114
|
+
actionExecutor.registerAction(ACTION_TYPES.SHOW_ALERT, showAlertAction);
|
|
115
|
+
|
|
112
116
|
actionExecutor.registerAction(
|
|
113
117
|
ACTION_TYPES.SEND_CLOUD_EVENT,
|
|
114
118
|
sendCloudEventAction
|
|
@@ -157,6 +161,7 @@ export function withActionExecutor(Component) {
|
|
|
157
161
|
const navigator = useNavigation();
|
|
158
162
|
const rivers = useRivers();
|
|
159
163
|
const contentTypes = useContentTypes();
|
|
164
|
+
const presentScreen = usePresentScreen();
|
|
160
165
|
|
|
161
166
|
const handlers = useMemo(() => {
|
|
162
167
|
return {
|
|
@@ -191,6 +196,13 @@ export function withActionExecutor(Component) {
|
|
|
191
196
|
);
|
|
192
197
|
}, [navigator, rivers, contentTypes]);
|
|
193
198
|
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
return _actionExecutor.registerAction(
|
|
201
|
+
ACTION_TYPES.PRESENT_SCREEN,
|
|
202
|
+
createPresentScreenAction(presentScreen)
|
|
203
|
+
);
|
|
204
|
+
}, [presentScreen]);
|
|
205
|
+
|
|
194
206
|
useEffect(() => {
|
|
195
207
|
return _actionExecutor.registerAction(
|
|
196
208
|
ACTION_TYPES.GO_BACK,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ActionExecutor, ActionResult } from "../ActionExecutor";
|
|
2
|
+
|
|
3
|
+
jest.mock("../../logger", () => ({
|
|
4
|
+
createLogger: () => ({
|
|
5
|
+
log_error: jest.fn(),
|
|
6
|
+
log_debug: jest.fn(),
|
|
7
|
+
log_info: jest.fn(),
|
|
8
|
+
}),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("handleActions", () => {
|
|
12
|
+
const executor = () => {
|
|
13
|
+
const ran: string[] = [];
|
|
14
|
+
const actionExecutor = new ActionExecutor();
|
|
15
|
+
|
|
16
|
+
const register = (type: string, result?: ActionResult) =>
|
|
17
|
+
actionExecutor.registerAction(type, async () => {
|
|
18
|
+
ran.push(type);
|
|
19
|
+
|
|
20
|
+
return result as ActionResult;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
register("ok", ActionResult.Success);
|
|
24
|
+
register("fails", ActionResult.Error);
|
|
25
|
+
register("cancels", ActionResult.Cancel);
|
|
26
|
+
register("recover", ActionResult.Success);
|
|
27
|
+
register("after", ActionResult.Success);
|
|
28
|
+
|
|
29
|
+
return { actionExecutor, ran };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Everything after a failed step was written for a state that never
|
|
33
|
+
// happened: the event was refused, the screen never opened.
|
|
34
|
+
it("stops when an action fails", async () => {
|
|
35
|
+
const { actionExecutor, ran } = executor();
|
|
36
|
+
|
|
37
|
+
const result = await actionExecutor.handleActions([
|
|
38
|
+
{ type: "fails" },
|
|
39
|
+
{ type: "after" },
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
expect(ran).toEqual(["fails"]);
|
|
43
|
+
expect(result).toBe(ActionResult.Error);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("still stops on a cancel", async () => {
|
|
47
|
+
const { actionExecutor, ran } = executor();
|
|
48
|
+
|
|
49
|
+
const result = await actionExecutor.handleActions([
|
|
50
|
+
{ type: "cancels" },
|
|
51
|
+
{ type: "after" },
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
expect(ran).toEqual(["cancels"]);
|
|
55
|
+
expect(result).toBe(ActionResult.Cancel);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// An action nobody registered never ran, which is a misconfigured feed
|
|
59
|
+
// rather than a step that failed. It is stepped over, as it always was.
|
|
60
|
+
it("steps over an action with no handler", async () => {
|
|
61
|
+
const { actionExecutor, ran } = executor();
|
|
62
|
+
|
|
63
|
+
const result = await actionExecutor.handleActions([
|
|
64
|
+
{ type: "nobody-registered-this" },
|
|
65
|
+
{ type: "after" },
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
expect(ran).toEqual(["after"]);
|
|
69
|
+
expect(result).toBe(ActionResult.Success);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createPresentScreenAction } from "../presentScreen";
|
|
2
|
+
import { ActionResult } from "../../ActionExecutor";
|
|
3
|
+
|
|
4
|
+
const contextEntry = { id: "context-entry" } as any;
|
|
5
|
+
|
|
6
|
+
const succeeded = { success: true, cancelled: false };
|
|
7
|
+
|
|
8
|
+
const cancelled = { success: false, cancelled: true };
|
|
9
|
+
|
|
10
|
+
describe("presentScreenAction", () => {
|
|
11
|
+
let presentScreen: jest.Mock;
|
|
12
|
+
let presentScreenAction: ReturnType<typeof createPresentScreenAction>;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
presentScreen = jest.fn().mockResolvedValue(succeeded);
|
|
16
|
+
presentScreenAction = createPresentScreenAction(presentScreen);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("presents the screen named by typeMapping", async () => {
|
|
20
|
+
await presentScreenAction({
|
|
21
|
+
type: "presentScreen",
|
|
22
|
+
options: { typeMapping: "parent_lock" },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(presentScreen).toHaveBeenCalledWith({
|
|
26
|
+
typeMapping: "parent_lock",
|
|
27
|
+
entry: undefined,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("fails without presenting anything when typeMapping is missing", async () => {
|
|
32
|
+
const result = await presentScreenAction({
|
|
33
|
+
type: "presentScreen",
|
|
34
|
+
options: {},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(result).toBe(ActionResult.Error);
|
|
38
|
+
expect(presentScreen).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("resolves the '@{entry/}' token to the entry from the execution context", async () => {
|
|
42
|
+
await presentScreenAction(
|
|
43
|
+
{
|
|
44
|
+
type: "presentScreen",
|
|
45
|
+
options: { typeMapping: "parent_lock", entry: "@{entry/}" },
|
|
46
|
+
},
|
|
47
|
+
{ entry: contextEntry }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(presentScreen).toHaveBeenCalledWith({
|
|
51
|
+
typeMapping: "parent_lock",
|
|
52
|
+
entry: contextEntry,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("presents an entry supplied inline in the options", async () => {
|
|
57
|
+
const inlineEntry = { id: "inline-entry" } as any;
|
|
58
|
+
|
|
59
|
+
await presentScreenAction({
|
|
60
|
+
type: "presentScreen",
|
|
61
|
+
options: { typeMapping: "parent_lock", entry: inlineEntry },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(presentScreen).toHaveBeenCalledWith({
|
|
65
|
+
typeMapping: "parent_lock",
|
|
66
|
+
entry: inlineEntry,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("fails without presenting anything when the entry option is not an object", async () => {
|
|
71
|
+
const result = await presentScreenAction({
|
|
72
|
+
type: "presentScreen",
|
|
73
|
+
options: { typeMapping: "parent_lock", entry: "not-an-entry" as any },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(result).toBe(ActionResult.Error);
|
|
77
|
+
expect(presentScreen).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("succeeds when the presented screen reports success", async () => {
|
|
81
|
+
presentScreen.mockResolvedValue(succeeded);
|
|
82
|
+
|
|
83
|
+
const result = await presentScreenAction({
|
|
84
|
+
type: "presentScreen",
|
|
85
|
+
options: { typeMapping: "parent_lock" },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result).toBe(ActionResult.Success);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("cancels the action chain when the presented screen is cancelled", async () => {
|
|
92
|
+
presentScreen.mockResolvedValue(cancelled);
|
|
93
|
+
|
|
94
|
+
const result = await presentScreenAction({
|
|
95
|
+
type: "presentScreen",
|
|
96
|
+
options: { typeMapping: "parent_lock" },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result).toBe(ActionResult.Cancel);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("cancels the action chain when the presented screen reports failure", async () => {
|
|
103
|
+
presentScreen.mockResolvedValue({ success: false, cancelled: false });
|
|
104
|
+
|
|
105
|
+
const result = await presentScreenAction({
|
|
106
|
+
type: "presentScreen",
|
|
107
|
+
options: { typeMapping: "parent_lock" },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(result).toBe(ActionResult.Cancel);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("errors when the screen could not be presented at all", async () => {
|
|
114
|
+
presentScreen.mockResolvedValue({
|
|
115
|
+
success: false,
|
|
116
|
+
cancelled: false,
|
|
117
|
+
error: "can not resolve screen type mapping: parent_lock",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await presentScreenAction({
|
|
121
|
+
type: "presentScreen",
|
|
122
|
+
options: { typeMapping: "parent_lock" },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result).toBe(ActionResult.Error);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { showAlertAction } from "../showAlert";
|
|
2
|
+
import { ActionResult } from "../../ActionExecutor";
|
|
3
|
+
import { showAlertDialog } from "../../../alertUtils";
|
|
4
|
+
|
|
5
|
+
jest.mock("../../../alertUtils", () => ({
|
|
6
|
+
showAlertDialog: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const dialog = showAlertDialog as jest.Mock;
|
|
10
|
+
|
|
11
|
+
const dismiss = () => dialog.mock.calls[0][0].completion();
|
|
12
|
+
|
|
13
|
+
describe("showAlert", () => {
|
|
14
|
+
beforeEach(() => dialog.mockReset());
|
|
15
|
+
|
|
16
|
+
it("shows the notice and resolves once it is dismissed", async () => {
|
|
17
|
+
const result = showAlertAction({
|
|
18
|
+
type: "showAlert",
|
|
19
|
+
options: { title: "Locked", message: "Ask the account owner." },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(dialog).toHaveBeenCalledWith(
|
|
23
|
+
expect.objectContaining({
|
|
24
|
+
title: "Locked",
|
|
25
|
+
message: "Ask the account owner.",
|
|
26
|
+
okButtonText: "OK",
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
dismiss();
|
|
31
|
+
|
|
32
|
+
// Dismissing a notice is not a cancellation: the chain carries on.
|
|
33
|
+
await expect(result).resolves.toBe(ActionResult.Success);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("takes the dismiss label from the action", async () => {
|
|
37
|
+
const result = showAlertAction({
|
|
38
|
+
type: "showAlert",
|
|
39
|
+
options: { title: "Locked", okButtonText: "Got it" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(dialog).toHaveBeenCalledWith(
|
|
43
|
+
expect.objectContaining({ okButtonText: "Got it" })
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
dismiss();
|
|
47
|
+
await result;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -31,7 +31,7 @@ describe("showToastAction", () => {
|
|
|
31
31
|
message: "Added to queue",
|
|
32
32
|
extraMessage: "Extra info",
|
|
33
33
|
style: undefined,
|
|
34
|
-
timeout:
|
|
34
|
+
timeout: 5000,
|
|
35
35
|
source: "confirmation",
|
|
36
36
|
},
|
|
37
37
|
]);
|
|
@@ -78,7 +78,7 @@ describe("showToastAction", () => {
|
|
|
78
78
|
message: undefined,
|
|
79
79
|
extraMessage: undefined,
|
|
80
80
|
style: undefined,
|
|
81
|
-
timeout:
|
|
81
|
+
timeout: 5000,
|
|
82
82
|
source: "confirmation",
|
|
83
83
|
},
|
|
84
84
|
]);
|
|
@@ -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;
|
|
@@ -15,6 +15,8 @@ export { appRestartAction } from "./appRestart";
|
|
|
15
15
|
|
|
16
16
|
export { confirmDialogAction } from "./confirmDialog";
|
|
17
17
|
|
|
18
|
+
export { showAlertAction } from "./showAlert";
|
|
19
|
+
|
|
18
20
|
export { sendCloudEventAction } from "./sendCloudEvent";
|
|
19
21
|
|
|
20
22
|
export { sessionStorageToggleFlagAction } from "./sessionStorageToggleFlag";
|
|
@@ -27,6 +29,8 @@ export { screenToggleFlagAction } from "./screenToggleFlag";
|
|
|
27
29
|
|
|
28
30
|
export { createNavigateToScreenAction } from "./navigateToScreen";
|
|
29
31
|
|
|
32
|
+
export { createPresentScreenAction } from "./presentScreen";
|
|
33
|
+
|
|
30
34
|
export { createGoBackAction } from "./goBack";
|
|
31
35
|
|
|
32
36
|
export { createGoHomeAction } from "./goHome";
|
|
@@ -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
|
}
|
|
@@ -206,7 +208,10 @@ export async function openBottomSheetAction(
|
|
|
206
208
|
items: [],
|
|
207
209
|
onPress: () => {},
|
|
208
210
|
contentComponent: content.component,
|
|
209
|
-
contentComponentProps:
|
|
211
|
+
contentComponentProps: {
|
|
212
|
+
...(content.props || {}),
|
|
213
|
+
actionContext: context,
|
|
214
|
+
},
|
|
210
215
|
},
|
|
211
216
|
});
|
|
212
217
|
|
|
@@ -275,6 +280,7 @@ export async function openBottomSheetAction(
|
|
|
275
280
|
dynamicCollection,
|
|
276
281
|
themePluginId,
|
|
277
282
|
styleOverrides: styleOverrides ?? content?.styleOverrides,
|
|
283
|
+
context,
|
|
278
284
|
},
|
|
279
285
|
};
|
|
280
286
|
|
|
@@ -288,18 +294,21 @@ export async function openBottomSheetAction(
|
|
|
288
294
|
* reference it.
|
|
289
295
|
*/
|
|
290
296
|
const onItemPress = async (item: MenuItem): Promise<void> => {
|
|
291
|
-
|
|
292
|
-
// Submenu navigation is handled internally by the orchestrator
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
297
|
+
const actions = getMenuItemActions(item);
|
|
295
298
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
{
|
|
299
|
-
...context,
|
|
300
|
-
entry: item as any,
|
|
301
|
-
} as any
|
|
299
|
+
const executableActions = actions.filter(
|
|
300
|
+
(action) => !isPresentSubMenuAction(action)
|
|
302
301
|
);
|
|
302
|
+
|
|
303
|
+
if (executableActions.length > 0) {
|
|
304
|
+
await actionExecutor.handleActions(
|
|
305
|
+
executableActions as ActionType[],
|
|
306
|
+
{
|
|
307
|
+
...context,
|
|
308
|
+
entry: item as any,
|
|
309
|
+
} as any
|
|
310
|
+
);
|
|
311
|
+
}
|
|
303
312
|
};
|
|
304
313
|
|
|
305
314
|
if (action.options?.replace) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/// <reference types="@applicaster/applicaster-types" />
|
|
2
|
+
import { ActionResult } from "../ActionExecutor";
|
|
3
|
+
import { ActionHandler, ActionExecutionContext } from "../types";
|
|
4
|
+
import type { PresentScreenFn } from "../../reactHooks/navigation/usePresentScreen";
|
|
5
|
+
import { createLogger } from "../../logger";
|
|
6
|
+
|
|
7
|
+
const { log_error } = createLogger({
|
|
8
|
+
subsystem: "ActionExecutorContext",
|
|
9
|
+
category: "General",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for the presentScreen action.
|
|
14
|
+
*/
|
|
15
|
+
export interface PresentScreenOptions {
|
|
16
|
+
/** The screen type to present (from content types mapping) */
|
|
17
|
+
typeMapping: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Entry to present the screen with. Can be:
|
|
21
|
+
* - "@{entry/}" to use the context entry
|
|
22
|
+
* - A ZappEntry object
|
|
23
|
+
* - undefined to present the screen without an entry
|
|
24
|
+
*/
|
|
25
|
+
entry?: "@{entry/}" | ZappEntry;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a present-screen action handler with the provided dependencies.
|
|
30
|
+
*
|
|
31
|
+
* Unlike navigateToScreen, this action does not return until the presented
|
|
32
|
+
* screen reports a result, so a screen that gates the flow (a parental lock,
|
|
33
|
+
* for example) can stop the remaining actions by being cancelled.
|
|
34
|
+
*
|
|
35
|
+
* @param presentScreen - Presents a screen as a modal and resolves with its result
|
|
36
|
+
* @returns An action handler that presents a screen and awaits its outcome
|
|
37
|
+
*/
|
|
38
|
+
export function createPresentScreenAction(
|
|
39
|
+
presentScreen: PresentScreenFn
|
|
40
|
+
): ActionHandler<PresentScreenOptions> {
|
|
41
|
+
return async function presentScreenAction(
|
|
42
|
+
action,
|
|
43
|
+
context?: ActionExecutionContext
|
|
44
|
+
): Promise<ActionResult> {
|
|
45
|
+
const typeMapping = action.options?.typeMapping;
|
|
46
|
+
|
|
47
|
+
if (!typeMapping) {
|
|
48
|
+
log_error("presentScreen: typeMapping option is missing");
|
|
49
|
+
|
|
50
|
+
return ActionResult.Error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const entrySource = action.options?.entry;
|
|
54
|
+
|
|
55
|
+
const entry = entrySource
|
|
56
|
+
? entrySource === "@{entry/}"
|
|
57
|
+
? context?.entry
|
|
58
|
+
: entrySource
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
if (entry && typeof entry !== "object") {
|
|
62
|
+
log_error(
|
|
63
|
+
`presentScreen: entry option is not an object, entry: ${entry}`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return ActionResult.Error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = await presentScreen({ typeMapping, entry });
|
|
70
|
+
|
|
71
|
+
if (result.error) {
|
|
72
|
+
return ActionResult.Error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Anything short of success stops the remaining actions in the chain.
|
|
76
|
+
return result.success ? ActionResult.Success : ActionResult.Cancel;
|
|
77
|
+
};
|
|
78
|
+
}
|