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
@@ -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 = () => {
@@ -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}
@@ -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 = (
@@ -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';
@@ -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 = ({
@@ -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>
);
};
@@ -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)}`,
);
};
@@ -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}
@@ -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>
);
};
@@ -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';
@@ -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';
@@ -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';
@@ -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', () => {
@@ -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', () => {
@@ -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();
@@ -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';
@@ -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';
@@ -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: {