Add first frontend version for iterators (#14213)
Add a base for testing iterator step in frontend. Loop next step ids need to be added to an input since the loop handle does not exist on the step yet. Here is an example of run: <img width="1271" height="653" alt="Capture d’écran 2025-09-01 à 16 21 39" src="https://github.com/user-attachments/assets/06ecf981-be17-4d31-84f6-11c2cc98cb66" /> First create record in the loop name is `{{currentItem}} - first`, the second is `{{currentItem}} - second` <img width="276" height="326" alt="Capture d’écran 2025-09-01 à 16 22 25" src="https://github.com/user-attachments/assets/0ae03b65-aeda-41a6-8693-c17711ec2102" />
This commit is contained in:
@@ -980,6 +980,7 @@ export enum FeatureFlagKey {
|
||||
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
|
||||
IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
|
||||
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
|
||||
}
|
||||
|
||||
|
||||
@@ -944,6 +944,7 @@ export enum FeatureFlagKey {
|
||||
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
|
||||
IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
|
||||
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -16,6 +16,7 @@ import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/s
|
||||
import { AI_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/AiActions';
|
||||
import { CORE_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/CoreActions';
|
||||
import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions';
|
||||
import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions';
|
||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
@@ -91,6 +92,9 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
||||
const theme = useTheme();
|
||||
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const isIteratorEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
|
||||
);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -116,6 +120,14 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
||||
{t`Human Input`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{WorkflowActionMenuItems(HUMAN_INPUT_ACTIONS, theme, handleCreateStep)}
|
||||
{isIteratorEnabled && (
|
||||
<>
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
{t`Others`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{WorkflowActionMenuItems(OTHER_ACTIONS, theme, handleCreateStep)}
|
||||
</>
|
||||
)}
|
||||
</RightDrawerStepListContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type workflowFindRecordsActionSchema,
|
||||
type workflowFormActionSchema,
|
||||
type workflowHttpRequestActionSchema,
|
||||
type workflowIteratorActionSchema,
|
||||
type workflowManualTriggerSchema,
|
||||
type workflowRunSchema,
|
||||
type workflowRunStateSchema,
|
||||
@@ -42,7 +43,9 @@ export type WorkflowFormAction = z.infer<typeof workflowFormActionSchema>;
|
||||
export type WorkflowHttpRequestAction = z.infer<
|
||||
typeof workflowHttpRequestActionSchema
|
||||
>;
|
||||
|
||||
export type WorkflowIteratorAction = z.infer<
|
||||
typeof workflowIteratorActionSchema
|
||||
>;
|
||||
export type WorkflowAiAgentAction = z.infer<typeof workflowAiAgentActionSchema>;
|
||||
|
||||
export type WorkflowAction =
|
||||
@@ -55,7 +58,8 @@ export type WorkflowAction =
|
||||
| WorkflowFilterAction
|
||||
| WorkflowFormAction
|
||||
| WorkflowHttpRequestAction
|
||||
| WorkflowAiAgentAction;
|
||||
| WorkflowAiAgentAction
|
||||
| WorkflowIteratorAction;
|
||||
|
||||
export type WorkflowActionType = WorkflowAction['type'];
|
||||
export type WorkflowStep = WorkflowAction;
|
||||
|
||||
+12
@@ -14,6 +14,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act
|
||||
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
|
||||
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 { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator';
|
||||
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
|
||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
|
||||
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
|
||||
@@ -214,6 +215,17 @@ export const WorkflowRunStepNodeDetail = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'ITERATOR': {
|
||||
return (
|
||||
<WorkflowEditActionIterator
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={{
|
||||
readonly: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -13,6 +13,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act
|
||||
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
|
||||
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 { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator';
|
||||
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
|
||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
|
||||
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
|
||||
@@ -196,7 +197,15 @@ export const WorkflowStepDetail = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'ITERATOR': {
|
||||
return (
|
||||
<WorkflowEditActionIterator
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return assertUnreachable(
|
||||
stepDefinition.definition,
|
||||
|
||||
+4
-27
@@ -2,35 +2,12 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
|
||||
export const OTHER_ACTIONS: Array<{
|
||||
label: string;
|
||||
type: Exclude<
|
||||
WorkflowActionType,
|
||||
'CREATE_RECORD' | 'UPDATE_RECORD' | 'DELETE_RECORD' | 'FIND_RECORDS'
|
||||
>;
|
||||
type: Extract<WorkflowActionType, 'ITERATOR'>;
|
||||
icon: string;
|
||||
}> = [
|
||||
{
|
||||
label: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
icon: 'IconSend',
|
||||
},
|
||||
{
|
||||
label: 'Code',
|
||||
type: 'CODE',
|
||||
icon: 'IconCode',
|
||||
},
|
||||
{
|
||||
label: 'Form',
|
||||
type: 'FORM',
|
||||
icon: 'IconForms',
|
||||
},
|
||||
{
|
||||
label: 'HTTP Request',
|
||||
type: 'HTTP_REQUEST',
|
||||
icon: 'IconWorld',
|
||||
},
|
||||
{
|
||||
label: 'AI Agent',
|
||||
type: 'AI_AGENT',
|
||||
icon: 'IconBrain',
|
||||
label: 'Iterator',
|
||||
type: 'ITERATOR',
|
||||
icon: 'IconRepeat',
|
||||
},
|
||||
];
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { type WorkflowIteratorAction } from '@/workflow/types/Workflow';
|
||||
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 { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
type WorkflowEditActionIteratorProps = {
|
||||
action: WorkflowIteratorAction;
|
||||
actionOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowIteratorAction) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const WorkflowEditActionIterator = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionIteratorProps) => {
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: 'Iterator',
|
||||
});
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
items: action.settings.input.items || [],
|
||||
initialLoopStepIds: action.settings.input.initialLoopStepIds || [],
|
||||
});
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
(updatedFormData: typeof formData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate?.({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
items: updatedFormData.items,
|
||||
initialLoopStepIds: updatedFormData.initialLoopStepIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
1000,
|
||||
);
|
||||
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedFormData = { ...formData, [field]: value };
|
||||
setFormData(updatedFormData);
|
||||
saveAction(updatedFormData);
|
||||
};
|
||||
|
||||
const handleTitleChange = (newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate?.({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={handleTitleChange}
|
||||
Icon={getIcon(headerIcon || 'IconRepeat')}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<FormTextFieldInput
|
||||
label={t`Items to iterate over`}
|
||||
placeholder={t`Enter array of items or variable expression`}
|
||||
defaultValue={
|
||||
Array.isArray(formData.items)
|
||||
? JSON.stringify(formData.items)
|
||||
: (formData.items as string) || ''
|
||||
}
|
||||
onChange={(value: string) => handleFieldChange('items', value)}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -20,7 +20,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
|
||||
case 'FILTER': {
|
||||
return msg`Filter`;
|
||||
}
|
||||
|
||||
case 'ITERATOR': {
|
||||
return msg`Iterator`;
|
||||
}
|
||||
default:
|
||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||
}
|
||||
|
||||
+2
@@ -21,6 +21,8 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
|
||||
return CORE_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
case 'FORM':
|
||||
return HUMAN_INPUT_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
case 'ITERATOR':
|
||||
return 'IconRepeat';
|
||||
default:
|
||||
return 'IconDefault';
|
||||
}
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ export const getActionIconColorOrThrow = ({
|
||||
return theme.font.color.tertiary;
|
||||
case 'FORM':
|
||||
return theme.color.orange;
|
||||
case 'ITERATOR':
|
||||
case 'FILTER':
|
||||
return theme.font.color.tertiary;
|
||||
case 'AI_AGENT':
|
||||
|
||||
+1
@@ -15,4 +15,5 @@ export enum FeatureFlagKey {
|
||||
IS_API_KEY_ROLES_ENABLED = 'IS_API_KEY_ROLES_ENABLED',
|
||||
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
|
||||
IS_MESSAGE_FOLDER_CONTROL_ENABLED = 'IS_MESSAGE_FOLDER_CONTROL_ENABLED',
|
||||
IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
|
||||
}
|
||||
|
||||
+2
@@ -138,6 +138,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_API_KEY_ROLES_ENABLED: false,
|
||||
IS_PAGE_LAYOUT_ENABLED: false,
|
||||
IS_MESSAGE_FOLDER_CONTROL_ENABLED: false,
|
||||
IS_WORKFLOW_ITERATOR_ENABLED: false,
|
||||
},
|
||||
eventEmitterService: {
|
||||
emitMutationEvent: jest.fn(),
|
||||
@@ -163,6 +164,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_WORKSPACE_MIGRATION_V2_ENABLED: false,
|
||||
IS_API_KEY_ROLES_ENABLED: false,
|
||||
IS_PAGE_LAYOUT_ENABLED: false,
|
||||
IS_WORKFLOW_ITERATOR_ENABLED: false,
|
||||
},
|
||||
permissionsPerRoleId: {},
|
||||
} as WorkspaceDataSource;
|
||||
|
||||
+5
@@ -80,6 +80,11 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export {
|
||||
workflowHttpRequestActionSettingsSchema,
|
||||
workflowAiAgentActionSettingsSchema,
|
||||
workflowFilterActionSettingsSchema,
|
||||
workflowIteratorActionSettingsSchema,
|
||||
workflowCodeActionSchema,
|
||||
workflowSendEmailActionSchema,
|
||||
workflowCreateRecordActionSchema,
|
||||
@@ -33,6 +34,7 @@ export {
|
||||
workflowHttpRequestActionSchema,
|
||||
workflowAiAgentActionSchema,
|
||||
workflowFilterActionSchema,
|
||||
workflowIteratorActionSchema,
|
||||
workflowActionSchema,
|
||||
workflowDatabaseEventTriggerSchema,
|
||||
workflowManualTriggerSchema,
|
||||
|
||||
@@ -219,6 +219,15 @@ export const workflowFilterActionSettingsSchema =
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowIteratorActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
items: z.union([z.array(z.any()), z.string()]).optional(),
|
||||
// TODO: should never be a string once fix the UI
|
||||
initialLoopStepIds: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Action schemas
|
||||
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('CODE'),
|
||||
@@ -270,6 +279,11 @@ export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({
|
||||
settings: workflowFilterActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('ITERATOR'),
|
||||
settings: workflowIteratorActionSettingsSchema,
|
||||
});
|
||||
|
||||
// Combined action schema
|
||||
export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowCodeActionSchema,
|
||||
@@ -282,6 +296,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowHttpRequestActionSchema,
|
||||
workflowAiAgentActionSchema,
|
||||
workflowFilterActionSchema,
|
||||
workflowIteratorActionSchema,
|
||||
]);
|
||||
|
||||
// Trigger schemas
|
||||
|
||||
Reference in New Issue
Block a user