Add test tab to tool step (#19760)
So we can use those as variables. When the function input is updated, invalidate the input using an effect. <img width="770" height="635" alt="Capture d’écran 2026-04-16 à 14 45 41" src="https://github.com/user-attachments/assets/cbf0b3e4-baad-424d-8f08-06eb1028abdb" />
This commit is contained in:
+162
-19
@@ -1,20 +1,38 @@
|
||||
import { getFunctionInputFromInputSchema } from 'twenty-shared/workflow';
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
|
||||
import { LogicFunctionLogs } from '@/logic-functions/components/LogicFunctionLogs';
|
||||
import { LogicFunctionTestInputInitEffect } from '@/logic-functions/components/LogicFunctionTestInputInitEffect';
|
||||
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
|
||||
import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { type WorkflowLogicFunctionAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { WorkflowEditActionCodeFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields';
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||
import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/setNestedValue';
|
||||
import { WORKFLOW_LOGIC_FUNCTION_ACTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/logic-function-action/constants/WorkflowLogicFunctionActionTabListComponentId';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { useMemo } from 'react';
|
||||
import { getOutputSchemaFromValue } from 'twenty-shared/logic-function';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Callout } from 'twenty-ui/display';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { getFunctionInputFromInputSchema } from 'twenty-shared/workflow';
|
||||
import {
|
||||
Callout,
|
||||
IconPlayerPlay,
|
||||
IconSettingsAutomation,
|
||||
} from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
const INPUT_TAB_ID = 'input';
|
||||
const TEST_TAB_ID = 'test';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -22,6 +40,17 @@ const StyledContainer = styled.div`
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledTabListContainer = styled.div`
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
padding-left: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledResultContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type WorkflowEditActionLogicFunctionProps = {
|
||||
action: WorkflowLogicFunctionAction;
|
||||
actionOptions:
|
||||
@@ -46,6 +75,11 @@ export const WorkflowEditActionLogicFunction = ({
|
||||
id: logicFunctionId,
|
||||
});
|
||||
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
WORKFLOW_LOGIC_FUNCTION_ACTION_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
|
||||
const functionInput = useMemo(() => {
|
||||
const toolInputSchema = logicFunction?.toolInputSchema;
|
||||
|
||||
@@ -86,6 +120,34 @@ export const WorkflowEditActionLogicFunction = ({
|
||||
500,
|
||||
);
|
||||
|
||||
const updateOutputSchemaFromTestResult = (testResult: object) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOutputSchema = getOutputSchemaFromValue(testResult);
|
||||
|
||||
updateAction({
|
||||
...action,
|
||||
settings: { ...action.settings, outputSchema: newOutputSchema },
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
executeLogicFunction,
|
||||
isExecuting,
|
||||
logicFunctionTestData,
|
||||
updateLogicFunctionInput,
|
||||
} = useExecuteLogicFunction({
|
||||
logicFunctionId,
|
||||
callback: updateOutputSchemaFromTestResult,
|
||||
});
|
||||
|
||||
const testInput = mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput: functionInput,
|
||||
oldInput: logicFunctionTestData.input,
|
||||
});
|
||||
|
||||
const handleInputChange = (value: unknown, path: string[]) => {
|
||||
const updatedFunctionInput = setNestedValue(functionInput, path, value);
|
||||
|
||||
@@ -100,34 +162,115 @@ export const WorkflowEditActionLogicFunction = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleTestInputChange = (value: unknown, path: string[]) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedTestFunctionInput = setNestedValue(testInput, path, value);
|
||||
|
||||
updateLogicFunctionInput(updatedTestFunctionInput);
|
||||
};
|
||||
|
||||
const handleTestFunction = async () => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
await executeLogicFunction();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasInputFields = Object.keys(functionInput).length > 0;
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: INPUT_TAB_ID,
|
||||
title: t`Input`,
|
||||
Icon: IconSettingsAutomation,
|
||||
},
|
||||
{
|
||||
id: TEST_TAB_ID,
|
||||
title: t`Test`,
|
||||
Icon: IconPlayerPlay,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<LogicFunctionTestInputInitEffect logicFunctionId={logicFunctionId} />
|
||||
<StyledTabListContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={
|
||||
WORKFLOW_LOGIC_FUNCTION_ACTION_TAB_LIST_COMPONENT_ID
|
||||
}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
<WorkflowStepBody>
|
||||
<StyledContainer>
|
||||
{hasInputFields ? (
|
||||
{activeTabId === TEST_TAB_ID ? (
|
||||
<>
|
||||
<WorkflowEditActionCodeFields
|
||||
functionInput={functionInput}
|
||||
functionInput={testInput}
|
||||
onInputChange={handleTestInputChange}
|
||||
readonly={actionOptions.readonly}
|
||||
onInputChange={handleInputChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
<Callout
|
||||
variant={'neutral'}
|
||||
title={t`No input fields for this action`}
|
||||
description={t`You can see the function logic in your application settings.`}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
<StyledResultContainer>
|
||||
<InputLabel>{t`Result`}</InputLabel>
|
||||
<LogicFunctionExecutionResult
|
||||
logicFunctionTestData={logicFunctionTestData}
|
||||
isTesting={isExecuting}
|
||||
/>
|
||||
</StyledResultContainer>
|
||||
{logicFunctionTestData.output.logs.length > 0 && (
|
||||
<StyledResultContainer>
|
||||
<LogicFunctionLogs
|
||||
componentInstanceId={`workflow-edit-action-logs-${action.id}`}
|
||||
value={isExecuting ? '' : logicFunctionTestData.output.logs}
|
||||
/>
|
||||
</StyledResultContainer>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<StyledContainer>
|
||||
{hasInputFields ? (
|
||||
<WorkflowEditActionCodeFields
|
||||
functionInput={functionInput}
|
||||
readonly={actionOptions.readonly}
|
||||
onInputChange={handleInputChange}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
<Callout
|
||||
variant={'neutral'}
|
||||
title={t`No input fields for this action`}
|
||||
description={t`You can see the function logic in your application settings.`}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
{!actionOptions.readonly && (
|
||||
<WorkflowStepFooter
|
||||
stepId={action.id}
|
||||
additionalActions={
|
||||
activeTabId === TEST_TAB_ID
|
||||
? [
|
||||
<WorkflowStepCmdEnterButton
|
||||
title={t`Test`}
|
||||
onClick={handleTestFunction}
|
||||
disabled={isExecuting}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const WORKFLOW_LOGIC_FUNCTION_ACTION_TAB_LIST_COMPONENT_ID =
|
||||
'workflow-logic-function-action-tab-list-component-id';
|
||||
+13
@@ -61,6 +61,15 @@ describe('computeStepOutputSchema', () => {
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for LOGIC_FUNCTION step type', () => {
|
||||
const result = computeStepOutputSchema({
|
||||
step: { type: 'LOGIC_FUNCTION', settings: {} } as any,
|
||||
objectMetadataItems: [],
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DATABASE_EVENT trigger', () => {
|
||||
@@ -466,6 +475,10 @@ describe('shouldComputeOutputSchemaOnFrontend', () => {
|
||||
expect(shouldComputeOutputSchemaOnFrontend('ITERATOR')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for LOGIC_FUNCTION', () => {
|
||||
expect(shouldComputeOutputSchemaOnFrontend('LOGIC_FUNCTION')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for DATABASE_EVENT', () => {
|
||||
expect(shouldComputeOutputSchemaOnFrontend('DATABASE_EVENT')).toBe(true);
|
||||
});
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ const PERSISTED_OUTPUT_SCHEMA_TYPES = [
|
||||
'AI_AGENT',
|
||||
'CODE',
|
||||
'HTTP_REQUEST',
|
||||
'LOGIC_FUNCTION',
|
||||
'WEBHOOK',
|
||||
'ITERATOR',
|
||||
];
|
||||
|
||||
+14
-1
@@ -224,9 +224,22 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
type: WorkflowActionType.LOGIC_FUNCTION,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
outputSchema: {
|
||||
link: {
|
||||
isLeaf: true,
|
||||
icon: 'IconVariable',
|
||||
tab: 'test',
|
||||
label: 'Generate Function Output',
|
||||
},
|
||||
_outputSchemaType: 'LINK',
|
||||
},
|
||||
input: {
|
||||
logicFunctionId,
|
||||
logicFunctionInput: {},
|
||||
logicFunctionInput: isDefined(flatLogicFunction.toolInputSchema)
|
||||
? (getFunctionInputFromInputSchema([
|
||||
flatLogicFunction.toolInputSchema,
|
||||
])[0] ?? {})
|
||||
: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user