@applicaster/zapp-react-native-ui-components 16.0.0-rc.84 → 16.0.0-rc.85

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.
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect } from "react";
2
2
  import { TouchableOpacity, ViewStyle } from "react-native";
3
3
 
4
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
+ import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
5
6
 
6
7
  import Image from "./Image";
7
8
  type Props = {
@@ -57,6 +58,7 @@ export const ActionButton = React.memo(function ActionButtonComponent(
57
58
  ) {
58
59
  const { item, action, asset, flavour = "flavour_1", cellUUID } = props;
59
60
  const actionContext = useActions(action?.identifier);
61
+ const component = useUIComponentContext();
60
62
 
61
63
  // TODO: add subscription API for action availability
62
64
  const actionDisabled =
@@ -81,8 +83,9 @@ export const ActionButton = React.memo(function ActionButtonComponent(
81
83
  updateState: (state) => {
82
84
  setActionState(state);
83
85
  },
86
+ ...(component ? { component } : {}),
84
87
  });
85
- }, [actionState, actionContext?.state, item?.id]);
88
+ }, [actionState, actionContext?.state, item, component]);
86
89
 
87
90
  useEffect(() => {
88
91
  if (typeof actionContext?.addListener === "function") {
@@ -6,6 +6,7 @@ import React, {
6
6
  useState,
7
7
  } from "react";
8
8
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
9
+ import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
9
10
 
10
11
  type ActionDefinition = {
11
12
  identifier?: string;
@@ -61,6 +62,7 @@ export function ActionButtonController({
61
62
  const resolvedEntry = entry;
62
63
  const resolvedIdentifier = action?.identifier || pluginIdentifier || "";
63
64
  const actionContext = useActions(resolvedIdentifier);
65
+ const component = useUIComponentContext();
64
66
 
65
67
  const actionDisabled =
66
68
  typeof actionContext?.isActionAvailable === "function" &&
@@ -128,20 +130,33 @@ export function ActionButtonController({
128
130
  return;
129
131
  }
130
132
 
133
+ const invokeOptions: InvokeArgsOptions = {
134
+ ...(component ? { component } : {}),
135
+ };
136
+
131
137
  if (supportsEntryState) {
132
138
  return actionContext.invokeAction?.(resolvedEntry, {
139
+ ...invokeOptions,
133
140
  updateState: setActionState,
134
141
  });
135
142
  }
136
143
 
144
+ if (typeof actionContext.invokeAction === "function") {
145
+ return actionContext.invokeAction(resolvedEntry, invokeOptions);
146
+ }
147
+
137
148
  const favouritesAction = legacySelected
138
149
  ? actionContext.removeFavourite
139
150
  : actionContext.addFavourite;
140
151
 
141
- const toggleAction = actionContext?.invokeAction ?? favouritesAction;
142
-
143
- return toggleAction?.(resolvedEntry);
144
- }, [actionContext, supportsEntryState, resolvedEntry, legacySelected]);
152
+ return favouritesAction?.(resolvedEntry);
153
+ }, [
154
+ actionContext,
155
+ supportsEntryState,
156
+ resolvedEntry,
157
+ legacySelected,
158
+ component,
159
+ ]);
145
160
 
146
161
  if (!actionContext || actionDisabled) {
147
162
  if (!actionContext) {
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { Text } from "react-native";
3
3
  import { render, fireEvent } from "@testing-library/react-native";
4
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
+ import { UIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
5
6
 
6
7
  import { ActionButtonController } from "../ActionButtonController";
7
8
 
@@ -89,6 +90,47 @@ describe("ActionButtonController", () => {
89
90
  );
90
91
  });
91
92
 
93
+ it("passes the List UIComponentContext into invokeAction so the sheet can refresh it", () => {
94
+ const listComponent = {
95
+ id: "list-1",
96
+ data: { source: "https://example.com/user/collections/abc" },
97
+ };
98
+
99
+ const actionContext = {
100
+ isActionAvailable: jest.fn(() => true),
101
+ initialEntryState: jest.fn(() => ({ active: false })),
102
+ invokeAction: jest.fn(),
103
+ addListener: jest.fn(() => jest.fn()),
104
+ };
105
+
106
+ mockUseActions.mockReturnValue(actionContext);
107
+
108
+ const { getByTestId } = render(
109
+ <UIComponentContext.Provider value={listComponent}>
110
+ <ActionButtonController
111
+ action={{ identifier: "open-modal-bottom-sheet-cell-action" }}
112
+ entry={entry}
113
+ >
114
+ {({ onPress }) => (
115
+ <Text testID="content" onPress={onPress}>
116
+ press
117
+ </Text>
118
+ )}
119
+ </ActionButtonController>
120
+ </UIComponentContext.Provider>
121
+ );
122
+
123
+ fireEvent.press(getByTestId("content"));
124
+
125
+ expect(actionContext.invokeAction).toHaveBeenCalledWith(
126
+ entry,
127
+ expect.objectContaining({
128
+ updateState: expect.any(Function),
129
+ component: listComponent,
130
+ })
131
+ );
132
+ });
133
+
92
134
  it("updates state from addListener", () => {
93
135
  let listener;
94
136
 
@@ -328,7 +370,7 @@ describe("ActionButtonController", () => {
328
370
 
329
371
  fireEvent.press(getByTestId("content"));
330
372
 
331
- expect(invokeAction).toHaveBeenCalledWith(entry);
373
+ expect(invokeAction).toHaveBeenCalledWith(entry, {});
332
374
  expect(addFavourite).not.toHaveBeenCalled();
333
375
  expect(removeFavourite).not.toHaveBeenCalled();
334
376
  });
@@ -12,6 +12,7 @@ export type ActionButtonProps = {
12
12
  item: RegisteredAction;
13
13
  configuration: any;
14
14
  width: number;
15
+ component?: ZappUIComponent & { parent?: ZappUIComponent };
15
16
  };
16
17
 
17
18
  type Props = ActionButtonProps & {
@@ -27,7 +28,7 @@ type Props = ActionButtonProps & {
27
28
  * (backed by the action's observable / `addListener`).
28
29
  */
29
30
  export function ActionButton(props: Props) {
30
- const { item, entry, onPress: onItemPress } = props;
31
+ const { item, entry, onPress: onItemPress, component } = props;
31
32
 
32
33
  const action = item?.action;
33
34
 
@@ -38,9 +39,10 @@ export function ActionButton(props: Props) {
38
39
  invokeAction({
39
40
  context: "toast",
40
41
  dismiss: () => onItemPress?.(item),
42
+ ...(component ? { component } : {}),
41
43
  });
42
44
  },
43
- [invokeAction, item, onItemPress]
45
+ [invokeAction, item, onItemPress, component]
44
46
  );
45
47
 
46
48
  if (!isActionAvailableFor(action, entry as ZappEntry)) return null;
@@ -0,0 +1,38 @@
1
+ import { openBottomSheetModal } from "@applicaster/zapp-react-native-utils/modalState";
2
+ import { boundActionButton } from "../boundActionButton";
3
+ import { openActionsBottomSheet } from "../openActionsBottomSheet";
4
+
5
+ jest.mock("@applicaster/zapp-react-native-utils/modalState", () => ({
6
+ openBottomSheetModal: jest.fn(),
7
+ }));
8
+
9
+ jest.mock("../boundActionButton", () => ({
10
+ boundActionButton: jest.fn(() => jest.fn()),
11
+ }));
12
+
13
+ describe("openActionsBottomSheet", () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ it("binds the List component into the sheet row button", () => {
19
+ const entry = { id: "entry-1" } as ZappEntry;
20
+
21
+ const component = {
22
+ data: { source: "https://example.com/user/collections/abc" },
23
+ };
24
+
25
+ const actions = [
26
+ { identifier: "entry_action_0", action: { invokeAction: jest.fn() } },
27
+ ];
28
+
29
+ openActionsBottomSheet({
30
+ entry,
31
+ actions: actions as any,
32
+ component: component as any,
33
+ });
34
+
35
+ expect(boundActionButton).toHaveBeenCalledWith(entry, component);
36
+ expect(openBottomSheetModal).toHaveBeenCalledTimes(1);
37
+ });
38
+ });
@@ -8,9 +8,12 @@ import { ActionButton, ActionButtonProps } from "./ActionButton";
8
8
  * `BottomSheetModalContent` renders `buttonComponent` per item and does not
9
9
  * know about entries, so the entry is closed over here instead.
10
10
  */
11
- export const boundActionButton = (entry: ZappEntry | ZappFeed) => {
11
+ export const boundActionButton = (
12
+ entry: ZappEntry | ZappFeed,
13
+ component?: ZappUIComponent & { parent?: ZappUIComponent }
14
+ ) => {
12
15
  const ActionButtonWithEntry = (props: ActionButtonProps) => (
13
- <ActionButton {...props} entry={entry as ZappEntry} />
16
+ <ActionButton {...props} entry={entry as ZappEntry} component={component} />
14
17
  );
15
18
 
16
19
  ActionButtonWithEntry.displayName = "ActionButtonWithEntry";
@@ -17,6 +17,8 @@ export type OpenActionsBottomSheetArgs = {
17
17
  actions: RegisteredAction[];
18
18
  title?: string;
19
19
  summary?: string;
20
+ /** List that opened the sheet; forwarded into row invokeAction context. */
21
+ component?: ZappUIComponent & { parent?: ZappUIComponent };
20
22
  };
21
23
 
22
24
  /**
@@ -32,6 +34,7 @@ export function openActionsBottomSheet({
32
34
  actions,
33
35
  title,
34
36
  summary,
37
+ component,
35
38
  }: OpenActionsBottomSheetArgs): void {
36
39
  if (!actions?.length) {
37
40
  // Every action resolved away - unavailable for this entry, or not
@@ -50,7 +53,7 @@ export function openActionsBottomSheet({
50
53
  // BottomSheetModalContent still requires an onPress.
51
54
  onPress: noop,
52
55
  items: actions,
53
- buttonComponent: boundActionButton(entry),
56
+ buttonComponent: boundActionButton(entry, component),
54
57
  title,
55
58
  summary,
56
59
  },
@@ -20,6 +20,7 @@ export type TextInputContentProps = {
20
20
  defaultValue?: string;
21
21
  buttonLabel: string;
22
22
  actions: ActionType[];
23
+ actionContext?: Record<string, any>;
23
24
  };
24
25
 
25
26
  const styles = StyleSheet.create({
@@ -38,7 +39,13 @@ const styles = StyleSheet.create({
38
39
  * Standard dismiss header comes from BottomSheetModalContent / ModalHeader.
39
40
  */
40
41
  export function TextInputContent(props: TextInputContentProps) {
41
- const { inputLabel, defaultValue = "", buttonLabel, actions } = props;
42
+ const {
43
+ inputLabel,
44
+ defaultValue = "",
45
+ buttonLabel,
46
+ actions,
47
+ actionContext,
48
+ } = props;
42
49
 
43
50
  const [textValue, setTextValue] = useState(defaultValue);
44
51
  const [submitting, setSubmitting] = useState(false);
@@ -68,12 +75,17 @@ export function TextInputContent(props: TextInputContentProps) {
68
75
  return item;
69
76
  });
70
77
 
71
- await actionExecutor.handleActions(updatedActions);
78
+ if (actionContext) {
79
+ await actionExecutor.handleActions(updatedActions, actionContext);
80
+ } else {
81
+ await actionExecutor.handleActions(updatedActions);
82
+ }
83
+
72
84
  await actionExecutor.handleAction({ type: "dismissBottomSheet" });
73
85
  } finally {
74
86
  setSubmitting(false);
75
87
  }
76
- }, [submitting, actions, textValue]);
88
+ }, [submitting, actions, textValue, actionContext]);
77
89
 
78
90
  return (
79
91
  <View style={styles.container}>
@@ -117,4 +117,40 @@ describe("TextInputContent", () => {
117
117
  type: "dismissBottomSheet",
118
118
  });
119
119
  });
120
+
121
+ it("forwards actionContext into handleActions so rename can refresh the List", async () => {
122
+ const actionContext = {
123
+ component: {
124
+ id: "list-1",
125
+ data: { source: "https://example.com/user/collections/abc" },
126
+ },
127
+ screenData: { id: "playlist-screen" },
128
+ };
129
+
130
+ render(<TextInputContent {...baseProps} actionContext={actionContext} />);
131
+
132
+ fireEvent.press(screen.getByTestId("submit-button"));
133
+
134
+ await waitFor(() => {
135
+ expect(actionExecutor.handleActions).toHaveBeenCalledTimes(1);
136
+ });
137
+
138
+ expect(actionExecutor.handleActions).toHaveBeenCalledWith(
139
+ [
140
+ {
141
+ type: "sendCloudEvent",
142
+ options: {
143
+ url: "https://server.com/cloud-events",
144
+ type: "com.applicaster.collection.create.v1",
145
+ subject: "edit_collection",
146
+ data: {
147
+ collectionId: "playlist-123",
148
+ name: "Summer Hits",
149
+ },
150
+ },
151
+ },
152
+ ],
153
+ actionContext
154
+ );
155
+ });
120
156
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "16.0.0-rc.84",
3
+ "version": "16.0.0-rc.85",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "16.0.0-rc.84",
32
- "@applicaster/zapp-react-native-bridge": "16.0.0-rc.84",
33
- "@applicaster/zapp-react-native-redux": "16.0.0-rc.84",
34
- "@applicaster/zapp-react-native-utils": "16.0.0-rc.84",
31
+ "@applicaster/applicaster-types": "16.0.0-rc.85",
32
+ "@applicaster/zapp-react-native-bridge": "16.0.0-rc.85",
33
+ "@applicaster/zapp-react-native-redux": "16.0.0-rc.85",
34
+ "@applicaster/zapp-react-native-utils": "16.0.0-rc.85",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "react-native-sortables": "1.7.1",