@applicaster/zapp-react-native-utils 16.0.0-rc.63 → 16.0.0-rc.64
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/__tests__/feedDecorator.test.ts +61 -0
- package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +133 -0
- package/actionsExecutor/actions/navigateToScreen.ts +18 -4
- package/actionsExecutor/feedDecorator.ts +6 -6
- package/cellUtils/index.ts +30 -3
- package/package.json +2 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { decorateFeed } from "../feedDecorator";
|
|
2
|
+
|
|
3
|
+
const preferenceFeed = (selectMode: string) =>
|
|
4
|
+
({
|
|
5
|
+
id: "agree-component",
|
|
6
|
+
title: "I agree to the terms",
|
|
7
|
+
type: { value: "feed" },
|
|
8
|
+
extensions: {
|
|
9
|
+
role: "preference_editor",
|
|
10
|
+
preference_editor_options: {
|
|
11
|
+
key: "agree",
|
|
12
|
+
scope: "screen",
|
|
13
|
+
select_mode: selectMode,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
entry: [
|
|
17
|
+
{
|
|
18
|
+
id: "I agree to the terms-agree",
|
|
19
|
+
title: "I agree to the terms",
|
|
20
|
+
type: { value: "action" },
|
|
21
|
+
extensions: { tag: "true" },
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
}) as unknown as ZappFeed;
|
|
25
|
+
|
|
26
|
+
describe("decorateFeed", () => {
|
|
27
|
+
it("decorates single select feeds with screenSetVariable", () => {
|
|
28
|
+
const decorated = decorateFeed(preferenceFeed("single"));
|
|
29
|
+
|
|
30
|
+
expect(decorated.extensions.behavior).toEqual(
|
|
31
|
+
expect.objectContaining({
|
|
32
|
+
select_mode: "single",
|
|
33
|
+
current_selection: "@{screen/agree}",
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(decorated.entry[0].extensions.tap_actions.actions[0].type).toBe(
|
|
38
|
+
"screenSetVariable"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("decorates multi select feeds with screenToggleFlag", () => {
|
|
43
|
+
const decorated = decorateFeed(preferenceFeed("multi"));
|
|
44
|
+
|
|
45
|
+
expect(decorated.extensions.behavior.select_mode).toBe("multi");
|
|
46
|
+
|
|
47
|
+
expect(decorated.entry[0].extensions.tap_actions.actions[0].type).toBe(
|
|
48
|
+
"screenToggleFlag"
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("leaves a feed without the preference_editor role untouched", () => {
|
|
53
|
+
const feed = preferenceFeed("single");
|
|
54
|
+
feed.extensions.role = undefined;
|
|
55
|
+
|
|
56
|
+
const decorated = decorateFeed(feed);
|
|
57
|
+
|
|
58
|
+
expect(decorated.extensions.behavior).toBeUndefined();
|
|
59
|
+
expect(decorated.entry[0].extensions.tap_actions).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createNavigateToScreenAction } from "../navigateToScreen";
|
|
2
|
+
import { ActionResult } from "../../ActionExecutor";
|
|
3
|
+
|
|
4
|
+
const river = { id: "screen-id", type: "parent-lock" } as unknown as ZappRiver;
|
|
5
|
+
|
|
6
|
+
const rivers = { "screen-id": river } as Record<string, ZappRiver>;
|
|
7
|
+
|
|
8
|
+
const contentTypes = {
|
|
9
|
+
parentLock: { screen_id: "screen-id" },
|
|
10
|
+
} as unknown as ZappContentTypes;
|
|
11
|
+
|
|
12
|
+
function createNavigator() {
|
|
13
|
+
return {
|
|
14
|
+
push: jest.fn(),
|
|
15
|
+
replace: jest.fn(),
|
|
16
|
+
replaceTop: jest.fn(),
|
|
17
|
+
} as unknown as QuickBrickAppNavigator;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("navigateToScreen navigationOptions", () => {
|
|
21
|
+
it("forwards navigationOptions to push when navigating to a screen", async () => {
|
|
22
|
+
const navigator = createNavigator();
|
|
23
|
+
|
|
24
|
+
const action = {
|
|
25
|
+
type: "navigateToScreen",
|
|
26
|
+
options: {
|
|
27
|
+
typeMapping: "parentLock",
|
|
28
|
+
navigationAction: "push",
|
|
29
|
+
navigationOptions: { pinFlow: "set-pin" },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const result = await createNavigateToScreenAction(
|
|
34
|
+
navigator,
|
|
35
|
+
rivers,
|
|
36
|
+
contentTypes
|
|
37
|
+
)(action);
|
|
38
|
+
|
|
39
|
+
expect(navigator.push).toHaveBeenCalledWith(river, {
|
|
40
|
+
pinFlow: "set-pin",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result).toBe(ActionResult.Success);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("forwards navigationOptions to replace when navigating to a screen", async () => {
|
|
47
|
+
const navigator = createNavigator();
|
|
48
|
+
|
|
49
|
+
const action = {
|
|
50
|
+
type: "navigateToScreen",
|
|
51
|
+
options: {
|
|
52
|
+
typeMapping: "parentLock",
|
|
53
|
+
navigationOptions: { pinFlow: "change-pin" },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
|
|
58
|
+
|
|
59
|
+
expect(navigator.replace).toHaveBeenCalledWith(river, {
|
|
60
|
+
pinFlow: "change-pin",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("forwards navigationOptions to push when navigating with an entry", async () => {
|
|
65
|
+
const navigator = createNavigator();
|
|
66
|
+
const entry = { id: "entry-id" } as unknown as ZappEntry;
|
|
67
|
+
|
|
68
|
+
const action = {
|
|
69
|
+
type: "navigateToScreen",
|
|
70
|
+
options: {
|
|
71
|
+
typeMapping: "parentLock",
|
|
72
|
+
navigationAction: "push",
|
|
73
|
+
entry,
|
|
74
|
+
navigationOptions: { pinFlow: "set-pin" },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
|
|
79
|
+
|
|
80
|
+
expect(navigator.push).toHaveBeenCalledWith(
|
|
81
|
+
{ id: "entry-id", type: { value: "parentLock" } },
|
|
82
|
+
{ pinFlow: "set-pin" }
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Entries cannot go through replace(), which only accepts a river, so the
|
|
87
|
+
// non-push path falls back to replaceTop().
|
|
88
|
+
it("forwards navigationOptions to replaceTop when navigating with an entry", async () => {
|
|
89
|
+
const navigator = createNavigator();
|
|
90
|
+
const entry = { id: "entry-id" } as unknown as ZappEntry;
|
|
91
|
+
|
|
92
|
+
const action = {
|
|
93
|
+
type: "navigateToScreen",
|
|
94
|
+
options: {
|
|
95
|
+
typeMapping: "parentLock",
|
|
96
|
+
entry,
|
|
97
|
+
navigationOptions: { pinFlow: "change-pin" },
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
|
|
102
|
+
|
|
103
|
+
expect(navigator.replaceTop).toHaveBeenCalledWith(
|
|
104
|
+
{ id: "entry-id", type: { value: "parentLock" } },
|
|
105
|
+
{ pinFlow: "change-pin" }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(navigator.replace).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Regression guard: the no-options path must keep working unchanged.
|
|
112
|
+
it("still navigates when no navigationOptions are configured", async () => {
|
|
113
|
+
const navigator = createNavigator();
|
|
114
|
+
|
|
115
|
+
const action = {
|
|
116
|
+
type: "navigateToScreen",
|
|
117
|
+
options: {
|
|
118
|
+
typeMapping: "parentLock",
|
|
119
|
+
navigationAction: "push",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = await createNavigateToScreenAction(
|
|
124
|
+
navigator,
|
|
125
|
+
rivers,
|
|
126
|
+
contentTypes
|
|
127
|
+
)(action);
|
|
128
|
+
|
|
129
|
+
expect(navigator.push).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect((navigator.push as jest.Mock).mock.calls[0][0]).toBe(river);
|
|
131
|
+
expect(result).toBe(ActionResult.Success);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -25,6 +25,19 @@ export interface NavigateToScreenOptions {
|
|
|
25
25
|
* - undefined to navigate to screen without entry
|
|
26
26
|
*/
|
|
27
27
|
entry?: "@{entry/}" | ZappEntry;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Arbitrary values passed through to the destination screen.
|
|
31
|
+
*
|
|
32
|
+
* They are stored on the navigation route as `NavigationScreenData.options`
|
|
33
|
+
* and read on the target screen via `useNavigation().data?.options`. Use this
|
|
34
|
+
* to hand per-navigation parameters to a screen instead of routing them
|
|
35
|
+
* through module-level state.
|
|
36
|
+
*
|
|
37
|
+
* Note: `legacyScreenData()` does not carry these values, so they are not
|
|
38
|
+
* visible through `useCurrentScreenData()` / `useRoute()`.
|
|
39
|
+
*/
|
|
40
|
+
navigationOptions?: Record<string, unknown>;
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
/**
|
|
@@ -55,6 +68,7 @@ export function createNavigateToScreenAction(
|
|
|
55
68
|
|
|
56
69
|
const navigationAction = action.options?.navigationAction;
|
|
57
70
|
const entrySource = action.options?.entry;
|
|
71
|
+
const navigationOptions = action.options?.navigationOptions;
|
|
58
72
|
|
|
59
73
|
const entry = entrySource
|
|
60
74
|
? entrySource === "@{entry/}"
|
|
@@ -83,10 +97,10 @@ export function createNavigateToScreenAction(
|
|
|
83
97
|
};
|
|
84
98
|
|
|
85
99
|
if (navigationAction === "push") {
|
|
86
|
-
navigator.push(overriddenEntry);
|
|
100
|
+
navigator.push(overriddenEntry, navigationOptions);
|
|
87
101
|
} else {
|
|
88
102
|
// Use replaceTop for entries since replace() only accepts ZappRiver
|
|
89
|
-
navigator.replaceTop(overriddenEntry);
|
|
103
|
+
navigator.replaceTop(overriddenEntry, navigationOptions);
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
return ActionResult.Success;
|
|
@@ -113,9 +127,9 @@ export function createNavigateToScreenAction(
|
|
|
113
127
|
context?.callback?.({ success: false, error: null, abort: true });
|
|
114
128
|
|
|
115
129
|
if (navigationAction === "push") {
|
|
116
|
-
navigator.push(river);
|
|
130
|
+
navigator.push(river, navigationOptions);
|
|
117
131
|
} else {
|
|
118
|
-
navigator.replace(river);
|
|
132
|
+
navigator.replace(river, navigationOptions);
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
return ActionResult.Success;
|
|
@@ -159,13 +159,13 @@ export const decorateFeed = (feed: ZappFeed) => {
|
|
|
159
159
|
|
|
160
160
|
const decoratedFeed = R.clone(feed);
|
|
161
161
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
const selectMode =
|
|
163
|
+
feed.extensions?.preference_editor_options?.select_mode ||
|
|
164
|
+
feed.extensions?.behavior?.select_mode;
|
|
165
165
|
|
|
166
|
-
if (
|
|
166
|
+
if (selectMode === "single") {
|
|
167
167
|
return makeSingleSelect(feed, key, decoratedFeed);
|
|
168
|
-
} else {
|
|
169
|
-
return makeMultiSelect(feed, key, decoratedFeed);
|
|
170
168
|
}
|
|
169
|
+
|
|
170
|
+
return makeMultiSelect(feed, key, decoratedFeed);
|
|
171
171
|
};
|
package/cellUtils/index.ts
CHANGED
|
@@ -65,11 +65,38 @@ export const isEmptyOrNil = (value: any): boolean =>
|
|
|
65
65
|
*/
|
|
66
66
|
export const getLabel = (dataKeyValue, customDataKeyValue) => (entry) => {
|
|
67
67
|
const label = getKeyForLabel(dataKeyValue, customDataKeyValue);
|
|
68
|
-
const isPath = label &&
|
|
69
|
-
|
|
68
|
+
const isPath = label && label.length > 0;
|
|
69
|
+
|
|
70
|
+
// An unresolved key is rendered as-is, because a data key is allowed to be a
|
|
71
|
+
// literal label ("More") rather than a path. Fall back to the whole key
|
|
72
|
+
// instead of its first segment: "extensions.section" reads as an unresolved
|
|
73
|
+
// path, while "extensions" reads as a legitimate label and hides the problem.
|
|
74
|
+
const fallback = isEmptyOrNil(label) ? "" : label.join(".");
|
|
70
75
|
|
|
71
76
|
try {
|
|
72
|
-
|
|
77
|
+
if (!isPath) {
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resolved = label.reduce(
|
|
82
|
+
(value, key) => (value == null ? undefined : value[key]),
|
|
83
|
+
entry
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (isNotNil(resolved)) {
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A multi-segment key is unambiguously a path, so failing to resolve it is
|
|
91
|
+
// a misconfiguration rather than someone typing a literal label.
|
|
92
|
+
if (label.length > 1) {
|
|
93
|
+
cellUtilsLogger.warning({
|
|
94
|
+
message: `getLabel: data key "${fallback}" was not found on the entry, rendering the key itself`,
|
|
95
|
+
data: { dataKeyValue, customDataKeyValue, entryId: entry?.id },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return fallback;
|
|
73
100
|
} catch (error) {
|
|
74
101
|
cellUtilsLogger.warning({
|
|
75
102
|
data: { error, dataKeyValue, customDataKeyValue },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "16.0.0-rc.
|
|
3
|
+
"version": "16.0.0-rc.64",
|
|
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.
|
|
30
|
+
"@applicaster/applicaster-types": "16.0.0-rc.64",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|