Separate workflow step details and run step details (#11069)

Workflows step details in workflows and versions should be different
from the node tab in run. For most cases, it was using the same
component. But for forms, it will be a different one.

This PR:
- renames form action into formBuilder. formFiller is coming
- put code into a separated folder
- creates a new component for node details
This commit is contained in:
Thomas Trompette
2025-03-20 17:22:55 +01:00
committed by GitHub
parent 24bae89ebc
commit 3876cb8250
23 changed files with 260 additions and 85 deletions

View File

@ -21,8 +21,8 @@ import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdCom
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/types/WorkflowServerlessFunctionTabId';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
import { useRecoilCallback } from 'recoil';
export const useCommandMenuCloseAnimationCompleteCleanup = () => {

View File

@ -7,8 +7,8 @@ import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThro
import { WorkflowStepContextProvider } from '@/workflow/states/context/WorkflowStepContext';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
import { WorkflowRunStepNodeDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepNodeDetail';
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import {
WorkflowRunTabId,
@ -82,8 +82,7 @@ export const CommandMenuWorkflowRunViewStep = () => {
/>
{activeTabId === WorkflowRunTabId.NODE ? (
<WorkflowStepDetail
readonly
<WorkflowRunStepNodeDetail
stepId={workflowSelectedNode}
trigger={flow.trigger}
steps={flow.steps}

View File

@ -1,5 +1,5 @@
import { InputSchema } from '@/workflow/types/InputSchema';
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/types/FunctionInput';
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
import { isDefined } from 'twenty-shared';
export const getDefaultFunctionInputFromInputSchema = (

View File

@ -1,6 +1,6 @@
import { getDefaultFunctionInputFromInputSchema } from '@/serverless-functions/utils/getDefaultFunctionInputFromInputSchema';
import { getFunctionInputSchema } from '@/serverless-functions/utils/getFunctionInputSchema';
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/types/FunctionInput';
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
import { isObject } from '@sniptt/guards';
import { isDefined } from 'twenty-shared';

View File

@ -1,4 +1,4 @@
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/types/FunctionInput';
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
import { isObject } from '@sniptt/guards';
export const mergeDefaultFunctionInputAndFunctionInput = ({

View File

@ -1,22 +0,0 @@
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { WorkflowStepContextProvider } from '@/workflow/states/context/WorkflowStepContext';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
export const RightDrawerWorkflowViewStep = () => {
const flow = useFlowOrThrow();
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
return (
<WorkflowStepContextProvider
value={{ workflowVersionId: flow.workflowVersionId }}
>
<WorkflowStepDetail
stepId={workflowSelectedNode}
trigger={flow.trigger}
steps={flow.steps}
readonly
/>
</WorkflowStepContextProvider>
);
};

View File

@ -0,0 +1,175 @@
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
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 { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
import { WorkflowEditTriggerWebhookForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm';
import { isDefined } from 'twenty-shared';
type WorkflowRunStepNodeDetailProps = {
stepId: string;
trigger: WorkflowTrigger | null;
steps: Array<WorkflowAction> | null;
};
export const WorkflowRunStepNodeDetail = ({
stepId,
trigger,
steps,
}: WorkflowRunStepNodeDetailProps) => {
const stepDefinition = getStepDefinitionOrThrow({
stepId,
trigger,
steps,
});
if (!isDefined(stepDefinition) || !isDefined(stepDefinition.definition)) {
return null;
}
switch (stepDefinition.type) {
case 'trigger': {
switch (stepDefinition.definition.type) {
case 'DATABASE_EVENT': {
return (
<WorkflowEditTriggerDatabaseEventForm
key={stepId}
trigger={stepDefinition.definition}
triggerOptions={{
readonly: true,
}}
/>
);
}
case 'MANUAL': {
return (
<WorkflowEditTriggerManualForm
key={stepId}
trigger={stepDefinition.definition}
triggerOptions={{
readonly: true,
}}
/>
);
}
case 'WEBHOOK': {
return (
<WorkflowEditTriggerWebhookForm
key={stepId}
trigger={stepDefinition.definition}
triggerOptions={{
readonly: true,
}}
/>
);
}
case 'CRON': {
return (
<WorkflowEditTriggerCronForm
key={stepId}
trigger={stepDefinition.definition}
triggerOptions={{
readonly: true,
}}
/>
);
}
}
return assertUnreachable(
stepDefinition.definition,
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
);
}
case 'action': {
switch (stepDefinition.definition.type) {
case 'CODE': {
return (
<WorkflowActionServerlessFunction
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'SEND_EMAIL': {
return (
<WorkflowEditActionSendEmail
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'CREATE_RECORD': {
return (
<WorkflowEditActionCreateRecord
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'UPDATE_RECORD': {
return (
<WorkflowEditActionUpdateRecord
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'DELETE_RECORD': {
return (
<WorkflowEditActionDeleteRecord
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'FIND_RECORDS': {
return (
<WorkflowEditActionFindRecords
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'FORM': {
// TODO: Implement form filler
return null;
}
}
}
}
return assertUnreachable(
stepDefinition,
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
);
};

View File

@ -1,35 +1,18 @@
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
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 { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
import { WorkflowEditActionForm } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionForm';
import { WorkflowEditActionFormBuilder } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
import { WorkflowEditTriggerWebhookForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm';
const WorkflowEditActionServerlessFunction = lazy(() =>
import(
'@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionServerlessFunction'
).then((module) => ({
default: module.WorkflowEditActionServerlessFunction,
})),
);
const WorkflowReadonlyActionServerlessFunction = lazy(() =>
import(
'@/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionServerlessFunction'
).then((module) => ({
default: module.WorkflowReadonlyActionServerlessFunction,
})),
);
import { isDefined } from 'twenty-shared';
type WorkflowStepDetailProps = {
stepId: string;
@ -115,20 +98,11 @@ export const WorkflowStepDetail = ({
switch (stepDefinition.definition.type) {
case 'CODE': {
return (
<Suspense fallback={<RightDrawerSkeletonLoader />}>
{props.readonly ? (
<WorkflowReadonlyActionServerlessFunction
key={stepId}
action={stepDefinition.definition}
/>
) : (
<WorkflowEditActionServerlessFunction
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
)}
</Suspense>
<WorkflowActionServerlessFunction
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
);
}
case 'SEND_EMAIL': {
@ -182,7 +156,7 @@ export const WorkflowStepDetail = ({
case 'FORM': {
return (
<WorkflowEditActionForm
<WorkflowEditActionFormBuilder
key={stepId}
action={stepDefinition.definition}
actionOptions={props}

View File

@ -0,0 +1,49 @@
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
import { lazy, Suspense } from 'react';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
type WorkflowActionServerlessFunctionProps = {
action: WorkflowCodeAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowCodeAction) => void;
};
};
const WorkflowEditActionServerlessFunction = lazy(() =>
import(
'@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction'
).then((module) => ({
default: module.WorkflowEditActionServerlessFunction,
})),
);
const WorkflowReadonlyActionServerlessFunction = lazy(() =>
import(
'@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowReadonlyActionServerlessFunction'
).then((module) => ({
default: module.WorkflowReadonlyActionServerlessFunction,
})),
);
export const WorkflowActionServerlessFunction = ({
action,
actionOptions,
}: WorkflowActionServerlessFunctionProps) => {
return (
<Suspense fallback={<RightDrawerSkeletonLoader />}>
{actionOptions.readonly ? (
<WorkflowReadonlyActionServerlessFunction action={action} />
) : (
<WorkflowEditActionServerlessFunction
action={action}
actionOptions={actionOptions}
/>
)}
</Suspense>
);
};

View File

@ -6,7 +6,7 @@ import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithC
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/utils/setNestedValue';
import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/setNestedValue';
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
import { ServerlessFunctionExecutionResult } from '@/serverless-functions/components/ServerlessFunctionExecutionResult';
@ -23,11 +23,11 @@ import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdCom
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionServerlessFunctionFields';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/types/WorkflowServerlessFunctionTabId';
import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';

View File

@ -2,7 +2,7 @@ import { FormNestedFieldInputContainer } from '@/object-record/record-field/form
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/types/FunctionInput';
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
import styled from '@emotion/styled';
import { isObject } from '@sniptt/guards';

View File

@ -5,9 +5,9 @@ import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/Workflo
import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionServerlessFunctionFields';
import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Monaco } from '@monaco-editor/react';

View File

@ -1,4 +1,4 @@
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers';
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
describe('getWrongExportedFunctionMarkers', () => {
it('should return marker when no exported function', () => {

View File

@ -1,4 +1,4 @@
import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/utils/setNestedValue';
import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/setNestedValue';
describe('setNestedValue', () => {
it('should set nested value properly', () => {

View File

@ -24,7 +24,7 @@ import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
export type WorkflowEditActionFormProps = {
export type WorkflowEditActionFormBuilderProps = {
action: WorkflowFormAction;
actionOptions:
| {
@ -99,10 +99,10 @@ const StyledAddFieldContainer = styled.div`
gap: ${({ theme }) => theme.spacing(0.5)};
`;
export const WorkflowEditActionForm = ({
export const WorkflowEditActionFormBuilder = ({
action,
actionOptions,
}: WorkflowEditActionFormProps) => {
}: WorkflowEditActionFormBuilderProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { t } = useLingui();

View File

@ -1,7 +1,7 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionForm';
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
import { WorkflowFormFieldSettingsByType } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsByType';
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
import { useTheme } from '@emotion/react';

View File

@ -1,4 +1,4 @@
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionForm';
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
import { assertUnreachable, FieldMetadataType } from 'twenty-shared';
import { WorkflowFormFieldSettingsNumber } from './WorkflowFormFieldSettingsNumber';
import { WorkflowFormFieldSettingsText } from './WorkflowFormFieldSettingsText';

View File

@ -1,5 +1,5 @@
import { WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowEditActionForm } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionForm';
import { WorkflowEditActionFormBuilder } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, within } from '@storybook/test';
import { FieldMetadataType } from 'twenty-shared';
@ -43,9 +43,9 @@ const DEFAULT_ACTION = {
},
} satisfies WorkflowFormAction;
const meta: Meta<typeof WorkflowEditActionForm> = {
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionForm',
component: WorkflowEditActionForm,
const meta: Meta<typeof WorkflowEditActionFormBuilder> = {
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormBuilder',
component: WorkflowEditActionFormBuilder,
parameters: {
msw: graphqlMocks,
},
@ -62,7 +62,7 @@ const meta: Meta<typeof WorkflowEditActionForm> = {
export default meta;
type Story = StoryObj<typeof WorkflowEditActionForm>;
type Story = StoryObj<typeof WorkflowEditActionFormBuilder>;
export const Default: Story = {
args: {