Skip to content

Commit fa679da

Browse files
feat(function-appaction-example): render parametersSchema via RJSF [EXT-6767] (#10125)
1 parent 197866d commit fa679da

File tree

8 files changed

+453
-59
lines changed

8 files changed

+453
-59
lines changed

examples/function-appaction/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
"@contentful/f36-tokens": "4.2.0",
99
"@contentful/react-apps-toolkit": "1.2.16",
1010
"@emotion/css": "^11.13.5",
11+
"@rjsf/core": "^5.24.13",
12+
"@rjsf/validator-ajv8": "^5.24.13",
1113
"@vitejs/plugin-react": "^4.3.4",
12-
"contentful-management": "^11.57.2",
14+
"contentful-management": "^11.57.4",
1315
"react": "19.0.0",
1416
"react-dom": "19.0.0",
1517
"vite": "^6.2.0",

examples/function-appaction/src/components/ActionFailure.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@ interface Props {
1515
const ActionFailure = (props: Props) => {
1616
const { actionResult, accordionState, handleCollapse, handleExpand } = props;
1717
const { error, timestamp, actionId } = actionResult;
18-
const details: string | undefined = (actionResult.data as any)?.error?.details as
19-
| string
20-
| undefined;
21-
const data: any = actionResult.data;
22-
const createdAt = data?.sys?.createdAt;
23-
const updatedAt = data?.sys?.updatedAt;
18+
const details: string | undefined = actionResult.call?.error?.details as string | undefined;
19+
const call = actionResult.call;
20+
const createdAt = call?.sys?.createdAt;
21+
const updatedAt = call?.sys?.updatedAt;
2422
const duration = computeDuration(createdAt, updatedAt);
2523
const { message, statusCode } = useParseError(error);
2624

examples/function-appaction/src/components/ActionResult.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ interface Props {
1212

1313
const ActionResult = (props: Props) => {
1414
const [accordionState, setAccordionState] = useState<any>({});
15-
const sdk = useSDK<PageAppSDK>();
1615

1716
const { actionResult } = props;
1817
const { success } = actionResult;

examples/function-appaction/src/components/ActionSuccess.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,17 @@ interface Props {
1515

1616
const ActionSuccess = (props: Props) => {
1717
const { actionResult, accordionState, handleCollapse, handleExpand, handleCopy } = props;
18-
const { data, timestamp, actionId } = actionResult;
19-
const statusCode = data?.response?.statusCode ?? (data as any)?.status;
18+
const { call, response, timestamp, actionId } = actionResult;
19+
const statusCode = response?.response?.statusCode;
2020

2121
const responseContentType =
22-
getHeaderValue((data as any)?.response?.headers as any, 'content-type') ||
23-
getHeaderValue((data as any)?.response?.headers as any, 'Content-Type');
24-
const responseSource = (data as any)?.response?.body ?? (data as any)?.result;
22+
getHeaderValue(response?.response?.headers, 'content-type') ||
23+
getHeaderValue(response?.response?.headers, 'Content-Type');
24+
const responseSource = response?.response?.body ?? call?.result;
2525
const responseBody = formatBodyForDisplay(responseSource, responseContentType);
2626

27-
// Prefer structured call timestamps (sys.createdAt -> sys.updatedAt) only
28-
const createdAt = (data as any)?.sys?.createdAt;
29-
const updatedAt = (data as any)?.sys?.updatedAt;
27+
const createdAt = call?.sys?.createdAt;
28+
const updatedAt = call?.sys?.updatedAt;
3029
const duration = computeDuration(createdAt, updatedAt);
3130

3231
return (
@@ -35,7 +34,9 @@ const ActionSuccess = (props: Props) => {
3534
title={
3635
<Text>
3736
<Badge variant="positive">Success</Badge>
38-
<Text className={styles.accordionTitleMargin}>[{statusCode}]</Text> - {timestamp}
37+
{statusCode && (
38+
<Text className={styles.accordionTitleMargin}>[{statusCode}]</Text>
39+
)} - {timestamp}
3940
{typeof duration === 'number' && (
4041
<Text className={styles.accordionTitleMargin}>
4142
Duration: <strong>{duration}</strong> ms
@@ -69,10 +70,9 @@ const ActionSuccess = (props: Props) => {
6970
</Flex>
7071
</Text>
7172
<Text>
72-
{(data as any)?.sys?.updatedAt && (
73+
{call?.sys?.updatedAt && (
7374
<>
74-
<strong>Completed at:</strong>{' '}
75-
{new Date((data as any).sys.updatedAt).toLocaleString()}
75+
<strong>Completed at:</strong> {new Date(call.sys.updatedAt).toLocaleString()}
7676
</>
7777
)}
7878
</Text>

examples/function-appaction/src/components/AppActionCard.tsx

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@ import {
66
CopyButton,
77
Flex,
88
FormControl,
9+
SectionHeading,
910
Stack,
1011
Subheading,
1112
Text,
1213
TextInput,
1314
} from '@contentful/f36-components';
1415
import { useSDK } from '@contentful/react-apps-toolkit';
15-
import { AppActionProps } from 'contentful-management';
16+
import { AppActionCallRawResponseProps, AppActionProps } from 'contentful-management';
1617
import { useState } from 'react';
17-
import { ActionResultData, ActionResultType } from '../locations/Page';
18+
import { ActionResultType } from '../locations/Page';
1819
import ActionResult from './ActionResult';
1920
import { styles } from './AppActionCard.styles';
21+
import Forma36Form from './rjsf/Forma36Form';
22+
import validator from '@rjsf/validator-ajv8';
23+
import type { IChangeEvent } from '@rjsf/core';
2024

2125
interface Props {
2226
action: AppActionProps;
@@ -26,50 +30,66 @@ const AppActionCard = (props: Props) => {
2630
const [actionResults, setActionResults] = useState<ActionResultType[]>([]);
2731
const [loadingAction, setLoadingAction] = useState<string | null>(null);
2832
const [actionParameters, setActionParameters] = useState<any>({});
33+
const [schemaErrorsByAction, setSchemaErrorsByAction] = useState<Record<string, number>>({});
2934

3035
const sdk = useSDK<PageAppSDK>();
3136
const { action } = props;
3237

3338
const callAction = async (action: AppActionProps) => {
3439
setLoadingAction(action.sys.id);
40+
let response: AppActionCallRawResponseProps | undefined;
3541
try {
36-
const result = (await sdk.cma.appActionCall.createWithResult(
42+
const result = await sdk.cma.appActionCall.createWithResult(
3743
{
3844
appDefinitionId: sdk.ids.app || '',
3945
appActionId: action.sys.id,
4046
},
4147
{
4248
parameters: actionParameters[action.sys.id] || {},
4349
}
44-
)) as unknown as ActionResultData;
50+
);
4551

4652
const timestamp = new Date().toLocaleString();
47-
const call: any = result as any;
48-
const callId = (call as any)?.sys?.id;
53+
const call = result;
54+
const callId = call?.sys?.id;
4955
const base = { timestamp, actionId: action.sys.id, callId } as const;
56+
if (call.sys.appActionCallResponse) {
57+
response = await sdk.cma.appActionCall.getResponse({
58+
appDefinitionId: sdk.ids.app || '',
59+
appActionId: action.sys.id,
60+
callId,
61+
});
62+
}
5063

5164
if (call?.status === 'succeeded') {
52-
setActionResults((prev) => [{ success: true, data: call, ...base }, ...prev]);
65+
setActionResults((prev) => [{ success: true, call, ...base }, ...prev]);
5366
} else if (call?.status === 'failed') {
5467
setActionResults((prev) => [
5568
{
5669
success: false,
57-
data: call,
70+
call,
71+
response,
5872
error: call?.error || new Error('App action failed'),
5973
...base,
6074
},
6175
...prev,
6276
]);
6377
} else {
6478
setActionResults((prev) => [
65-
{ success: false, data: call, error: new Error('App action still processing'), ...base },
79+
{
80+
success: false,
81+
call,
82+
response,
83+
error: new Error('App action still processing'),
84+
...base,
85+
},
6686
...prev,
6787
]);
6888
}
6989
} catch (error) {
7090
const timestamp = new Date().toLocaleString();
7191
setActionResults((prev) => [
72-
{ success: false, error, timestamp, actionId: action.sys.id },
92+
{ success: false, error, timestamp, actionId: action.sys.id, response },
7393
...prev,
7494
]);
7595
} finally {
@@ -136,12 +156,31 @@ const AppActionCard = (props: Props) => {
136156
};
137157

138158
const isButtonDisabled = () => {
139-
const parameters = (action as any).parameters as any[] | undefined;
140-
const requiredParameters = parameters?.filter((param: any) => param.required) ?? [];
159+
const actionId = action.sys.id;
160+
const formData = actionParameters[actionId] || {};
141161

142-
const hasEmptyRequiredParameters = requiredParameters.find((param: any) => {
143-
const paramValue = actionParameters[action.sys.id]?.[param.id];
144-
return !paramValue;
162+
const hasSchema = action.parametersSchema;
163+
if (hasSchema) {
164+
const schema = action.parametersSchema;
165+
const requiredKeys: string[] = Array.isArray(schema?.required) ? schema.required : [];
166+
const hasEmptyRequired = requiredKeys.some((key) => {
167+
const value = formData?.[key];
168+
if (value === undefined || value === null) return true;
169+
if (typeof value === 'string' && value.trim() === '') return true;
170+
if (typeof value === 'number' && Number.isNaN(value)) return true;
171+
return false;
172+
});
173+
const hasErrors = Boolean(schemaErrorsByAction[actionId]);
174+
return hasErrors || hasEmptyRequired;
175+
}
176+
177+
const customAction = action as unknown as {
178+
parameters?: Array<{ id: string; required?: boolean }>;
179+
};
180+
const requiredParameters = customAction.parameters?.filter((param) => param.required) || [];
181+
const hasEmptyRequiredParameters = requiredParameters.some((param) => {
182+
const paramValue = formData?.[param.id];
183+
return paramValue === undefined || paramValue === null || paramValue === '';
145184
});
146185

147186
return Boolean(hasEmptyRequiredParameters);
@@ -175,8 +214,34 @@ const AppActionCard = (props: Props) => {
175214
</Button>
176215
</Box>
177216
</Flex>
178-
{Array.isArray((action as any).parameters) &&
179-
(action as { parameters: any[] }).parameters.length ? (
217+
{action.parametersSchema ? (
218+
<Box marginTop="spacingS">
219+
<Box marginBottom="spacingS">
220+
<SectionHeading as="h4">Parameters</SectionHeading>
221+
</Box>
222+
<Forma36Form
223+
schema={action.parametersSchema}
224+
formData={actionParameters[action.sys.id] || {}}
225+
validator={validator}
226+
liveValidate
227+
showErrorList={false}
228+
uiSchema={{ 'ui:submitButtonOptions': { norender: true } }}
229+
onChange={(e: IChangeEvent<Record<string, unknown>>) => {
230+
setActionParameters({
231+
...actionParameters,
232+
[action.sys.id]: e.formData,
233+
});
234+
const errorCount = Array.isArray(e.errors) ? e.errors.length : 0;
235+
setSchemaErrorsByAction({
236+
...schemaErrorsByAction,
237+
[action.sys.id]: errorCount,
238+
});
239+
}}>
240+
<></>
241+
</Forma36Form>
242+
</Box>
243+
) : Array.isArray((action as any).parameters) &&
244+
(action as { parameters: any[] }).parameters.length ? (
180245
<Box marginTop="spacingS">
181246
<Box marginBottom="spacingM">
182247
<Subheading as="h4">Parameters</Subheading>

examples/function-appaction/src/components/RawResponseViewer.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,18 @@ const RawResponseViewer = ({
3434
setLoading(true);
3535
setError(undefined);
3636
try {
37-
const response = (await sdk.cma.appActionCall.getResponse({
37+
const response = await sdk.cma.appActionCall.getResponse({
3838
appDefinitionId: sdk.ids.app || '',
3939
appActionId: actionId,
4040
callId,
41-
})) as any;
41+
});
4242
setRaw(response);
4343
const contentType =
44-
getHeaderValue(response?.response?.headers as any, 'content-type') ||
45-
getHeaderValue(response?.response?.headers as any, 'Content-Type') ||
46-
getHeaderValue(response?.response?.headers as any, 'contentType');
44+
getHeaderValue(response?.response?.headers, 'content-type') ||
45+
getHeaderValue(response?.response?.headers, 'Content-Type') ||
46+
getHeaderValue(response?.response?.headers, 'contentType');
4747
const formatted = formatBodyForDisplay(response?.response?.body, contentType);
48-
const duration = computeDuration(
49-
response?.sys?.createdAt ?? response?.requestAt,
50-
response?.sys?.updatedAt ?? response?.responseAt
51-
);
52-
onLoaded?.({ contentType, body: formatted, duration });
48+
onLoaded?.({ contentType, body: formatted });
5349
} catch (e: any) {
5450
setError(e?.message || 'Failed to fetch raw response');
5551
} finally {
@@ -58,9 +54,9 @@ const RawResponseViewer = ({
5854
};
5955

6056
const contentType =
61-
getHeaderValue(raw?.response?.headers as any, 'content-type') ||
62-
getHeaderValue(raw?.response?.headers as any, 'Content-Type') ||
63-
getHeaderValue(raw?.response?.headers as any, 'contentType');
57+
getHeaderValue(raw?.response?.headers, 'content-type') ||
58+
getHeaderValue(raw?.response?.headers, 'Content-Type') ||
59+
getHeaderValue(raw?.response?.headers, 'contentType');
6460

6561
return (
6662
<>

0 commit comments

Comments
 (0)