diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx index c8c7f5b9f1c..69b144d8155 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx @@ -8,10 +8,8 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo import { type WorkflowCodeAction } from '@/workflow/types/Workflow'; import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/setNestedValue'; -import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton'; import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult'; import { LogicFunctionLogs } from '@/logic-functions/components/LogicFunctionLogs'; -import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput'; 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'; @@ -19,15 +17,19 @@ import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotke import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; +import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton'; import { WorkflowCodeEditor } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowCodeEditor'; import { WorkflowEditActionCodeFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields'; import { WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowLogicFunctionTabListComponentId'; import { WorkflowLogicFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowLogicFunctionTabId'; import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers'; +import { mergeDefaultFunctionInputAndFunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/mergeDefaultFunctionInputAndFunctionInput'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; +import { LogicFunctionTestInputInitEffect } from '@/logic-functions/components/LogicFunctionTestInputInitEffect'; +import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction'; 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'; @@ -35,20 +37,18 @@ import { type editor } from 'monaco-editor'; import { AutoTypings } from 'monaco-editor-auto-typings'; import { useState } from 'react'; import { Key } from 'ts-key-enum'; -import { isDefined } from 'twenty-shared/utils'; import { getOutputSchemaFromValue, jsonSchemaToInputSchema, type InputJsonSchema, } from 'twenty-shared/logic-function'; +import { isDefined } from 'twenty-shared/utils'; +import { getFunctionInputFromInputSchema } from 'twenty-shared/workflow'; import { IconCode, IconPlayerPlay } from 'twenty-ui/display'; import { CodeEditor } from 'twenty-ui/input'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; import { useIsMobile } from 'twenty-ui/utilities'; import { useDebouncedCallback } from 'use-debounce'; -import { getFunctionInputFromInputSchema } from 'twenty-shared/workflow'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { LogicFunctionTestInputInitEffect } from '@/logic-functions/components/LogicFunctionTestInputInitEffect'; -import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction'; const StyledCodeEditorContainer = styled.div` display: flex; @@ -341,6 +341,7 @@ export const WorkflowEditActionCode = ({
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx index c423eacbce1..deb44af1785 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx @@ -1,11 +1,18 @@ +import { FormBooleanFieldInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldInput'; import { FormNestedFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormNestedFieldInputContainer'; +import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput'; +import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent'; import { InputLabel } from '@/ui/input/components/InputLabel'; -import { type FunctionInput } from 'twenty-shared/workflow'; +import { getInputSchemaPropertyAtPath } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getInputSchemaPropertyAtPath'; +import { getWorkflowCodeFieldsEnumSelectOptions } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsEnumSelectOptions'; +import { getWorkflowCodeFieldsLeafKind } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; -import { isObject } from '@sniptt/guards'; +import { isNonEmptyArray, isObject } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; +import { type FunctionInput, type InputSchema } from 'twenty-shared/workflow'; import { themeCssVariables } from 'twenty-ui/theme-constants'; const StyledContainer = styled.div<{ fullWidth?: boolean }>` @@ -26,6 +33,7 @@ type WorkflowEditActionCodeFieldsProps = { onInputChange?: (value: any, path: string[]) => void | Promise; VariablePicker?: VariablePickerComponent; fullWidth?: boolean; + inputSchema?: InputSchema; }; export const WorkflowEditActionCodeFields = ({ @@ -35,6 +43,7 @@ export const WorkflowEditActionCodeFields = ({ onInputChange, VariablePicker, fullWidth, + inputSchema, }: WorkflowEditActionCodeFieldsProps) => { return ( @@ -54,12 +63,84 @@ export const WorkflowEditActionCodeFields = ({ onInputChange={onInputChange} VariablePicker={VariablePicker} fullWidth={fullWidth} + inputSchema={inputSchema} />
); } + const schemaProperty = getInputSchemaPropertyAtPath( + inputSchema, + currentPath, + ); + const leafKind = getWorkflowCodeFieldsLeafKind(schemaProperty); + + if (leafKind === 'boolean') { + return ( + onInputChange?.(value, currentPath)} + VariablePicker={VariablePicker} + /> + ); + } + + if (leafKind === 'number') { + return ( + onInputChange?.(value, currentPath)} + VariablePicker={VariablePicker} + /> + ); + } + + if (leafKind === 'enum' && isDefined(schemaProperty)) { + const enumOptions = + getWorkflowCodeFieldsEnumSelectOptions(schemaProperty); + + if (isNonEmptyArray(enumOptions)) { + return ( + onInputChange?.(value, currentPath)} + VariablePicker={VariablePicker} + options={enumOptions} + /> + ); + } + } + return ( onInputChange?.(value, currentPath)} VariablePicker={VariablePicker} + multiline={schemaProperty?.multiline === true} /> ); })} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionCode.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionCode.tsx index 3b240a6b683..cf13f18afcd 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionCode.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionCode.tsx @@ -1,6 +1,7 @@ import { useGetAvailablePackages } from '@/logic-functions/hooks/useGetAvailablePackages'; -import { type WorkflowCodeAction } from '@/workflow/types/Workflow'; import { useGetLogicFunctionSourceCode } from '@/logic-functions/hooks/useGetLogicFunctionSourceCode'; +import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction'; +import { type WorkflowCodeAction } from '@/workflow/types/Workflow'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowEditActionCodeFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields'; @@ -29,6 +30,10 @@ export const WorkflowReadonlyActionCode = ({ id: logicFunctionId, }); + const { logicFunction } = useGetOneLogicFunction({ + id: logicFunctionId, + }); + const { sourceHandlerCode, loading } = useGetLogicFunctionSourceCode({ logicFunctionId, }); @@ -55,6 +60,9 @@ export const WorkflowReadonlyActionCode = ({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getInputSchemaPropertyAtPath.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getInputSchemaPropertyAtPath.test.ts new file mode 100644 index 00000000000..668b8ede6fa --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getInputSchemaPropertyAtPath.test.ts @@ -0,0 +1,56 @@ +import { type InputSchema } from 'twenty-shared/workflow'; + +import { getInputSchemaPropertyAtPath } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getInputSchemaPropertyAtPath'; + +describe('getInputSchemaPropertyAtPath', () => { + const inputSchema = [ + { + type: 'object' as const, + properties: { + slackChannelId: { type: 'string' as const }, + messageFormat: { + type: 'string' as const, + enum: ['plain', 'markdown'], + }, + count: { type: 'number' as const }, + nested: { + type: 'object' as const, + properties: { + inner: { type: 'string' as const }, + }, + }, + }, + }, + ] as InputSchema; + + it('should return property at top-level path', () => { + expect( + getInputSchemaPropertyAtPath(inputSchema, ['messageFormat']), + ).toEqual({ + type: 'string', + enum: ['plain', 'markdown'], + }); + }); + + it('should return property at nested path', () => { + expect( + getInputSchemaPropertyAtPath(inputSchema, ['nested', 'inner']), + ).toEqual({ + type: 'string', + }); + }); + + it('should return undefined when path is invalid', () => { + expect( + getInputSchemaPropertyAtPath(inputSchema, ['missing']), + ).toBeUndefined(); + expect( + getInputSchemaPropertyAtPath(inputSchema, ['slackChannelId', 'tooDeep']), + ).toBeUndefined(); + }); + + it('should return undefined when inputSchema is missing or empty', () => { + expect(getInputSchemaPropertyAtPath(undefined, ['a'])).toBeUndefined(); + expect(getInputSchemaPropertyAtPath([], ['a'])).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getWorkflowCodeFieldsEnumSelectOptions.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getWorkflowCodeFieldsEnumSelectOptions.test.ts new file mode 100644 index 00000000000..83fe6497a84 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getWorkflowCodeFieldsEnumSelectOptions.test.ts @@ -0,0 +1,22 @@ +import { getWorkflowCodeFieldsEnumSelectOptions } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsEnumSelectOptions'; + +describe('getWorkflowCodeFieldsEnumSelectOptions', () => { + it('should build select options from schema enum', () => { + expect( + getWorkflowCodeFieldsEnumSelectOptions({ + type: 'string', + enum: ['plain', 'markdown'], + }), + ).toEqual([ + { value: 'plain', label: 'plain' }, + { value: 'markdown', label: 'markdown' }, + ]); + }); + + it('should return empty array when property has no enum', () => { + expect(getWorkflowCodeFieldsEnumSelectOptions({ type: 'string' })).toEqual( + [], + ); + expect(getWorkflowCodeFieldsEnumSelectOptions(undefined)).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getWorkflowCodeFieldsLeafKind.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getWorkflowCodeFieldsLeafKind.test.ts new file mode 100644 index 00000000000..4c68d33d869 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/__tests__/getWorkflowCodeFieldsLeafKind.test.ts @@ -0,0 +1,36 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { getWorkflowCodeFieldsLeafKind } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind'; + +describe('getWorkflowCodeFieldsLeafKind', () => { + it('should map schema types to leaf editor kind', () => { + expect(getWorkflowCodeFieldsLeafKind({ type: 'boolean' })).toBe('boolean'); + expect( + getWorkflowCodeFieldsLeafKind({ type: FieldMetadataType.BOOLEAN }), + ).toBe('boolean'); + expect(getWorkflowCodeFieldsLeafKind({ type: 'number' })).toBe('number'); + expect( + getWorkflowCodeFieldsLeafKind({ type: FieldMetadataType.NUMBER }), + ).toBe('number'); + expect( + getWorkflowCodeFieldsLeafKind({ type: FieldMetadataType.NUMERIC }), + ).toBe('number'); + expect(getWorkflowCodeFieldsLeafKind({ type: 'string' })).toBe('text'); + expect( + getWorkflowCodeFieldsLeafKind({ + type: 'string', + enum: ['plain', 'markdown'], + }), + ).toBe('enum'); + expect( + getWorkflowCodeFieldsLeafKind({ + type: FieldMetadataType.TEXT, + enum: ['a', 'b'], + }), + ).toBe('enum'); + expect(getWorkflowCodeFieldsLeafKind({ type: 'string', enum: [] })).toBe( + 'text', + ); + expect(getWorkflowCodeFieldsLeafKind(undefined)).toBe('text'); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getInputSchemaPropertyAtPath.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getInputSchemaPropertyAtPath.ts new file mode 100644 index 00000000000..7fa5fa1d1d2 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getInputSchemaPropertyAtPath.ts @@ -0,0 +1,30 @@ +import { + type InputSchema, + type InputSchemaProperty, +} from 'twenty-shared/workflow'; +import { isDefined } from 'twenty-shared/utils'; + +export const getInputSchemaPropertyAtPath = ( + inputSchema: InputSchema | undefined, + path: string[], +): InputSchemaProperty | undefined => { + if (!isDefined(inputSchema) || inputSchema.length === 0) { + return undefined; + } + + let current: InputSchemaProperty | undefined = inputSchema[0]; + + for (const segment of path) { + if (!isDefined(current)) { + return undefined; + } + + if (current.type === 'object' && isDefined(current.properties?.[segment])) { + current = current.properties[segment]; + } else { + return undefined; + } + } + + return current; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsEnumSelectOptions.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsEnumSelectOptions.ts new file mode 100644 index 00000000000..6c2af84526e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsEnumSelectOptions.ts @@ -0,0 +1,17 @@ +import { isNonEmptyArray } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; +import { type InputSchemaProperty } from 'twenty-shared/workflow'; +import { type SelectOption } from 'twenty-ui/input'; + +export const getWorkflowCodeFieldsEnumSelectOptions = ( + property: InputSchemaProperty | undefined, +): SelectOption[] => { + if (!isDefined(property) || !isNonEmptyArray(property.enum)) { + return []; + } + + return property.enum.map((value) => ({ + value, + label: value, + })); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind.ts new file mode 100644 index 00000000000..247455ee2d3 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/utils/getWorkflowCodeFieldsLeafKind.ts @@ -0,0 +1,33 @@ +import { isNonEmptyArray } from '@sniptt/guards'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { type InputSchemaProperty } from 'twenty-shared/workflow'; + +type WorkflowCodeFieldsLeafKind = 'boolean' | 'enum' | 'number' | 'text'; + +export const getWorkflowCodeFieldsLeafKind = ( + property: InputSchemaProperty | undefined, +): WorkflowCodeFieldsLeafKind => { + if (!isDefined(property)) { + return 'text'; + } + + if ( + (property.type === 'string' || property.type === FieldMetadataType.TEXT) && + isNonEmptyArray(property.enum) + ) { + return 'enum'; + } + + switch (property.type) { + case 'boolean': + case FieldMetadataType.BOOLEAN: + return 'boolean'; + case 'number': + case FieldMetadataType.NUMBER: + case FieldMetadataType.NUMERIC: + return 'number'; + default: + return 'text'; + } +}; 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 741d914537f..4abb86e0147 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 @@ -213,6 +213,9 @@ export const WorkflowEditActionLogicFunction = ({ <> @@ -237,6 +240,9 @@ export const WorkflowEditActionLogicFunction = ({ {hasInputFields ? ( { enum: ['a', 'b'], }); }); + + it('preserves multiline on string properties when true', () => { + const result = jsonSchemaToInputSchema({ + type: 'object', + properties: { + body: { type: 'string', multiline: true }, + }, + }); + + expect(result[0].properties?.body).toEqual({ + type: 'string', + multiline: true, + }); + }); + + it('does not set multiline when false or omitted', () => { + const result = jsonSchemaToInputSchema({ + type: 'object', + properties: { + title: { type: 'string', multiline: false }, + other: { type: 'string' }, + }, + }); + + expect(result[0].properties?.title).toEqual({ type: 'string' }); + expect(result[0].properties?.other).toEqual({ type: 'string' }); + }); }); diff --git a/packages/twenty-shared/src/logic-function/input-json-schema.type.ts b/packages/twenty-shared/src/logic-function/input-json-schema.type.ts index 7d3fecdf84d..ec0f8179862 100644 --- a/packages/twenty-shared/src/logic-function/input-json-schema.type.ts +++ b/packages/twenty-shared/src/logic-function/input-json-schema.type.ts @@ -15,4 +15,5 @@ export type InputJsonSchema = { additionalProperties?: boolean | InputJsonSchema; minimum?: number; maximum?: number; + multiline?: boolean; }; diff --git a/packages/twenty-shared/src/logic-function/json-schema-to-input-schema.ts b/packages/twenty-shared/src/logic-function/json-schema-to-input-schema.ts index eea64416a74..dd2bb5cda18 100644 --- a/packages/twenty-shared/src/logic-function/json-schema-to-input-schema.ts +++ b/packages/twenty-shared/src/logic-function/json-schema-to-input-schema.ts @@ -46,6 +46,10 @@ const convertProperty = (jsonSchema: InputJsonSchema): InputSchemaProperty => { ); } + if (jsonSchema.multiline === true) { + property.multiline = true; + } + return property; }; diff --git a/packages/twenty-shared/src/workflow/types/InputSchema.ts b/packages/twenty-shared/src/workflow/types/InputSchema.ts index 726019a14af..83cffd9a5c2 100644 --- a/packages/twenty-shared/src/workflow/types/InputSchema.ts +++ b/packages/twenty-shared/src/workflow/types/InputSchema.ts @@ -1,5 +1,5 @@ -import { type LeafType, type NodeType } from '@/workflow'; import { type FieldMetadataType } from '@/types'; +import { type LeafType, type NodeType } from '@/workflow'; export type InputSchemaPropertyType = LeafType | NodeType | FieldMetadataType; @@ -8,6 +8,7 @@ export type InputSchemaProperty = { enum?: string[]; items?: InputSchemaProperty; properties?: Properties; + multiline?: boolean; }; type Properties = {