Allow to insert into loop step ids (#14425)

- Create empty node on iterator creation
- Add options on step creation to insert in loop
- Handle properly when a step is removed from loop
- Remove loopNextStepIds from frontend action
- Add a frontend skeleton for empty action



https://github.com/user-attachments/assets/281a8c15-8062-4702-afb4-0d9a50902252
This commit is contained in:
Thomas Trompette
2025-09-12 09:30:28 +02:00
committed by GitHub
parent bdf60b1680
commit 231d1a98af
23 changed files with 538 additions and 92 deletions
@@ -8,6 +8,7 @@ import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-ac
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
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';
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
@@ -226,6 +227,17 @@ export const WorkflowRunStepNodeDetail = ({
/>
);
}
case 'EMPTY': {
return (
<WorkflowEditActionEmpty
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
}
}
}
@@ -7,6 +7,7 @@ import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-ac
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
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';
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
@@ -206,6 +207,15 @@ export const WorkflowStepDetail = ({
/>
);
}
case 'EMPTY': {
return (
<WorkflowEditActionEmpty
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
);
}
default:
return assertUnreachable(
stepDefinition.definition,
@@ -0,0 +1,54 @@
import { type WorkflowEmptyAction } from '@/workflow/types/Workflow';
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { useIcons } from 'twenty-ui/display';
type WorkflowEditActionEmptyProps = {
action: WorkflowEmptyAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowEmptyAction) => void;
};
};
export const WorkflowEditActionEmpty = ({
action,
actionOptions,
}: WorkflowEditActionEmptyProps) => {
const { getIcon } = useIcons();
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: 'Empty Node',
});
return (
<>
<WorkflowStepHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={actionOptions.readonly}
/>
<WorkflowStepBody>{'Empty Node'}</WorkflowStepBody>
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
</>
);
};
@@ -101,20 +101,6 @@ export const WorkflowEditActionIterator = ({
readonly={actionOptions.readonly}
VariablePicker={WorkflowVariablePicker}
/>
{/* TODO: remove once the UI is implemented */}
<FormTextFieldInput
label={t`Initial loop step IDs`}
placeholder={t`Enter array of step IDs`}
defaultValue={
Array.isArray(formData.initialLoopStepIds)
? JSON.stringify(formData.initialLoopStepIds)
: (formData.initialLoopStepIds as string) || ''
}
onChange={(value: string) =>
handleFieldChange('initialLoopStepIds', value)
}
readonly={actionOptions.readonly}
/>
</WorkflowStepBody>
</>
);
@@ -23,6 +23,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
case 'ITERATOR': {
return msg`Iterator`;
}
case 'EMPTY': {
return msg`Empty Node`;
}
default:
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
}
@@ -23,6 +23,7 @@ export const getActionIconColorOrThrow = ({
return theme.color.orange;
case 'ITERATOR':
case 'FILTER':
case 'EMPTY':
return theme.font.color.tertiary;
case 'AI_AGENT':
return theme.color.pink;