diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction.tsx index c33752df83..b1d1da4963 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction.tsx @@ -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 ( <> + + + + - - {hasInputFields ? ( + {activeTabId === TEST_TAB_ID ? ( + <> - ) : ( - - )} - + + {t`Result`} + + + {logicFunctionTestData.output.logs.length > 0 && ( + + + + )} + + ) : ( + + {hasInputFields ? ( + + ) : ( + + )} + + )} - {!actionOptions.readonly && } + {!actionOptions.readonly && ( + , + ] + : [] + } + /> + )} ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/constants/WorkflowLogicFunctionActionTabListComponentId.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/constants/WorkflowLogicFunctionActionTabListComponentId.ts new file mode 100644 index 0000000000..be435dcfd9 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/logic-function-action/constants/WorkflowLogicFunctionActionTabListComponentId.ts @@ -0,0 +1,2 @@ +export const WORKFLOW_LOGIC_FUNCTION_ACTION_TAB_LIST_COMPONENT_ID = + 'workflow-logic-function-action-tab-list-component-id'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/computeStepOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/computeStepOutputSchema.test.ts index 90282d0538..d3d6f40ad4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/computeStepOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/computeStepOutputSchema.test.ts @@ -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); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts index 103a278cb4..0a16fa6e1a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts @@ -17,6 +17,7 @@ const PERSISTED_OUTPUT_SCHEMA_TYPES = [ 'AI_AGENT', 'CODE', 'HTTP_REQUEST', + 'LOGIC_FUNCTION', 'WEBHOOK', 'ITERATOR', ]; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts index 8dd4915835..84fe0221bf 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts @@ -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] ?? {}) + : {}, }, }, },