Skip to content

Commit d687eaa

Browse files
authored
Add example CustomAgentforceView and wire ViewProvider in HomeScreen (#48)
* Add example CustomAgentforceView component and wire into HomeScreen - CustomAgentforceView: React Native component that renders in place of native SDK views - Registered via AppRegistry in index.js for native RCTRootView/ReactRootView rendering - HomeScreen checks enableCustomViewProvider flag and registers the delegate on mount - Handles copilot/richText and copilot/markdown as an example * fix: update example to use componentMap API Use the new 1:1 componentMap format in registerViewProviderIfEnabled instead of the old componentTypes + reactComponentName. * feat: render nested objects/arrays as expandable key/value tables Replace raw JSON dump with recursive ValueRenderer that breaks out nested objects and arrays into structured tables up to 6 levels deep. Deeper nesting progressively compacts padding and key widths to stay usable on mobile screens. * fix: await view provider registration before configure and update JSDoc - Make registerViewProviderIfEnabled sequential with checkConfigurations to avoid a race where configure() runs before registration completes. - Update CustomAgentforceView JSDoc to show current componentMap API. * fix: add signpost comments to CustomAgentforceView for ViewProvider wiring The file header now explains the 3-step wiring (AppRegistry, componentMap, props) and a separator comment marks where the example rendering begins, so the integration points aren't buried in the rendering machinery. * refactor: simplify CustomAgentforceView to minimal JSON.stringify example Move the complex recursive key/value table rendering to ComplexAgentforceDataView.tsx. CustomAgentforceView is now a minimal example (~90 lines) that shows the ViewProvider wiring clearly: badge + definition + JSON.stringify(properties).
1 parent ca2cba8 commit d687eaa

File tree

4 files changed

+443
-1
lines changed

4 files changed

+443
-1
lines changed

index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,9 @@
2727
import 'react-native-gesture-handler';
2828
import { AppRegistry } from 'react-native';
2929
import App from './App';
30+
import CustomAgentforceView from './src/components/CustomAgentforceView';
3031

3132
AppRegistry.registerComponent('ReactAgentforce', () => App);
33+
34+
// Register the custom view provider component so native can render it via RCTRootView/ReactRootView
35+
AppRegistry.registerComponent('CustomAgentforceView', () => CustomAgentforceView);
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
/*
2+
* Copyright (c) 2024-present, salesforce.com, inc. All rights reserved.
3+
*
4+
* A richer custom view provider component that renders nested objects and
5+
* arrays as expandable key/value tables. Drop-in replacement for
6+
* CustomAgentforceView when you need structured data display.
7+
*
8+
* Wire up the same way as CustomAgentforceView — see that file for the
9+
* 3-step ViewProvider integration guide.
10+
*/
11+
12+
import React from 'react';
13+
import { View, Text, StyleSheet, ScrollView } from 'react-native';
14+
import type { ViewProviderComponentData } from 'react-native-agentforce';
15+
16+
interface ComplexAgentforceDataViewProps {
17+
definition?: string;
18+
name?: string;
19+
properties?: Record<string, unknown>;
20+
subComponents?: ViewProviderComponentData[];
21+
}
22+
23+
/** Max nesting depth to prevent runaway recursion. */
24+
const MAX_DEPTH = 6;
25+
26+
/** Check whether a value is a plain object (not an array). */
27+
function isPlainObject(v: unknown): v is Record<string, unknown> {
28+
return v !== null && typeof v === 'object' && !Array.isArray(v);
29+
}
30+
31+
/** Format a leaf value as a string. */
32+
function formatLeaf(value: unknown): string {
33+
if (value === null || value === undefined) return '—';
34+
if (typeof value === 'string') return value;
35+
if (typeof value === 'boolean') return value ? 'true' : 'false';
36+
return String(value);
37+
}
38+
39+
/** Whether a value will render as a nested block (table or array). */
40+
function isNestedValue(v: unknown, depth: number): boolean {
41+
if (depth >= MAX_DEPTH) return false;
42+
if (isPlainObject(v)) return Object.keys(v).length > 0;
43+
if (Array.isArray(v)) return v.length > 0;
44+
return false;
45+
}
46+
47+
/**
48+
* Recursively renders a value. Plain objects become nested key/value tables,
49+
* arrays render each element, and primitives render as text.
50+
*/
51+
const ValueRenderer: React.FC<{ value: unknown; depth: number }> = ({
52+
value,
53+
depth,
54+
}) => {
55+
const compact = depth >= 3; // tighter padding at deeper levels
56+
57+
if (isPlainObject(value) && depth < MAX_DEPTH) {
58+
const entries = Object.entries(value);
59+
if (entries.length === 0) return <Text style={styles.emptyText}>{'{ }'}</Text>;
60+
return (
61+
<View style={[styles.nestedTable, compact && styles.nestedTableCompact]}>
62+
{entries.map(([k, v], i) => (
63+
<PropertyRow key={k} label={k} value={v} depth={depth} isLast={i === entries.length - 1} />
64+
))}
65+
</View>
66+
);
67+
}
68+
69+
if (Array.isArray(value) && depth < MAX_DEPTH) {
70+
if (value.length === 0) return <Text style={styles.emptyText}>{'[ ]'}</Text>;
71+
return (
72+
<View style={[styles.nestedArray, compact && styles.nestedTableCompact]}>
73+
{value.map((item, i) => {
74+
const itemNested = isNestedValue(item, depth + 1);
75+
return (
76+
<View
77+
key={i}
78+
style={[
79+
itemNested ? styles.arrayItemVertical : styles.arrayItem,
80+
i < value.length - 1 && styles.arrayItemBorder,
81+
]}
82+
>
83+
<Text style={styles.arrayIndex}>{i}</Text>
84+
<View style={itemNested ? undefined : styles.arrayValue}>
85+
<ValueRenderer value={item} depth={depth + 1} />
86+
</View>
87+
</View>
88+
);
89+
})}
90+
</View>
91+
);
92+
}
93+
94+
// Leaf value (or max depth reached — fall back to JSON)
95+
const text =
96+
depth >= MAX_DEPTH && typeof value === 'object'
97+
? JSON.stringify(value)
98+
: formatLeaf(value);
99+
100+
return (
101+
<Text style={styles.rowValue} selectable>
102+
{text}
103+
</Text>
104+
);
105+
};
106+
107+
/**
108+
* Renders a single key/value row, stacking vertically when the value is a
109+
* nested object/array so it can use the full width.
110+
*/
111+
const PropertyRow: React.FC<{
112+
label: string;
113+
value: unknown;
114+
depth: number;
115+
isLast: boolean;
116+
}> = ({ label, value, depth, isLast }) => {
117+
const nested = isNestedValue(value, depth + 1);
118+
const compact = depth >= 2;
119+
120+
return (
121+
<View
122+
style={[
123+
nested ? styles.rowVertical : styles.row,
124+
compact && styles.rowCompact,
125+
!isLast && styles.rowBorder,
126+
]}
127+
>
128+
<Text
129+
style={[styles.rowKey, compact && styles.rowKeyCompact]}
130+
numberOfLines={1}
131+
>
132+
{label}
133+
</Text>
134+
<ValueRenderer value={value} depth={depth + 1} />
135+
</View>
136+
);
137+
};
138+
139+
const ComplexAgentforceDataView: React.FC<ComplexAgentforceDataViewProps> = ({
140+
definition = 'unknown',
141+
properties = {},
142+
}) => {
143+
const entries = Object.entries(properties);
144+
145+
// If the only meaningful property is a single text-like value, show it inline
146+
const textKeys = ['text', 'value', 'content', 'label'];
147+
const singleText =
148+
entries.length === 1 &&
149+
textKeys.includes(entries[0][0]) &&
150+
typeof entries[0][1] === 'string'
151+
? (entries[0][1] as string)
152+
: null;
153+
154+
return (
155+
<View style={styles.container}>
156+
<View style={styles.header}>
157+
<View style={styles.badge}>
158+
<Text style={styles.badgeText}>Custom RN View</Text>
159+
</View>
160+
<Text style={styles.definitionText}>{definition}</Text>
161+
</View>
162+
163+
{singleText ? (
164+
<Text style={styles.contentText} selectable>
165+
{singleText}
166+
</Text>
167+
) : entries.length > 0 ? (
168+
<ScrollView style={styles.contentScroll} nestedScrollEnabled>
169+
<View style={styles.table}>
170+
{entries.map(([key, val], idx) => (
171+
<PropertyRow
172+
key={key}
173+
label={key}
174+
value={val}
175+
depth={0}
176+
isLast={idx === entries.length - 1}
177+
/>
178+
))}
179+
</View>
180+
</ScrollView>
181+
) : (
182+
<Text style={styles.emptyText}>No properties</Text>
183+
)}
184+
</View>
185+
);
186+
};
187+
188+
const styles = StyleSheet.create({
189+
container: {
190+
backgroundColor: '#f0f4ff',
191+
borderRadius: 8,
192+
borderWidth: 1,
193+
borderColor: '#4a90d9',
194+
padding: 12,
195+
margin: 4,
196+
},
197+
header: {
198+
flexDirection: 'row',
199+
alignItems: 'center',
200+
marginBottom: 8,
201+
gap: 8,
202+
},
203+
badge: {
204+
backgroundColor: '#4a90d9',
205+
borderRadius: 4,
206+
paddingHorizontal: 8,
207+
paddingVertical: 2,
208+
},
209+
badgeText: {
210+
color: '#ffffff',
211+
fontSize: 10,
212+
fontWeight: '700',
213+
},
214+
definitionText: {
215+
fontSize: 11,
216+
color: '#6c757d',
217+
fontFamily: 'monospace',
218+
},
219+
contentScroll: {
220+
maxHeight: 400,
221+
},
222+
table: {
223+
backgroundColor: '#ffffff',
224+
borderRadius: 6,
225+
borderWidth: 1,
226+
borderColor: '#d0d7e2',
227+
overflow: 'hidden',
228+
},
229+
row: {
230+
flexDirection: 'row',
231+
paddingVertical: 8,
232+
paddingHorizontal: 10,
233+
},
234+
rowVertical: {
235+
flexDirection: 'column',
236+
paddingVertical: 8,
237+
paddingHorizontal: 10,
238+
gap: 4,
239+
},
240+
rowCompact: {
241+
paddingVertical: 5,
242+
paddingHorizontal: 8,
243+
},
244+
rowBorder: {
245+
borderBottomWidth: StyleSheet.hairlineWidth,
246+
borderBottomColor: '#d0d7e2',
247+
},
248+
rowKey: {
249+
width: 100,
250+
fontSize: 12,
251+
fontWeight: '600',
252+
color: '#4a5568',
253+
marginRight: 8,
254+
},
255+
rowKeyCompact: {
256+
width: 80,
257+
fontSize: 11,
258+
},
259+
rowValue: {
260+
flex: 1,
261+
fontSize: 13,
262+
color: '#212529',
263+
lineHeight: 18,
264+
},
265+
nestedTable: {
266+
backgroundColor: '#f8f9fb',
267+
borderRadius: 4,
268+
borderWidth: StyleSheet.hairlineWidth,
269+
borderColor: '#d0d7e2',
270+
overflow: 'hidden',
271+
},
272+
nestedTableCompact: {
273+
borderRadius: 3,
274+
},
275+
nestedArray: {
276+
backgroundColor: '#f8f9fb',
277+
borderRadius: 4,
278+
borderWidth: StyleSheet.hairlineWidth,
279+
borderColor: '#d0d7e2',
280+
overflow: 'hidden',
281+
},
282+
arrayItem: {
283+
flexDirection: 'row',
284+
paddingVertical: 6,
285+
paddingHorizontal: 8,
286+
},
287+
arrayItemVertical: {
288+
flexDirection: 'column',
289+
paddingVertical: 6,
290+
paddingHorizontal: 8,
291+
gap: 4,
292+
},
293+
arrayItemBorder: {
294+
borderBottomWidth: StyleSheet.hairlineWidth,
295+
borderBottomColor: '#d0d7e2',
296+
},
297+
arrayIndex: {
298+
width: 24,
299+
fontSize: 11,
300+
fontWeight: '600',
301+
color: '#9ca3af',
302+
marginRight: 4,
303+
},
304+
arrayValue: {
305+
flex: 1,
306+
},
307+
contentText: {
308+
fontSize: 14,
309+
color: '#212529',
310+
lineHeight: 20,
311+
},
312+
emptyText: {
313+
fontSize: 12,
314+
color: '#9ca3af',
315+
fontStyle: 'italic',
316+
},
317+
});
318+
319+
export default ComplexAgentforceDataView;

0 commit comments

Comments
 (0)