App logic function as step (#17525)

- Add logic function as step
- Rename previous one as Code

<img width="494" height="573" alt="Capture d’écran 2026-01-28 à 16 15
29"
src="https://github.com/user-attachments/assets/82f7e720-20da-4926-b5bc-94a33cd1f53a"
/>
<img width="494" height="264" alt="Capture d’écran 2026-01-28 à 16 15
39"
src="https://github.com/user-attachments/assets/a0444ae9-8c50-418d-8c5f-68c4da08fce7"
/>
<img width="494" height="264" alt="Capture d’écran 2026-01-28 à 16 15
58"
src="https://github.com/user-attachments/assets/3025db09-4e59-421e-8dc5-f3a7a7326554"
/>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Thomas Trompette
2026-01-29 11:59:24 +01:00
committed by GitHub
parent 22c7e207e3
commit 3ad7a9ac94
42 changed files with 551 additions and 90 deletions
@@ -5,7 +5,7 @@ import {
} from '@/workflow/types/Workflow';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
import { WorkflowActionLogicFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionLogicFunction';
import { WorkflowActionCode } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionCode';
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
@@ -15,10 +15,11 @@ import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workfl
import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay';
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionIfElse } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElse';
import { WorkflowEditActionFormFiller } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller';
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
import { WorkflowEditActionIfElse } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElse';
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/components/WorkflowEditActionIterator';
import { WorkflowEditActionLogicFunction } from '@/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManual } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManual';
@@ -106,7 +107,18 @@ export const WorkflowRunStepNodeDetail = ({
switch (stepDefinition.definition.type) {
case 'CODE': {
return (
<WorkflowActionLogicFunction
<WorkflowActionCode
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'LOGIC_FUNCTION': {
return (
<WorkflowEditActionLogicFunction
key={stepId}
action={stepDefinition.definition}
actionOptions={{
@@ -4,7 +4,7 @@ import {
} from '@/workflow/types/Workflow';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
import { WorkflowActionLogicFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionLogicFunction';
import { WorkflowActionCode } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionCode';
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
@@ -14,10 +14,11 @@ import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workfl
import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay';
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionIfElse } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElse';
import { WorkflowEditActionFormBuilder } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
import { WorkflowEditActionIfElse } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElse';
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/components/WorkflowEditActionIterator';
import { WorkflowEditActionLogicFunction } from '@/workflow/workflow-steps/workflow-actions/logic-function-action/components/WorkflowEditActionLogicFunction';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManual } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManual';
@@ -108,7 +109,16 @@ export const WorkflowStepDetail = ({
switch (stepDefinition.definition.type) {
case 'CODE': {
return (
<WorkflowActionLogicFunction
<WorkflowActionCode
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
);
}
case 'LOGIC_FUNCTION': {
return (
<WorkflowEditActionLogicFunction
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
@@ -36,6 +36,8 @@ export const useCreateStep = () => {
connectionOptions,
shouldSelectNode = true,
workflowVersionId: providedWorkflowVersionId,
logicFunctionId,
name,
}: {
newStepType: WorkflowActionType;
parentStepId: string | undefined;
@@ -44,6 +46,8 @@ export const useCreateStep = () => {
connectionOptions?: WorkflowStepConnectionOptions;
shouldSelectNode?: boolean;
workflowVersionId?: string;
logicFunctionId?: string;
name?: string;
}) => {
if (isLoading === true) {
return;
@@ -65,6 +69,8 @@ export const useCreateStep = () => {
nextStepId,
position,
parentStepConnectionOptions: connectionOptions,
logicFunctionId,
name,
})
)?.data?.createWorkflowVersionStep;
@@ -2,7 +2,7 @@ import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
import { lazy, Suspense } from 'react';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
type WorkflowActionLogicFunctionProps = {
type WorkflowActionCodeProps = {
action: WorkflowCodeAction;
actionOptions:
| {
@@ -14,35 +14,32 @@ type WorkflowActionLogicFunctionProps = {
};
};
const WorkflowEditActionLogicFunction = lazy(() =>
const WorkflowEditActionCode = lazy(() =>
import(
'@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionLogicFunction'
'@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode'
).then((module) => ({
default: module.WorkflowEditActionLogicFunction,
default: module.WorkflowEditActionCode,
})),
);
const WorkflowReadonlyActionLogicFunction = lazy(() =>
const WorkflowReadonlyActionCode = lazy(() =>
import(
'@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionLogicFunction'
'@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionCode'
).then((module) => ({
default: module.WorkflowReadonlyActionLogicFunction,
default: module.WorkflowReadonlyActionCode,
})),
);
export const WorkflowActionLogicFunction = ({
export const WorkflowActionCode = ({
action,
actionOptions,
}: WorkflowActionLogicFunctionProps) => {
}: WorkflowActionCodeProps) => {
return (
<Suspense fallback={<RightDrawerSkeletonLoader />}>
{actionOptions.readonly ? (
<WorkflowReadonlyActionLogicFunction action={action} />
<WorkflowReadonlyActionCode action={action} />
) : (
<WorkflowEditActionLogicFunction
action={action}
actionOptions={actionOptions}
/>
<WorkflowEditActionCode action={action} actionOptions={actionOptions} />
)}
</Suspense>
);
@@ -24,7 +24,7 @@ const StyledFullScreenButtonContainer = styled.div`
z-index: 1;
`;
type WorkflowLogicFunctionCodeEditorProps = {
type WorkflowCodeEditorProps = {
value?: string;
onChange: (value: string) => void;
onMount: (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => void;
@@ -34,7 +34,7 @@ type WorkflowLogicFunctionCodeEditorProps = {
onEnterFullScreen?: () => void;
};
export const WorkflowLogicFunctionCodeEditor = ({
export const WorkflowCodeEditor = ({
value,
onChange,
onMount,
@@ -42,7 +42,7 @@ export const WorkflowLogicFunctionCodeEditor = ({
readonly = false,
fullScreenMode = false,
onEnterFullScreen,
}: WorkflowLogicFunctionCodeEditorProps) => {
}: WorkflowCodeEditorProps) => {
const { t } = useLingui();
return (
@@ -25,8 +25,8 @@ import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotke
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionLogicFunctionFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionLogicFunctionFields';
import { WorkflowLogicFunctionCodeEditor } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowLogicFunctionCodeEditor';
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 {
type LogicFunctionTestData,
@@ -77,7 +77,7 @@ const StyledFullScreenCodeEditorContainer = styled.div`
min-height: 0;
`;
type WorkflowEditActionLogicFunctionProps = {
type WorkflowEditActionCodeProps = {
action: WorkflowCodeAction;
actionOptions:
| {
@@ -93,10 +93,10 @@ type LogicFunctionInputFormData = {
[field: string]: string | LogicFunctionInputFormData;
};
export const WorkflowEditActionLogicFunction = ({
export const WorkflowEditActionCode = ({
action,
actionOptions,
}: WorkflowEditActionLogicFunctionProps) => {
}: WorkflowEditActionCodeProps) => {
const { t } = useLingui();
const [isFullScreen, setIsFullScreen] = useState(false);
const isMobile = useIsMobile();
@@ -391,7 +391,7 @@ export const WorkflowEditActionLogicFunction = ({
const fullScreenOverlay = renderFullScreenModal(
<div data-globally-prevent-click-outside="true">
<WorkflowEditActionLogicFunctionFields
<WorkflowEditActionCodeFields
functionInput={functionInput}
VariablePicker={WorkflowVariablePicker}
onInputChange={handleInputChange}
@@ -428,13 +428,13 @@ export const WorkflowEditActionLogicFunction = ({
<WorkflowStepBody>
{activeTabId === WorkflowLogicFunctionTabId.CODE && (
<>
<WorkflowEditActionLogicFunctionFields
<WorkflowEditActionCodeFields
functionInput={functionInput}
VariablePicker={WorkflowVariablePicker}
onInputChange={handleInputChange}
readonly={actionOptions.readonly}
/>
<WorkflowLogicFunctionCodeEditor
<WorkflowCodeEditor
value={indexFileContent}
onChange={handleCodeChange}
onMount={handleEditorDidMount}
@@ -452,7 +452,7 @@ export const WorkflowEditActionLogicFunction = ({
)}
{activeTabId === WorkflowLogicFunctionTabId.TEST && (
<>
<WorkflowEditActionLogicFunctionFields
<WorkflowEditActionCodeFields
functionInput={logicFunctionTestData.input}
onInputChange={handleTestInputChange}
readonly={actionOptions.readonly}
@@ -3,38 +3,40 @@ import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/c
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { type FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
import { t } from '@lingui/core/macro';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isObject } from '@sniptt/guards';
const StyledContainer = styled.div`
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
display: flex;
gap: ${({ theme }) => theme.spacing(3)};
flex-wrap: wrap;
> * {
flex: 1;
min-width: 200px;
min-width: ${({ fullWidth }) => (fullWidth ? '100%' : '200px')};
}
`;
type WorkflowEditActionLogicFunctionFieldsProps = {
type WorkflowEditActionCodeFieldsProps = {
functionInput: FunctionInput;
path?: string[];
readonly?: boolean;
onInputChange?: (value: any, path: string[]) => void | Promise<void>;
VariablePicker?: VariablePickerComponent;
fullWidth?: boolean;
};
export const WorkflowEditActionLogicFunctionFields = ({
export const WorkflowEditActionCodeFields = ({
functionInput,
path = [],
readonly,
onInputChange,
VariablePicker,
}: WorkflowEditActionLogicFunctionFieldsProps) => {
fullWidth,
}: WorkflowEditActionCodeFieldsProps) => {
return (
<StyledContainer>
<StyledContainer fullWidth={fullWidth}>
{Object.entries(functionInput).map(([inputKey, inputValue]) => {
const currentPath = [...path, inputKey];
const pathKey = currentPath.join('.');
@@ -44,12 +46,13 @@ export const WorkflowEditActionLogicFunctionFields = ({
<div key={pathKey}>
<InputLabel>{inputKey}</InputLabel>
<FormNestedFieldInputContainer>
<WorkflowEditActionLogicFunctionFields
<WorkflowEditActionCodeFields
functionInput={inputValue}
path={currentPath}
readonly={readonly}
onInputChange={onInputChange}
VariablePicker={VariablePicker}
fullWidth={fullWidth}
/>
</FormNestedFieldInputContainer>
</div>
@@ -3,28 +3,28 @@ import { useLogicFunctionUpdateFormState } from '@/settings/logic-functions/hook
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
import { INDEX_FILE_NAME } from '@/logic-functions/constants/IndexFileName';
import { SOURCE_FOLDER_NAME } from '@/logic-functions/constants/SourceFolderName';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionLogicFunctionFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionLogicFunctionFields';
import { WorkflowEditActionCodeFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
import styled from '@emotion/styled';
import { type Monaco } from '@monaco-editor/react';
import { type editor } from 'monaco-editor';
import { AutoTypings } from 'monaco-editor-auto-typings';
import { CodeEditor } from 'twenty-ui/input';
import { SOURCE_FOLDER_NAME } from '@/logic-functions/constants/SourceFolderName';
const StyledCodeEditorContainer = styled.div`
display: flex;
flex-direction: column;
`;
type WorkflowReadonlyActionLogicFunctionProps = {
type WorkflowReadonlyActionCodeProps = {
action: WorkflowCodeAction;
};
export const WorkflowReadonlyActionLogicFunction = ({
export const WorkflowReadonlyActionCode = ({
action,
}: WorkflowReadonlyActionLogicFunctionProps) => {
}: WorkflowReadonlyActionCodeProps) => {
const logicFunctionId = action.settings.input.logicFunctionId;
const logicFunctionVersion = action.settings.input.logicFunctionVersion;
@@ -63,7 +63,7 @@ export const WorkflowReadonlyActionLogicFunction = ({
return (
<>
<WorkflowStepBody>
<WorkflowEditActionLogicFunctionFields
<WorkflowEditActionCodeFields
functionInput={action.settings.input.logicFunctionInput}
readonly
/>
@@ -1,5 +1,5 @@
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
import { WorkflowEditActionLogicFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionLogicFunction';
import { WorkflowEditActionCode } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { graphql, HttpResponse } from 'msw';
import { fn } from 'storybook/test';
@@ -75,9 +75,9 @@ const CONFIGURED_ACTION: WorkflowCodeAction = {
},
};
const meta: Meta<typeof WorkflowEditActionLogicFunction> = {
const meta: Meta<typeof WorkflowEditActionCode> = {
title: 'Modules/Workflow/Actions/Code/EditAction',
component: WorkflowEditActionLogicFunction,
component: WorkflowEditActionCode,
parameters: {
msw: {
handlers: [
@@ -138,7 +138,7 @@ const meta: Meta<typeof WorkflowEditActionLogicFunction> = {
export default meta;
type Story = StoryObj<typeof WorkflowEditActionLogicFunction>;
type Story = StoryObj<typeof WorkflowEditActionCode>;
export const Default: Story = {
args: {
@@ -2,7 +2,6 @@ import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-ty
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
const StyledMessageContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(4)};
@@ -40,24 +39,28 @@ const StyledFieldContainer = styled.div`
width: 100%;
`;
export const WorkflowFormEmptyMessage = () => {
const { t } = useLingui();
export const WorkflowMessage = ({
title,
description,
}: {
title: string;
description: string;
}) => {
return (
<StyledMessageContainer>
<FormFieldInputContainer>
<FormFieldInputRowContainer multiline maxHeight={124}>
<FormFieldInputInnerContainer
formFieldInputInstanceId="empty-form-message"
formFieldInputInstanceId="workflow-message"
hasRightElement={false}
>
<StyledFieldContainer>
<StyledMessageContentContainer>
<StyledMessageTitle data-testid="empty-form-message-title">
{t`Add inputs to your form`}
<StyledMessageTitle data-testid="workflow-message-title">
{title}
</StyledMessageTitle>
<StyledMessageDescription data-testid="empty-form-message-description">
{t`Click on "Add Field" below to add the first input to your form. The form will pop up on the user's screen when the workflow is launched from a manual trigger. For other types of triggers, it will be displayed in the Workflow run record page.`}
<StyledMessageDescription data-testid="workflow-message-description">
{description}
</StyledMessageDescription>
</StyledMessageContentContainer>
</StyledFieldContainer>
@@ -1,10 +1,10 @@
import { WorkflowFormEmptyMessage } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage';
import { WorkflowMessage } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowMessage';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, within } from 'storybook/test';
const meta: Meta<typeof WorkflowFormEmptyMessage> = {
title: 'Modules/Workflow/Actions/Form/WorkflowFormEmptyMessage',
component: WorkflowFormEmptyMessage,
const meta: Meta<typeof WorkflowMessage> = {
title: 'Modules/Workflow/Actions/Form/WorkflowMessage',
component: WorkflowMessage,
parameters: {
layout: 'centered',
},
@@ -12,17 +12,17 @@ const meta: Meta<typeof WorkflowFormEmptyMessage> = {
};
export default meta;
type Story = StoryObj<typeof WorkflowFormEmptyMessage>;
type Story = StoryObj<typeof WorkflowMessage>;
export const Default: Story = {
args: {},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const messageTitleContainer = await canvas.findByTestId(
'empty-form-message-title',
'workflow-message-title',
);
const messageDescriptionContainer = await canvas.findByTestId(
'empty-form-message-description',
'workflow-message-description',
);
expect(messageTitleContainer).toBeVisible();
@@ -8,8 +8,8 @@ import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableLi
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { WorkflowMessage } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowMessage';
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
import { WorkflowFormEmptyMessage } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage';
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
import { css, useTheme } from '@emotion/react';
@@ -214,7 +214,10 @@ export const WorkflowEditActionFormBuilder = ({
<>
<StyledWorkflowStepBody>
{formData.length === 0 && (
<WorkflowFormEmptyMessage data-testid="empty-form-message" />
<WorkflowMessage
title={t`Add inputs to your form`}
description={t`Click on "Add Field" below to add the first input to your form. The form will pop up on the user's screen when the workflow is launched from a manual trigger. For other types of triggers, it will be displayed in the Workflow run record page.`}
/>
)}
<DraggableList
onDragEnd={handleDragEnd}
@@ -180,7 +180,7 @@ export const EmptyForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const messageContainer = await canvas.findByTestId(
'empty-form-message-title',
'workflow-message-title',
);
expect(messageContainer).toBeVisible();
@@ -0,0 +1,131 @@
import { getDefaultFunctionInputFromInputSchema } from '@/logic-functions/utils/getDefaultFunctionInputFromInputSchema';
import { mergeDefaultFunctionInputAndFunctionInput } from '@/logic-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
import { useGetOneLogicFunction } from '@/settings/logic-functions/hooks/useGetOneLogicFunction';
import { type WorkflowLogicFunctionAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { WorkflowEditActionCodeFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields';
import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/setNestedValue';
import { WorkflowMessage } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowMessage';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isObject } from '@sniptt/guards';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useDebouncedCallback } from 'use-debounce';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
type WorkflowEditActionLogicFunctionProps = {
action: WorkflowLogicFunctionAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowLogicFunctionAction) => void;
};
};
export const WorkflowEditActionLogicFunction = ({
action,
actionOptions,
}: WorkflowEditActionLogicFunctionProps) => {
const { t } = useLingui();
const logicFunctionId = action.settings.input.logicFunctionId;
const { logicFunction, loading } = useGetOneLogicFunction({
id: logicFunctionId,
});
const functionInput = useMemo(() => {
const toolInputSchema = logicFunction?.toolInputSchema;
if (!isDefined(toolInputSchema)) {
return action.settings.input.logicFunctionInput ?? {};
}
const schemaArray = Array.isArray(toolInputSchema)
? toolInputSchema
: [toolInputSchema];
const defaultInput = getDefaultFunctionInputFromInputSchema(schemaArray)[0];
if (!isObject(defaultInput)) {
return action.settings.input.logicFunctionInput ?? {};
}
return mergeDefaultFunctionInputAndFunctionInput({
newInput: defaultInput,
oldInput: action.settings.input.logicFunctionInput ?? {},
});
}, [
logicFunction?.toolInputSchema,
action.settings.input.logicFunctionInput,
]);
const updateAction = useDebouncedCallback(
(actionUpdate: Partial<WorkflowLogicFunctionAction>) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
...actionUpdate,
});
},
500,
);
const handleInputChange = (value: unknown, path: string[]) => {
const updatedFunctionInput = setNestedValue(functionInput, path, value);
updateAction({
settings: {
...action.settings,
input: {
...action.settings.input,
logicFunctionInput: updatedFunctionInput,
},
},
});
};
if (loading) {
return null;
}
const hasInputFields = Object.keys(functionInput).length > 0;
return (
<>
<WorkflowStepBody>
<StyledContainer>
{hasInputFields ? (
<WorkflowEditActionCodeFields
functionInput={functionInput}
readonly={actionOptions.readonly}
onInputChange={handleInputChange}
VariablePicker={WorkflowVariablePicker}
fullWidth
/>
) : (
<WorkflowMessage
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} />}
</>
);
};
@@ -27,5 +27,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
return msg`AI`;
}
if (actionType === 'LOGIC_FUNCTION') {
return msg`Application`;
}
return msg`Action`;
};
@@ -19,6 +19,8 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
case 'HTTP_REQUEST':
case 'SEND_EMAIL':
return CORE_ACTIONS.find((item) => item.type === actionType)?.icon;
case 'LOGIC_FUNCTION':
return 'IconFunction';
case 'FORM':
return HUMAN_INPUT_ACTIONS.find((item) => item.type === actionType)?.icon;
case 'ITERATOR':
@@ -11,6 +11,7 @@ export const getActionIconColorOrThrow = ({
}) => {
switch (actionType) {
case 'CODE':
case 'LOGIC_FUNCTION':
case 'HTTP_REQUEST':
case 'SEND_EMAIL':
return theme.color.red;