Infer function input in workflow step (#8308)
- add `inputSchema` column in serverless function. This is an array of parameters, with their name and type - on serverless function id update, get the `inputSchema` + store empty settings in step - from step settings, build the form TODO in next PR: - use field type to decide what kind of form should be printed - have a strategy to handle object as input https://github.com/user-attachments/assets/ed96f919-24b5-4baf-a051-31f76f45e575
This commit is contained in:
@ -8,6 +8,10 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
||||
runtime
|
||||
syncStatus
|
||||
latestVersion
|
||||
latestVersionInputSchema {
|
||||
name
|
||||
type
|
||||
}
|
||||
publishedVersions
|
||||
createdAt
|
||||
updatedAt
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||
import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput';
|
||||
import { WorkflowCodeStep } from '@/workflow/types/Workflow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useState } from 'react';
|
||||
import { IconCode, isDefined } from 'twenty-ui';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type WorkflowEditActionFormServerlessFunctionProps =
|
||||
| {
|
||||
@ -23,6 +27,48 @@ export const WorkflowEditActionFormServerlessFunction = (
|
||||
|
||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||
|
||||
const defaultFunctionInput =
|
||||
props.action.settings.input.serverlessFunctionInput;
|
||||
|
||||
const [functionInput, setFunctionInput] =
|
||||
useState<Record<string, any>>(defaultFunctionInput);
|
||||
|
||||
const [serverlessFunctionId, setServerlessFunctionId] = useState<string>(
|
||||
props.action.settings.input.serverlessFunctionId,
|
||||
);
|
||||
|
||||
const updateFunctionInput = useDebouncedCallback(
|
||||
async (newFunctionInput: object) => {
|
||||
if (props.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onActionUpdate({
|
||||
...props.action,
|
||||
settings: {
|
||||
...props.action.settings,
|
||||
input: {
|
||||
serverlessFunctionId:
|
||||
props.action.settings.input.serverlessFunctionId,
|
||||
serverlessFunctionVersion:
|
||||
props.action.settings.input.serverlessFunctionVersion,
|
||||
serverlessFunctionInput: {
|
||||
...props.action.settings.input.serverlessFunctionInput,
|
||||
...newFunctionInput,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
const handleInputChange = (key: string, value: any) => {
|
||||
const newFunctionInput = { ...functionInput, [key]: value };
|
||||
setFunctionInput(newFunctionInput);
|
||||
updateFunctionInput(newFunctionInput);
|
||||
};
|
||||
|
||||
const availableFunctions: Array<SelectOption<string>> = [
|
||||
{ label: 'None', value: '' },
|
||||
...serverlessFunctions
|
||||
@ -32,9 +78,43 @@ export const WorkflowEditActionFormServerlessFunction = (
|
||||
.map((serverlessFunction) => ({
|
||||
label: serverlessFunction.name,
|
||||
value: serverlessFunction.id,
|
||||
latestVersionInputSchema: serverlessFunction.latestVersionInputSchema,
|
||||
})),
|
||||
];
|
||||
|
||||
const handleFunctionChange = (newServerlessFunctionId: string) => {
|
||||
setServerlessFunctionId(newServerlessFunctionId);
|
||||
|
||||
const serverlessFunction = serverlessFunctions.find(
|
||||
(f) => f.id === newServerlessFunctionId,
|
||||
);
|
||||
|
||||
const serverlessFunctionVersion =
|
||||
serverlessFunction?.latestVersion || 'latest';
|
||||
|
||||
const defaultFunctionInput = serverlessFunction?.latestVersionInputSchema
|
||||
? serverlessFunction.latestVersionInputSchema
|
||||
.map((parameter) => parameter.name)
|
||||
.reduce((acc, name) => ({ ...acc, [name]: null }), {})
|
||||
: {};
|
||||
|
||||
if (!props.readonly) {
|
||||
props.onActionUpdate({
|
||||
...props.action,
|
||||
settings: {
|
||||
...props.action.settings,
|
||||
input: {
|
||||
serverlessFunctionId: newServerlessFunctionId,
|
||||
serverlessFunctionVersion,
|
||||
serverlessFunctionInput: defaultFunctionInput,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setFunctionInput(defaultFunctionInput);
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkflowEditGenericFormBase
|
||||
HeaderIcon={<IconCode color={theme.color.orange} />}
|
||||
@ -42,31 +122,24 @@ export const WorkflowEditActionFormServerlessFunction = (
|
||||
headerType="Code"
|
||||
>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-function"
|
||||
dropdownId="select-serverless-function-id"
|
||||
label="Function"
|
||||
fullWidth
|
||||
value={props.action.settings.input.serverlessFunctionId}
|
||||
value={serverlessFunctionId}
|
||||
options={availableFunctions}
|
||||
disabled={props.readonly}
|
||||
onChange={(serverlessFunctionId) => {
|
||||
if (props.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onActionUpdate({
|
||||
...props.action,
|
||||
settings: {
|
||||
...props.action.settings,
|
||||
input: {
|
||||
serverlessFunctionId,
|
||||
serverlessFunctionVersion:
|
||||
serverlessFunctions.find((f) => f.id === serverlessFunctionId)
|
||||
?.latestVersion || 'latest',
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
onChange={handleFunctionChange}
|
||||
/>
|
||||
{functionInput &&
|
||||
Object.entries(functionInput).map(([inputKey, inputValue]) => (
|
||||
<VariableTagInput
|
||||
inputId={`input-${inputKey}`}
|
||||
label={capitalize(inputKey)}
|
||||
placeholder="Enter value (use {{variable}} for dynamic content)"
|
||||
value={inputValue ?? ''}
|
||||
onChange={(value) => handleInputChange(inputKey, value)}
|
||||
/>
|
||||
))}
|
||||
</WorkflowEditGenericFormBase>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,6 +15,9 @@ export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
|
||||
input: {
|
||||
serverlessFunctionId: string;
|
||||
serverlessFunctionVersion: string;
|
||||
serverlessFunctionInput: {
|
||||
[hello: string]: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ describe('addCreateStepNodes', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -42,6 +43,7 @@ describe('addCreateStepNodes', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
|
||||
@ -47,6 +47,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -64,6 +65,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -110,6 +112,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -127,6 +130,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
|
||||
@ -84,6 +84,7 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
|
||||
@ -28,6 +28,7 @@ describe('insertStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -70,6 +71,7 @@ describe('insertStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -106,6 +108,7 @@ describe('insertStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -123,6 +126,7 @@ describe('insertStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -148,6 +152,7 @@ describe('insertStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -188,6 +193,7 @@ describe('insertStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -205,6 +211,7 @@ describe('insertStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -230,6 +237,7 @@ describe('insertStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
|
||||
@ -13,6 +13,7 @@ it('returns a deep copy of the provided steps array instead of mutating it', ()
|
||||
input: {
|
||||
serverlessFunctionId: 'first',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -54,6 +55,7 @@ it('removes a step in a non-empty steps array', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -78,6 +80,7 @@ it('removes a step in a non-empty steps array', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -96,6 +99,7 @@ it('removes a step in a non-empty steps array', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
|
||||
@ -14,6 +14,7 @@ describe('replaceStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'first',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -46,6 +47,7 @@ describe('replaceStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'second',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -68,6 +70,7 @@ describe('replaceStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -92,6 +95,7 @@ describe('replaceStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
@ -110,6 +114,7 @@ describe('replaceStep', () => {
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
|
||||
@ -18,6 +18,7 @@ export const getStepDefaultDefinition = (
|
||||
input: {
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionVersion: '',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
|
||||
Reference in New Issue
Block a user