Support define is tool logic function (#17926)

- supports isTool and timeout settings in defineLogicFunction in apps
and in setting tabs definition
- compute for all toolInputSchema for logic funciton, in settings and in
code steps

<img width="991" height="802" alt="image"
src="https://github.com/user-attachments/assets/05dc1221-cac9-45a3-87b0-3b13161446fd"
/>
This commit is contained in:
martmull
2026-02-16 10:43:29 +01:00
committed by GitHub
parent f694bb99b3
commit da064d5e88
138 changed files with 4839 additions and 367 deletions
@@ -1,8 +1,5 @@
import { useGetAvailablePackages } from '@/logic-functions/hooks/useGetAvailablePackages';
import {
type LogicFunctionFormValues,
useLogicFunctionUpdateFormState,
} from '@/logic-functions/hooks/useLogicFunctionUpdateFormState';
import { useLogicFunctionEditor } from '@/logic-functions/hooks/useLogicFunctionEditor';
import { useFullScreenModal } from '@/ui/layout/fullscreen/hooks/useFullScreenModal';
import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
@@ -13,8 +10,7 @@ import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
import { getFunctionInputFromSourceCode } from '@/logic-functions/utils/getFunctionInputFromSourceCode';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/logic-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { TextArea } from '@/ui/input/components/TextArea';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
@@ -36,8 +32,6 @@ import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { CODE_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CodeAction';
import { type Monaco } from '@monaco-editor/react';
@@ -47,11 +41,15 @@ import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { buildOutputSchemaFromValue } from 'twenty-shared/workflow';
import {
getOutputSchemaFromValue,
type InputJsonSchema,
} from 'twenty-shared/logic-function';
import { IconCode, IconPlayerPlay } from 'twenty-ui/display';
import { CodeEditor } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
import { useDebouncedCallback } from 'use-debounce';
import { getFunctionInputFromInputSchema } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getFunctionInputFromInputSchema';
const CODE_EDITOR_MIN_HEIGHT = 343;
@@ -103,7 +101,6 @@ export const WorkflowEditActionCode = ({
activeTabIdComponentState,
WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID,
);
const { updateLogicFunction } = usePersistLogicFunction();
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow();
@@ -111,6 +108,24 @@ export const WorkflowEditActionCode = ({
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
const updateOutputSchemaFromTestResult = async (testResult: object) => {
if (actionOptions.readonly === true) {
return;
}
const newOutputSchema = getOutputSchemaFromValue(testResult);
updateAction({
...action,
settings: { ...action.settings, outputSchema: newOutputSchema },
});
};
const { formValues, loading, executeLogicFunction, onChange, isExecuting } =
useLogicFunctionEditor({
logicFunctionId,
executeCallback: updateOutputSchemaFromTestResult,
});
const { availablePackages } = useGetAvailablePackages({
id: logicFunctionId,
});
@@ -125,52 +140,8 @@ export const WorkflowEditActionCode = ({
action.settings.input.logicFunctionInput,
);
const { formValues, setFormValues, loading } =
useLogicFunctionUpdateFormState({ logicFunctionId });
const updateOutputSchemaFromTestResult = async (testResult: object) => {
if (actionOptions.readonly === true) {
return;
}
const newOutputSchema = buildOutputSchemaFromValue(testResult);
updateAction({
...action,
settings: { ...action.settings, outputSchema: newOutputSchema },
});
};
const { executeLogicFunction, isExecuting } = useExecuteLogicFunction({
logicFunctionId,
callback: updateOutputSchemaFromTestResult,
});
const handleSave = useDebouncedCallback(async () => {
await updateLogicFunction({
input: {
id: logicFunctionId,
update: {
sourceHandlerCode: formValues.code,
},
},
});
}, 500);
const onCodeChange = async (newCode: string) => {
if (actionOptions.readonly === true) {
return;
}
setFormValues((prevState: LogicFunctionFormValues) => {
return {
...prevState,
code: newCode,
};
});
await handleSave();
await handleUpdateFunctionInputSchema(newCode);
};
const handleUpdateFunctionInputSchema = useDebouncedCallback(
async (sourceCode: string) => {
async (sourceCode: string, toolInputSchema: InputJsonSchema) => {
if (actionOptions.readonly === true) {
return;
}
@@ -179,7 +150,12 @@ export const WorkflowEditActionCode = ({
return;
}
const newFunctionInput = await getFunctionInputFromSourceCode(sourceCode);
const schemaArray = Array.isArray(toolInputSchema)
? toolInputSchema
: [toolInputSchema];
const newFunctionInput = getFunctionInputFromInputSchema(schemaArray)[0];
const newMergedInput = mergeDefaultFunctionInputAndFunctionInput({
newInput: newFunctionInput,
oldInput: action.settings.input.logicFunctionInput,
@@ -190,6 +166,7 @@ export const WorkflowEditActionCode = ({
});
setFunctionInput(newMergedInput);
setLogicFunctionTestData((prev) => ({
...prev,
input: newMergedTestInput,
@@ -251,14 +228,12 @@ export const WorkflowEditActionCode = ({
}));
};
const handleRunFunction = async () => {
const handleTestFunction = async () => {
if (actionOptions.readonly === true) {
return;
}
if (!isExecuting) {
await executeLogicFunction();
}
await executeLogicFunction();
};
const handleEditorDidMount = async (
@@ -288,12 +263,18 @@ export const WorkflowEditActionCode = ({
500,
);
const handleCodeChange = async (value: string) => {
const handleCodeChange = async (newCode: string) => {
if (actionOptions.readonly === true || !isDefined(workflow)) {
return;
}
const toolInputSchema = await onChange('sourceHandlerCode')(newCode);
await getUpdatableWorkflowVersion();
await onCodeChange(value);
if (isDefined(toolInputSchema)) {
await handleUpdateFunctionInputSchema(newCode, toolInputSchema);
}
};
const tabs = [
@@ -382,7 +363,7 @@ export const WorkflowEditActionCode = ({
<StyledFullScreenCodeEditorContainer>
<CodeEditor
height="100%"
value={formValues.code}
value={formValues.sourceHandlerCode}
language="typescript"
onChange={handleCodeChange}
onMount={handleEditorDidMount}
@@ -417,7 +398,7 @@ export const WorkflowEditActionCode = ({
readonly={actionOptions.readonly}
/>
<WorkflowCodeEditor
value={formValues.code}
value={formValues.sourceHandlerCode}
onChange={handleCodeChange}
onMount={handleEditorDidMount}
options={{
@@ -469,7 +450,7 @@ export const WorkflowEditActionCode = ({
? [
<CmdEnterActionButton
title={t`Test`}
onClick={handleRunFunction}
onClick={handleTestFunction}
disabled={isExecuting}
/>,
]
@@ -29,7 +29,7 @@ export const WorkflowReadonlyActionCode = ({
id: logicFunctionId,
});
const { code, loading } = useGetLogicFunctionSourceCode({
const { sourceHandlerCode, loading } = useGetLogicFunctionSourceCode({
logicFunctionId,
});
@@ -60,7 +60,7 @@ export const WorkflowReadonlyActionCode = ({
<StyledCodeEditorContainer>
<CodeEditor
height={343}
value={code ?? undefined}
value={sourceHandlerCode ?? undefined}
language="typescript"
onMount={handleEditorDidMount}
setMarkers={getWrongExportedFunctionMarkers}
@@ -0,0 +1,44 @@
import { type InputSchema } from '@/workflow/types/InputSchema';
import { getFunctionInputFromInputSchema } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getFunctionInputFromInputSchema';
describe('getDefaultFunctionInputFromInputSchema', () => {
it('should init function input properly', () => {
const inputSchema = [
{
type: 'object',
properties: {
a: {
type: 'string',
},
b: {
type: 'number',
},
c: {
type: 'array',
items: { type: 'string' },
},
d: {
type: 'object',
properties: {
da: { type: 'string', enum: ['my', 'enum'] },
db: { type: 'number' },
},
},
e: { type: 'object' },
},
},
] as InputSchema;
const expectedResult = [
{
a: null,
b: null,
c: [],
d: { da: 'my', db: null },
e: {},
},
];
expect(getFunctionInputFromInputSchema(inputSchema)).toEqual(
expectedResult,
);
});
});
@@ -0,0 +1,27 @@
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput';
describe('mergeDefaultFunctionInputAndFunctionInput', () => {
it('should merge properly', () => {
const newInput = {
a: null,
b: null,
c: { cc: null },
d: null,
e: { ee: null },
};
const oldInput = { a: 'a', c: 'c', d: { da: null }, e: { ee: 'ee' } };
const expectedResult = {
a: 'a',
b: null,
c: { cc: null },
d: null,
e: { ee: 'ee' },
};
expect(
mergeDefaultFunctionInputAndFunctionInput({
newInput: newInput,
oldInput: oldInput,
}),
).toEqual(expectedResult);
});
});
@@ -0,0 +1,28 @@
import { type InputSchema } from '@/workflow/types/InputSchema';
import { type FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
import { isDefined } from 'twenty-shared/utils';
import type { InputJsonSchema } from 'twenty-shared/logic-function';
export const getFunctionInputFromInputSchema = (
inputSchema: InputSchema | InputJsonSchema[],
): FunctionInput => {
return inputSchema.map((param) => {
if (
isDefined(param.type) &&
['string', 'number', 'boolean'].includes(param.type)
) {
return param.enum && param.enum.length > 0 ? param.enum[0] : null;
} else if (param.type === 'object') {
const result: FunctionInput = {};
if (isDefined(param.properties)) {
Object.entries(param.properties).forEach(([key, val]) => {
result[key] = getFunctionInputFromInputSchema([val])[0];
});
}
return result;
} else if (param.type === 'array' && isDefined(param.items)) {
return [];
}
return null;
});
};
@@ -0,0 +1,32 @@
import { type FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
import { isObject } from '@sniptt/guards';
export const mergeDefaultFunctionInputAndFunctionInput = ({
newInput,
oldInput,
}: {
newInput: FunctionInput;
oldInput: FunctionInput;
}): FunctionInput => {
const result: FunctionInput = {};
for (const key of Object.keys(newInput)) {
const newValue = newInput[key];
const oldValue = oldInput[key];
if (!(key in oldInput)) {
result[key] = newValue;
} else if (newValue === null && isObject(oldValue)) {
result[key] = null;
} else if (isObject(newValue)) {
result[key] = mergeDefaultFunctionInputAndFunctionInput({
newInput: newValue,
oldInput: isObject(oldValue) ? oldValue : {},
});
} else {
result[key] = oldValue;
}
}
return result;
};
@@ -1,5 +1,5 @@
import { getDefaultFunctionInputFromInputSchema } from '@/logic-functions/utils/getDefaultFunctionInputFromInputSchema';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/logic-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
import { getFunctionInputFromInputSchema } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getFunctionInputFromInputSchema';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput';
import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction';
import { type WorkflowLogicFunctionAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
@@ -56,7 +56,7 @@ export const WorkflowEditActionLogicFunction = ({
? toolInputSchema
: [toolInputSchema];
const defaultInput = getDefaultFunctionInputFromInputSchema(schemaArray)[0];
const defaultInput = getFunctionInputFromInputSchema(schemaArray)[0];
if (!isObject(defaultInput)) {
return action.settings.input.logicFunctionInput ?? {};