Create workflow version show page (#7466)
In this PR: - Refactored components to clarify their behavior. For example, I renamed the `Workflow` component to `WorkflowVisualizer`. This moved forward the issue #7010. - Create two variants of several workflow-related components: one version for editing and another for viewing. For instance, there is `WorkflowDiagramCanvasEditable.tsx` and `WorkflowDiagramCanvasReadonly.tsx` - Implement the show page for workflow versions. On this page, we display a readonly workflow visualizer. Users can click on nodes and it will expand the right drawer. - I added buttons in the header of the RecordShowPage for workflow versions: users can activate, deactivate or use the currently viewed version as the next draft. **There are many cache desynchronisation and I'll fix them really soon.** ## Demo (Turn sound on) https://github.com/user-attachments/assets/97fafa48-8902-4dab-8b39-f40848bf041e
This commit is contained in:
committed by
GitHub
parent
d5bd320b8d
commit
1863636003
@ -11,6 +11,7 @@ import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDraw
|
||||
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
|
||||
import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep';
|
||||
import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction';
|
||||
import { RightDrawerWorkflowViewStep } from '@/workflow/components/RightDrawerWorkflowViewStep';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
||||
import { RightDrawerPages } from '../types/RightDrawerPages';
|
||||
@ -41,6 +42,7 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
|
||||
<RightDrawerWorkflowSelectAction />
|
||||
),
|
||||
[RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />,
|
||||
[RightDrawerPages.WorkflowStepView]: <RightDrawerWorkflowViewStep />,
|
||||
};
|
||||
|
||||
export const RightDrawerRouter = () => {
|
||||
|
||||
@ -7,4 +7,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = {
|
||||
[RightDrawerPages.Copilot]: 'IconSparkles',
|
||||
[RightDrawerPages.WorkflowStepEdit]: 'IconSparkles',
|
||||
[RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles',
|
||||
[RightDrawerPages.WorkflowStepView]: 'IconSparkles',
|
||||
};
|
||||
|
||||
@ -7,4 +7,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = {
|
||||
[RightDrawerPages.Copilot]: 'Copilot',
|
||||
[RightDrawerPages.WorkflowStepEdit]: 'Workflow',
|
||||
[RightDrawerPages.WorkflowStepSelectAction]: 'Workflow',
|
||||
[RightDrawerPages.WorkflowStepView]: 'Workflow',
|
||||
};
|
||||
|
||||
@ -4,5 +4,6 @@ export enum RightDrawerPages {
|
||||
ViewRecord = 'view-record',
|
||||
Copilot = 'copilot',
|
||||
WorkflowStepSelectAction = 'workflow-step-select-action',
|
||||
WorkflowStepView = 'workflow-step-view',
|
||||
WorkflowStepEdit = 'workflow-step-edit',
|
||||
}
|
||||
|
||||
@ -15,7 +15,10 @@ import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/Show
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { Workflow } from '@/workflow/components/Workflow';
|
||||
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
|
||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
|
||||
import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer';
|
||||
import { WorkflowVisualizerEffect } from '@/workflow/components/WorkflowVisualizerEffect';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
@ -130,6 +133,10 @@ export const ShowPageRightContainer = ({
|
||||
isWorkflowEnabled &&
|
||||
targetableObject.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.Workflow;
|
||||
const isWorkflowVersion =
|
||||
isWorkflowEnabled &&
|
||||
targetableObject.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.WorkflowVersion;
|
||||
|
||||
const shouldDisplayCalendarTab = isCompanyOrPerson;
|
||||
const shouldDisplayEmailsTab = emails && isCompanyOrPerson;
|
||||
@ -162,7 +169,7 @@ export const ShowPageRightContainer = ({
|
||||
id: 'timeline',
|
||||
title: 'Timeline',
|
||||
Icon: IconTimelineEvent,
|
||||
hide: !timeline || isInRightDrawer || isWorkflow,
|
||||
hide: !timeline || isInRightDrawer || isWorkflow || isWorkflowVersion,
|
||||
},
|
||||
{
|
||||
id: 'tasks',
|
||||
@ -174,7 +181,8 @@ export const ShowPageRightContainer = ({
|
||||
CoreObjectNameSingular.Note ||
|
||||
targetableObject.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.Task ||
|
||||
isWorkflow,
|
||||
isWorkflow ||
|
||||
isWorkflowVersion,
|
||||
},
|
||||
{
|
||||
id: 'notes',
|
||||
@ -186,13 +194,14 @@ export const ShowPageRightContainer = ({
|
||||
CoreObjectNameSingular.Note ||
|
||||
targetableObject.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.Task ||
|
||||
isWorkflow,
|
||||
isWorkflow ||
|
||||
isWorkflowVersion,
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
title: 'Files',
|
||||
Icon: IconPaperclip,
|
||||
hide: !notes || isWorkflow,
|
||||
hide: !notes || isWorkflow || isWorkflowVersion,
|
||||
},
|
||||
{
|
||||
id: 'emails',
|
||||
@ -212,6 +221,12 @@ export const ShowPageRightContainer = ({
|
||||
Icon: IconSettings,
|
||||
hide: !isWorkflow,
|
||||
},
|
||||
{
|
||||
id: 'workflowVersion',
|
||||
title: 'Workflow Version',
|
||||
Icon: IconSettings,
|
||||
hide: !isWorkflowVersion,
|
||||
},
|
||||
];
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
@ -251,7 +266,25 @@ export const ShowPageRightContainer = ({
|
||||
case 'calendar':
|
||||
return <Calendar targetableObject={targetableObject} />;
|
||||
case 'workflow':
|
||||
return <Workflow targetableObject={targetableObject} />;
|
||||
return (
|
||||
<>
|
||||
<WorkflowVisualizerEffect workflowId={targetableObject.id} />
|
||||
|
||||
<WorkflowVisualizer targetableObject={targetableObject} />
|
||||
</>
|
||||
);
|
||||
case 'workflowVersion':
|
||||
return (
|
||||
<>
|
||||
<WorkflowVersionVisualizerEffect
|
||||
workflowVersionId={targetableObject.id}
|
||||
/>
|
||||
|
||||
<WorkflowVersionVisualizer
|
||||
workflowVersionId={targetableObject.id}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import { assertWorkflowWithCurrentVersionIsDefined } from '../utils/assertWorkfl
|
||||
export const RecordShowPageWorkflowHeader = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string | undefined;
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
|
||||
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
|
||||
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { IconPencil, IconPlayerStop, IconPower, isDefined } from 'twenty-ui';
|
||||
|
||||
export const RecordShowPageWorkflowVersionHeader = ({
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||
|
||||
const workflowVersionRelatedWorkflowQuery = useFindOneRecord<
|
||||
Pick<Workflow, '__typename' | 'id' | 'lastPublishedVersionId'>
|
||||
>({
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
objectRecordId: workflowVersion?.workflowId,
|
||||
recordGqlFields: {
|
||||
id: true,
|
||||
lastPublishedVersionId: true,
|
||||
},
|
||||
skip: !isDefined(workflowVersion),
|
||||
});
|
||||
|
||||
// TODO: In the future, use the workflow.status property to determine if there is a draft version
|
||||
const {
|
||||
records: draftWorkflowVersions,
|
||||
loading: loadingDraftWorkflowVersions,
|
||||
} = useFindManyRecords<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
filter: {
|
||||
workflowId: {
|
||||
eq: workflowVersion?.workflow.id,
|
||||
},
|
||||
status: {
|
||||
eq: 'DRAFT',
|
||||
},
|
||||
},
|
||||
skip: !isDefined(workflowVersion),
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const showUseAsDraftButton =
|
||||
!loadingDraftWorkflowVersions &&
|
||||
isDefined(workflowVersion) &&
|
||||
!workflowVersionRelatedWorkflowQuery.loading &&
|
||||
isDefined(workflowVersionRelatedWorkflowQuery.record) &&
|
||||
workflowVersion.status !== 'DRAFT' &&
|
||||
workflowVersion.id !==
|
||||
workflowVersionRelatedWorkflowQuery.record.lastPublishedVersionId;
|
||||
|
||||
const hasAlreadyDraftVersion =
|
||||
!loadingDraftWorkflowVersions && draftWorkflowVersions.length > 0;
|
||||
|
||||
const isWaitingForWorkflowVersion = !isDefined(workflowVersion);
|
||||
|
||||
const { activateWorkflowVersion } = useActivateWorkflowVersion();
|
||||
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
|
||||
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||
useUpdateOneRecord<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{showUseAsDraftButton ? (
|
||||
<Button
|
||||
title={`Use as Draft${hasAlreadyDraftVersion ? ' (override)' : ''}`}
|
||||
variant="secondary"
|
||||
Icon={IconPencil}
|
||||
disabled={isWaitingForWorkflowVersion}
|
||||
onClick={async () => {
|
||||
if (hasAlreadyDraftVersion) {
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: draftWorkflowVersions[0].id,
|
||||
updateOneRecordInput: {
|
||||
trigger: workflowVersion.trigger,
|
||||
steps: workflowVersion.steps,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflowVersion.workflow.id,
|
||||
name: `v${workflowVersion.workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: workflowVersion.trigger,
|
||||
steps: workflowVersion.steps,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{workflowVersion?.status === 'DRAFT' ||
|
||||
workflowVersion?.status === 'DEACTIVATED' ? (
|
||||
<Button
|
||||
title="Activate"
|
||||
variant="secondary"
|
||||
Icon={IconPower}
|
||||
disabled={isWaitingForWorkflowVersion}
|
||||
onClick={() => {
|
||||
return activateWorkflowVersion(workflowVersion.id);
|
||||
}}
|
||||
/>
|
||||
) : workflowVersion?.status === 'ACTIVE' ? (
|
||||
<Button
|
||||
title="Deactivate"
|
||||
variant="secondary"
|
||||
Icon={IconPlayerStop}
|
||||
disabled={isWaitingForWorkflowVersion}
|
||||
onClick={() => {
|
||||
return deactivateWorkflowVersion(workflowVersion.id);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,62 +1,11 @@
|
||||
import { WorkflowEditActionFormSendEmail } from '@/workflow/components/WorkflowEditActionFormSendEmail';
|
||||
import { WorkflowEditActionFormServerlessFunction } from '@/workflow/components/WorkflowEditActionFormServerlessFunction';
|
||||
import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
||||
import { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail';
|
||||
import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { findStepPosition } from '@/workflow/utils/findStepPosition';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const getStepDefinitionOrThrow = ({
|
||||
stepId,
|
||||
workflow,
|
||||
}: {
|
||||
stepId: string;
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const currentVersion = workflow.currentVersion;
|
||||
if (!isDefined(currentVersion)) {
|
||||
throw new Error('Expected to find a current version');
|
||||
}
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
if (!isDefined(currentVersion.trigger)) {
|
||||
return {
|
||||
type: 'trigger',
|
||||
definition: undefined,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'trigger',
|
||||
definition: currentVersion.trigger,
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (!isDefined(currentVersion.steps)) {
|
||||
throw new Error(
|
||||
'Malformed workflow version: missing steps information; be sure to create at least one step before trying to edit one',
|
||||
);
|
||||
}
|
||||
|
||||
const selectedNodePosition = findStepPosition({
|
||||
steps: currentVersion.steps,
|
||||
stepId: stepId,
|
||||
});
|
||||
if (!isDefined(selectedNodePosition)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'action',
|
||||
definition: selectedNodePosition.steps[selectedNodePosition.index],
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const RightDrawerWorkflowEditStepContent = ({
|
||||
workflow,
|
||||
}: {
|
||||
@ -75,47 +24,12 @@ export const RightDrawerWorkflowEditStepContent = ({
|
||||
stepId: workflowSelectedNode,
|
||||
});
|
||||
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId: workflowSelectedNode,
|
||||
workflow,
|
||||
});
|
||||
if (!isDefined(stepDefinition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (stepDefinition.type) {
|
||||
case 'trigger': {
|
||||
return (
|
||||
<WorkflowEditTriggerForm
|
||||
trigger={stepDefinition.definition}
|
||||
onTriggerUpdate={updateTrigger}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'action': {
|
||||
switch (stepDefinition.definition.type) {
|
||||
case 'CODE': {
|
||||
return (
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
action={stepDefinition.definition}
|
||||
onActionUpdate={updateStep}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return (
|
||||
<WorkflowEditActionFormSendEmail
|
||||
action={stepDefinition.definition}
|
||||
onActionUpdate={updateStep}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(
|
||||
stepDefinition,
|
||||
`Unsupported step: ${JSON.stringify(stepDefinition)}`,
|
||||
return (
|
||||
<WorkflowStepDetail
|
||||
stepId={workflowSelectedNode}
|
||||
workflowVersion={workflow.currentVersion}
|
||||
onActionUpdate={updateStep}
|
||||
onTriggerUpdate={updateTrigger}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { RightDrawerWorkflowViewStepContent } from '@/workflow/components/RightDrawerWorkflowViewStepContent';
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const RightDrawerWorkflowViewStep = () => {
|
||||
const workflowVersionId = useRecoilValue(workflowVersionIdState);
|
||||
if (!isDefined(workflowVersionId)) {
|
||||
throw new Error('Expected a workflow version id');
|
||||
}
|
||||
|
||||
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RightDrawerWorkflowViewStepContent workflowVersion={workflowVersion} />
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const RightDrawerWorkflowViewStepContent = ({
|
||||
workflowVersion,
|
||||
}: {
|
||||
workflowVersion: WorkflowVersion;
|
||||
}) => {
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
if (!isDefined(workflowSelectedNode)) {
|
||||
throw new Error(
|
||||
'Expected a node to be selected. Selecting a node is mandatory to edit it.',
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowStepDetail
|
||||
stepId={workflowSelectedNode}
|
||||
workflowVersion={workflowVersion}
|
||||
readonly
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,58 +0,0 @@
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { WorkflowDiagramCanvas } from '@/workflow/components/WorkflowDiagramCanvas';
|
||||
import { WorkflowEffect } from '@/workflow/components/WorkflowEffect';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import styled from '@emotion/styled';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledFlowContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
/* Below we reset the default styling of Reactflow */
|
||||
.react-flow__node-input,
|
||||
.react-flow__node-default,
|
||||
.react-flow__node-output,
|
||||
.react-flow__node-group {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
--xy-node-border-radius: none;
|
||||
--xy-node-border: none;
|
||||
--xy-node-background-color: none;
|
||||
--xy-node-boxshadow-hover: none;
|
||||
--xy-node-boxshadow-selected: none;
|
||||
`;
|
||||
|
||||
export const Workflow = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const workflowId = targetableObject.id;
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowEffect
|
||||
workflowId={workflowId}
|
||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
||||
/>
|
||||
|
||||
<StyledFlowContainer>
|
||||
{isDefined(workflowDiagram) && isDefined(workflowWithCurrentVersion) ? (
|
||||
<WorkflowDiagramCanvas
|
||||
diagram={workflowDiagram}
|
||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
||||
/>
|
||||
) : null}
|
||||
</StyledFlowContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,14 +1,11 @@
|
||||
import { WorkflowDiagramCanvasEffect } from '@/workflow/components/WorkflowDiagramCanvasEffect';
|
||||
import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode';
|
||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger';
|
||||
import { WorkflowDiagramStepNode } from '@/workflow/components/WorkflowDiagramStepNode';
|
||||
import { WorkflowVersionStatusTag } from '@/workflow/components/WorkflowVersionStatusTag';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||
import {
|
||||
WorkflowDiagram,
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramNodeType,
|
||||
} from '@/workflow/types/WorkflowDiagram';
|
||||
import { getOrganizedDiagram } from '@/workflow/utils/getOrganizedDiagram';
|
||||
import styled from '@emotion/styled';
|
||||
@ -19,13 +16,34 @@ import {
|
||||
EdgeChange,
|
||||
FitViewOptions,
|
||||
NodeChange,
|
||||
NodeProps,
|
||||
ReactFlow,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { GRAY_SCALE, isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledResetReactflowStyles = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
/* Below we reset the default styling of Reactflow */
|
||||
.react-flow__node-input,
|
||||
.react-flow__node-default,
|
||||
.react-flow__node-output,
|
||||
.react-flow__node-group {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
--xy-node-border-radius: none;
|
||||
--xy-node-border: none;
|
||||
--xy-node-background-color: none;
|
||||
--xy-node-boxshadow-hover: none;
|
||||
--xy-node-boxshadow-selected: none;
|
||||
`;
|
||||
|
||||
const StyledStatusTagContainer = styled.div`
|
||||
left: 0;
|
||||
top: 0;
|
||||
@ -38,12 +56,26 @@ const defaultFitViewOptions: FitViewOptions = {
|
||||
maxZoom: 1.3,
|
||||
};
|
||||
|
||||
export const WorkflowDiagramCanvas = ({
|
||||
export const WorkflowDiagramCanvasBase = ({
|
||||
diagram,
|
||||
workflowWithCurrentVersion,
|
||||
status,
|
||||
nodeTypes,
|
||||
children,
|
||||
}: {
|
||||
diagram: WorkflowDiagram;
|
||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion;
|
||||
status: WorkflowVersionStatus;
|
||||
nodeTypes: Partial<
|
||||
Record<
|
||||
WorkflowDiagramNodeType,
|
||||
React.ComponentType<
|
||||
NodeProps & {
|
||||
data: any;
|
||||
type: any;
|
||||
}
|
||||
>
|
||||
>
|
||||
>;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const { nodes, edges } = useMemo(
|
||||
() => getOrganizedDiagram(diagram),
|
||||
@ -87,33 +119,26 @@ export const WorkflowDiagramCanvas = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledResetReactflowStyles>
|
||||
<ReactFlow
|
||||
key={workflowWithCurrentVersion.currentVersion.id}
|
||||
onInit={({ fitView }) => {
|
||||
fitView(defaultFitViewOptions);
|
||||
}}
|
||||
nodeTypes={{
|
||||
default: WorkflowDiagramStepNode,
|
||||
'create-step': WorkflowDiagramCreateStepNode,
|
||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||
}}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
nodes={nodes.map((node) => ({ ...node, draggable: false }))}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
>
|
||||
<WorkflowDiagramCanvasEffect />
|
||||
|
||||
<Background color={GRAY_SCALE.gray25} size={2} />
|
||||
</ReactFlow>
|
||||
|
||||
<StyledStatusTagContainer>
|
||||
<WorkflowVersionStatusTag
|
||||
versionStatus={workflowWithCurrentVersion.currentVersion.status}
|
||||
/>
|
||||
</StyledStatusTagContainer>
|
||||
</>
|
||||
{children}
|
||||
|
||||
<StyledStatusTagContainer>
|
||||
<WorkflowVersionStatusTag versionStatus={status} />
|
||||
</StyledStatusTagContainer>
|
||||
</ReactFlow>
|
||||
</StyledResetReactflowStyles>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import { WorkflowDiagramCanvasBase } from '@/workflow/components/WorkflowDiagramCanvasBase';
|
||||
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/components/WorkflowDiagramCanvasEditableEffect';
|
||||
import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode';
|
||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger';
|
||||
import { WorkflowDiagramStepNodeEditable } from '@/workflow/components/WorkflowDiagramStepNodeEditable';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
|
||||
|
||||
export const WorkflowDiagramCanvasEditable = ({
|
||||
diagram,
|
||||
workflowWithCurrentVersion,
|
||||
}: {
|
||||
diagram: WorkflowDiagram;
|
||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
return (
|
||||
<WorkflowDiagramCanvasBase
|
||||
key={workflowWithCurrentVersion.currentVersion.id}
|
||||
diagram={diagram}
|
||||
status={workflowWithCurrentVersion.currentVersion.status}
|
||||
nodeTypes={{
|
||||
default: WorkflowDiagramStepNodeEditable,
|
||||
'create-step': WorkflowDiagramCreateStepNode,
|
||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||
}}
|
||||
>
|
||||
<WorkflowDiagramCanvasEditableEffect />
|
||||
</WorkflowDiagramCanvasBase>
|
||||
);
|
||||
};
|
||||
@ -1,33 +1,20 @@
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation';
|
||||
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
|
||||
import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import {
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramNode,
|
||||
} from '@/workflow/types/WorkflowDiagram';
|
||||
import {
|
||||
OnSelectionChangeParams,
|
||||
useOnSelectionChange,
|
||||
useReactFlow,
|
||||
} from '@xyflow/react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { WorkflowDiagramNode } from '@/workflow/types/WorkflowDiagram';
|
||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowDiagramCanvasEffect = () => {
|
||||
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();
|
||||
|
||||
export const WorkflowDiagramCanvasEditableEffect = () => {
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
|
||||
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||
|
||||
const workflowDiagramTriggerNodeSelection = useRecoilValue(
|
||||
workflowDiagramTriggerNodeSelectionState,
|
||||
);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
({ nodes }: OnSelectionChangeParams) => {
|
||||
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
||||
@ -65,15 +52,7 @@ export const WorkflowDiagramCanvasEffect = () => {
|
||||
onChange: handleSelectionChange,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(workflowDiagramTriggerNodeSelection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactflow.updateNode(workflowDiagramTriggerNodeSelection, {
|
||||
selected: true,
|
||||
});
|
||||
}, [reactflow, workflowDiagramTriggerNodeSelection]);
|
||||
useTriggerNodeSelection();
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { WorkflowDiagramCanvasBase } from '@/workflow/components/WorkflowDiagramCanvasBase';
|
||||
import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/components/WorkflowDiagramCanvasReadonlyEffect';
|
||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger';
|
||||
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/components/WorkflowDiagramStepNodeReadonly';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
|
||||
|
||||
export const WorkflowDiagramCanvasReadonly = ({
|
||||
diagram,
|
||||
workflowVersion,
|
||||
}: {
|
||||
diagram: WorkflowDiagram;
|
||||
workflowVersion: WorkflowVersion;
|
||||
}) => {
|
||||
return (
|
||||
<WorkflowDiagramCanvasBase
|
||||
key={workflowVersion.id}
|
||||
diagram={diagram}
|
||||
status={workflowVersion.status}
|
||||
nodeTypes={{
|
||||
default: WorkflowDiagramStepNodeReadonly,
|
||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||
}}
|
||||
>
|
||||
<WorkflowDiagramCanvasReadonlyEffect />
|
||||
</WorkflowDiagramCanvasBase>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowDiagramNode } from '@/workflow/types/WorkflowDiagram';
|
||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowDiagramCanvasReadonlyEffect = () => {
|
||||
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
({ nodes }: OnSelectionChangeParams) => {
|
||||
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
||||
const isClosingStep = isDefined(selectedNode) === false;
|
||||
|
||||
if (isClosingStep) {
|
||||
closeRightDrawer();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(selectedNode.id);
|
||||
openRightDrawer(RightDrawerPages.WorkflowStepView);
|
||||
},
|
||||
[closeRightDrawer, openRightDrawer, setWorkflowSelectedNode],
|
||||
);
|
||||
|
||||
useOnSelectionChange({
|
||||
onChange: handleSelectionChange,
|
||||
});
|
||||
|
||||
useTriggerNodeSelection();
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import {
|
||||
WorkflowVersion,
|
||||
WorkflowWithCurrentVersion,
|
||||
@ -13,22 +12,13 @@ import { useEffect } from 'react';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
type WorkflowEffectProps = {
|
||||
workflowId: string;
|
||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined;
|
||||
};
|
||||
|
||||
export const WorkflowEffect = ({
|
||||
workflowId,
|
||||
export const WorkflowDiagramEffect = ({
|
||||
workflowWithCurrentVersion,
|
||||
}: WorkflowEffectProps) => {
|
||||
const setWorkflowId = useSetRecoilState(workflowIdState);
|
||||
}: {
|
||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined;
|
||||
}) => {
|
||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkflowId(workflowId);
|
||||
}, [setWorkflowId, workflowId]);
|
||||
|
||||
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
|
||||
({ snapshot, set }) => {
|
||||
return (currentVersion: WorkflowVersion) => {
|
||||
@ -1,15 +1,9 @@
|
||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||
import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode';
|
||||
import { useDeleteOneStep } from '@/workflow/hooks/useDeleteOneStep';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconCode, IconMail, IconPlaylistAdd, IconTrash } from 'twenty-ui';
|
||||
import { IconCode, IconMail, IconPlaylistAdd } from 'twenty-ui';
|
||||
|
||||
const StyledStepNodeLabelIconContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -20,27 +14,15 @@ const StyledStepNodeLabelIconContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramStepNode = ({
|
||||
id,
|
||||
export const WorkflowDiagramStepNodeBase = ({
|
||||
data,
|
||||
selected,
|
||||
RightFloatingElement,
|
||||
}: {
|
||||
id: string;
|
||||
data: WorkflowDiagramStepNodeData;
|
||||
selected?: boolean;
|
||||
RightFloatingElement?: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
assertWorkflowWithCurrentVersionIsDefined(workflowWithCurrentVersion);
|
||||
|
||||
const { deleteOneStep } = useDeleteOneStep({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
stepId: id,
|
||||
});
|
||||
|
||||
const renderStepIcon = () => {
|
||||
switch (data.nodeType) {
|
||||
case 'trigger': {
|
||||
@ -87,16 +69,7 @@ export const WorkflowDiagramStepNode = ({
|
||||
nodeType={data.nodeType}
|
||||
label={data.label}
|
||||
Icon={renderStepIcon()}
|
||||
RightFloatingElement={
|
||||
selected ? (
|
||||
<FloatingIconButton
|
||||
Icon={IconTrash}
|
||||
onClick={() => {
|
||||
return deleteOneStep();
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
RightFloatingElement={RightFloatingElement}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
||||
import { WorkflowDiagramStepNodeBase } from '@/workflow/components/WorkflowDiagramStepNodeBase';
|
||||
import { useDeleteOneStep } from '@/workflow/hooks/useDeleteOneStep';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconTrash } from 'twenty-ui';
|
||||
|
||||
export const WorkflowDiagramStepNodeEditable = ({
|
||||
id,
|
||||
data,
|
||||
selected,
|
||||
}: {
|
||||
id: string;
|
||||
data: WorkflowDiagramStepNodeData;
|
||||
selected?: boolean;
|
||||
}) => {
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
assertWorkflowWithCurrentVersionIsDefined(workflowWithCurrentVersion);
|
||||
|
||||
const { deleteOneStep } = useDeleteOneStep({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
stepId: id,
|
||||
});
|
||||
|
||||
return (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
data={data}
|
||||
RightFloatingElement={
|
||||
selected ? (
|
||||
<FloatingIconButton
|
||||
Icon={IconTrash}
|
||||
onClick={() => {
|
||||
return deleteOneStep();
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { WorkflowDiagramStepNodeBase } from '@/workflow/components/WorkflowDiagramStepNodeBase';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||
|
||||
export const WorkflowDiagramStepNodeReadonly = ({
|
||||
data,
|
||||
}: {
|
||||
data: WorkflowDiagramStepNodeData;
|
||||
}) => {
|
||||
return <WorkflowDiagramStepNodeBase data={data} />;
|
||||
};
|
||||
@ -16,18 +16,25 @@ const StyledTriggerSettings = styled.div`
|
||||
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
type WorkflowEditActionFormSendEmailProps =
|
||||
| {
|
||||
action: WorkflowSendEmailStep;
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
action: WorkflowSendEmailStep;
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowSendEmailStep) => void;
|
||||
};
|
||||
|
||||
type SendEmailFormData = {
|
||||
subject: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
export const WorkflowEditActionFormSendEmail = ({
|
||||
action,
|
||||
onActionUpdate,
|
||||
}: {
|
||||
action: WorkflowSendEmailStep;
|
||||
onActionUpdate: (action: WorkflowSendEmailStep) => void;
|
||||
}) => {
|
||||
export const WorkflowEditActionFormSendEmail = (
|
||||
props: WorkflowEditActionFormSendEmailProps,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const form = useForm<SendEmailFormData>({
|
||||
@ -35,18 +42,23 @@ export const WorkflowEditActionFormSendEmail = ({
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
disabled: props.readonly,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('subject', action.settings.subject ?? '');
|
||||
form.setValue('body', action.settings.template ?? '');
|
||||
}, [action.settings.subject, action.settings.template, form]);
|
||||
form.setValue('subject', props.action.settings.subject ?? '');
|
||||
form.setValue('body', props.action.settings.template ?? '');
|
||||
}, [props.action.settings.subject, props.action.settings.template, form]);
|
||||
|
||||
const saveAction = useDebouncedCallback((formData: SendEmailFormData) => {
|
||||
onActionUpdate({
|
||||
...action,
|
||||
if (props.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onActionUpdate({
|
||||
...props.action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
...props.action.settings,
|
||||
title: formData.subject,
|
||||
subject: formData.subject,
|
||||
template: formData.body,
|
||||
@ -77,6 +89,7 @@ export const WorkflowEditActionFormSendEmail = ({
|
||||
label="Subject"
|
||||
placeholder="Thank you for building such an awesome CRM!"
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onChange={(email) => {
|
||||
field.onChange(email);
|
||||
|
||||
@ -95,6 +108,7 @@ export const WorkflowEditActionFormSendEmail = ({
|
||||
placeholder="Thank you so much!"
|
||||
value={field.value}
|
||||
minRows={4}
|
||||
disabled={field.disabled}
|
||||
onChange={(email) => {
|
||||
field.onChange(email);
|
||||
|
||||
|
||||
@ -13,13 +13,20 @@ const StyledTriggerSettings = styled.div`
|
||||
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const WorkflowEditActionFormServerlessFunction = ({
|
||||
action,
|
||||
onActionUpdate,
|
||||
}: {
|
||||
action: WorkflowCodeStep;
|
||||
onActionUpdate: (trigger: WorkflowCodeStep) => void;
|
||||
}) => {
|
||||
type WorkflowEditActionFormServerlessFunctionProps =
|
||||
| {
|
||||
action: WorkflowCodeStep;
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
action: WorkflowCodeStep;
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowCodeStep) => void;
|
||||
};
|
||||
|
||||
export const WorkflowEditActionFormServerlessFunction = (
|
||||
props: WorkflowEditActionFormServerlessFunctionProps,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||
@ -47,13 +54,18 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
||||
dropdownId="workflow-edit-action-function"
|
||||
label="Function"
|
||||
fullWidth
|
||||
value={action.settings.serverlessFunctionId}
|
||||
value={props.action.settings.serverlessFunctionId}
|
||||
options={availableFunctions}
|
||||
disabled={props.readonly}
|
||||
onChange={(updatedFunction) => {
|
||||
onActionUpdate({
|
||||
...action,
|
||||
if (props.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onActionUpdate({
|
||||
...props.action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
...props.action.settings,
|
||||
serverlessFunctionId: updatedFunction,
|
||||
},
|
||||
});
|
||||
|
||||
@ -45,13 +45,23 @@ const StyledTriggerSettings = styled.div`
|
||||
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
type WorkflowEditTriggerFormProps =
|
||||
| {
|
||||
trigger: WorkflowTrigger | undefined;
|
||||
readonly: true;
|
||||
onTriggerUpdate?: undefined;
|
||||
}
|
||||
| {
|
||||
trigger: WorkflowTrigger | undefined;
|
||||
readonly?: false;
|
||||
onTriggerUpdate: (trigger: WorkflowTrigger) => void;
|
||||
};
|
||||
|
||||
export const WorkflowEditTriggerForm = ({
|
||||
trigger,
|
||||
readonly,
|
||||
onTriggerUpdate,
|
||||
}: {
|
||||
trigger: WorkflowTrigger | undefined;
|
||||
onTriggerUpdate: (trigger: WorkflowTrigger) => void;
|
||||
}) => {
|
||||
}: WorkflowEditTriggerFormProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
@ -102,9 +112,14 @@ export const WorkflowEditTriggerForm = ({
|
||||
dropdownId="workflow-edit-trigger-record-type"
|
||||
label="Record Type"
|
||||
fullWidth
|
||||
disabled={readonly}
|
||||
value={triggerEvent?.objectType}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedRecordType) => {
|
||||
if (readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
onTriggerUpdate(
|
||||
isDefined(trigger) && isDefined(triggerEvent)
|
||||
? {
|
||||
@ -129,7 +144,12 @@ export const WorkflowEditTriggerForm = ({
|
||||
fullWidth
|
||||
value={triggerEvent?.event}
|
||||
options={OBJECT_EVENT_TRIGGERS}
|
||||
disabled={readonly}
|
||||
onChange={(updatedEvent) => {
|
||||
if (readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
onTriggerUpdate(
|
||||
isDefined(trigger) && isDefined(triggerEvent)
|
||||
? {
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
import { WorkflowEditActionFormSendEmail } from '@/workflow/components/WorkflowEditActionFormSendEmail';
|
||||
import { WorkflowEditActionFormServerlessFunction } from '@/workflow/components/WorkflowEditActionFormServerlessFunction';
|
||||
import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowTrigger,
|
||||
WorkflowVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
type WorkflowStepDetailProps =
|
||||
| {
|
||||
stepId: string;
|
||||
workflowVersion: WorkflowVersion;
|
||||
readonly: true;
|
||||
onTriggerUpdate?: undefined;
|
||||
onActionUpdate?: undefined;
|
||||
}
|
||||
| {
|
||||
stepId: string;
|
||||
workflowVersion: WorkflowVersion;
|
||||
readonly?: false;
|
||||
onTriggerUpdate: (trigger: WorkflowTrigger) => void;
|
||||
onActionUpdate: (action: WorkflowAction) => void;
|
||||
};
|
||||
|
||||
export const WorkflowStepDetail = ({
|
||||
stepId,
|
||||
workflowVersion,
|
||||
...props
|
||||
}: WorkflowStepDetailProps) => {
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId,
|
||||
workflowVersion,
|
||||
});
|
||||
if (!isDefined(stepDefinition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (stepDefinition.type) {
|
||||
case 'trigger': {
|
||||
return (
|
||||
<WorkflowEditTriggerForm
|
||||
trigger={stepDefinition.definition}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'action': {
|
||||
switch (stepDefinition.definition.type) {
|
||||
case 'CODE': {
|
||||
return (
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
action={stepDefinition.definition}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return (
|
||||
<WorkflowEditActionFormSendEmail
|
||||
action={stepDefinition.definition}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(
|
||||
stepDefinition,
|
||||
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { WorkflowDiagramCanvasReadonly } from '@/workflow/components/WorkflowDiagramCanvasReadonly';
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowVersionVisualizer = ({
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||
|
||||
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
||||
|
||||
return isDefined(workflowDiagram) && isDefined(workflowVersion) ? (
|
||||
<WorkflowDiagramCanvasReadonly
|
||||
diagram={workflowDiagram}
|
||||
workflowVersion={workflowVersion}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
|
||||
import { getWorkflowVersionDiagram } from '@/workflow/utils/getWorkflowVersionDiagram';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowVersionVisualizerEffect = ({
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||
|
||||
const setWorkflowVersionId = useSetRecoilState(workflowVersionIdState);
|
||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkflowVersionId(workflowVersionId);
|
||||
}, [setWorkflowVersionId, workflowVersionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(workflowVersion)) {
|
||||
setWorkflowDiagram(undefined);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nextWorkflowDiagram = getWorkflowVersionDiagram(workflowVersion);
|
||||
|
||||
setWorkflowDiagram(nextWorkflowDiagram);
|
||||
}, [setWorkflowDiagram, workflowVersion]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { WorkflowDiagramCanvasEditable } from '@/workflow/components/WorkflowDiagramCanvasEditable';
|
||||
import { WorkflowDiagramEffect } from '@/workflow/components/WorkflowDiagramEffect';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowVisualizer = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const workflowId = targetableObject.id;
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowDiagramEffect
|
||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
||||
/>
|
||||
|
||||
{isDefined(workflowDiagram) && isDefined(workflowWithCurrentVersion) ? (
|
||||
<WorkflowDiagramCanvasEditable
|
||||
diagram={workflowDiagram}
|
||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
export const WorkflowVisualizerEffect = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const setWorkflowId = useSetRecoilState(workflowIdState);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkflowId(workflowId);
|
||||
}, [setWorkflowId, workflowId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -2,11 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
|
||||
export const useCreateNewWorkflowVersion = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
export const useCreateNewWorkflowVersion = () => {
|
||||
const { createOneRecord: createOneWorkflowVersion } =
|
||||
useCreateOneRecord<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
@ -15,13 +11,10 @@ export const useCreateNewWorkflowVersion = ({
|
||||
const createNewWorkflowVersion = (
|
||||
workflowVersionData: Pick<
|
||||
WorkflowVersion,
|
||||
'name' | 'status' | 'trigger' | 'steps'
|
||||
'workflowId' | 'name' | 'status' | 'trigger' | 'steps'
|
||||
>,
|
||||
) => {
|
||||
return createOneWorkflowVersion({
|
||||
workflowId,
|
||||
...workflowVersionData,
|
||||
});
|
||||
return createOneWorkflowVersion(workflowVersionData);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -32,9 +32,7 @@ export const useCreateStep = ({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
|
||||
const insertNodeAndSave = async ({
|
||||
parentNodeId,
|
||||
@ -66,6 +64,7 @@ export const useCreateStep = ({
|
||||
}
|
||||
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: `v${workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: workflow.currentVersion.trigger,
|
||||
|
||||
@ -20,9 +20,7 @@ export const useDeleteOneStep = ({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
|
||||
const deleteOneStep = async () => {
|
||||
if (workflow.currentVersion.status !== 'DRAFT') {
|
||||
@ -30,6 +28,7 @@ export const useDeleteOneStep = ({
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: newVersionName,
|
||||
status: 'DRAFT',
|
||||
trigger: null,
|
||||
@ -37,6 +36,7 @@ export const useDeleteOneStep = ({
|
||||
});
|
||||
} else {
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: newVersionName,
|
||||
status: 'DRAFT',
|
||||
trigger: workflow.currentVersion.trigger,
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
|
||||
import {
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramNode,
|
||||
} from '@/workflow/types/WorkflowDiagram';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useTriggerNodeSelection = () => {
|
||||
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();
|
||||
|
||||
const workflowDiagramTriggerNodeSelection = useRecoilValue(
|
||||
workflowDiagramTriggerNodeSelectionState,
|
||||
);
|
||||
const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
|
||||
workflowDiagramTriggerNodeSelectionState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(workflowDiagramTriggerNodeSelection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactflow.updateNode(workflowDiagramTriggerNodeSelection, {
|
||||
selected: true,
|
||||
});
|
||||
|
||||
setWorkflowDiagramTriggerNodeSelection(undefined);
|
||||
}, [
|
||||
reactflow,
|
||||
setWorkflowDiagramTriggerNodeSelection,
|
||||
workflowDiagramTriggerNodeSelection,
|
||||
]);
|
||||
};
|
||||
@ -21,9 +21,7 @@ export const useUpdateWorkflowVersionStep = ({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
|
||||
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
|
||||
if (!isDefined(workflow.currentVersion)) {
|
||||
@ -48,6 +46,7 @@ export const useUpdateWorkflowVersionStep = ({
|
||||
}
|
||||
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: `v${workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: workflow.currentVersion.trigger,
|
||||
|
||||
@ -18,9 +18,7 @@ export const useUpdateWorkflowVersionTrigger = ({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
|
||||
const updateTrigger = async (updatedTrigger: WorkflowTrigger) => {
|
||||
if (!isDefined(workflow.currentVersion)) {
|
||||
@ -39,6 +37,7 @@ export const useUpdateWorkflowVersionTrigger = ({
|
||||
}
|
||||
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflow.id,
|
||||
name: `v${workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: updatedTrigger,
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
|
||||
export const useWorkflowVersion = (workflowVersionId: string) => {
|
||||
const { record: workflowVersion } = useFindOneRecord<
|
||||
WorkflowVersion & {
|
||||
workflow: Omit<Workflow, 'versions'> & {
|
||||
versions: Array<{ __typename: string }>;
|
||||
};
|
||||
}
|
||||
>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
objectRecordId: workflowVersionId,
|
||||
recordGqlFields: {
|
||||
id: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
workflowId: true,
|
||||
trigger: true,
|
||||
steps: true,
|
||||
status: true,
|
||||
workflow: {
|
||||
id: true,
|
||||
name: true,
|
||||
statuses: true,
|
||||
versions: {
|
||||
totalCount: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return workflowVersion;
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const workflowVersionIdState = createState<string | undefined>({
|
||||
key: 'workflowVersionIdState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -28,3 +28,8 @@ export type WorkflowDiagramCreateStepNodeData = {
|
||||
export type WorkflowDiagramNodeData =
|
||||
| WorkflowDiagramStepNodeData
|
||||
| WorkflowDiagramCreateStepNodeData;
|
||||
|
||||
export type WorkflowDiagramNodeType =
|
||||
| 'default'
|
||||
| 'empty-trigger'
|
||||
| 'create-step';
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { findStepPosition } from '@/workflow/utils/findStepPosition';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const getStepDefinitionOrThrow = ({
|
||||
stepId,
|
||||
workflowVersion,
|
||||
}: {
|
||||
stepId: string;
|
||||
workflowVersion: WorkflowVersion;
|
||||
}) => {
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
if (!isDefined(workflowVersion.trigger)) {
|
||||
return {
|
||||
type: 'trigger',
|
||||
definition: undefined,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'trigger',
|
||||
definition: workflowVersion.trigger,
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (!isDefined(workflowVersion.steps)) {
|
||||
throw new Error(
|
||||
'Malformed workflow version: missing steps information; be sure to create at least one step before trying to edit one',
|
||||
);
|
||||
}
|
||||
|
||||
const selectedNodePosition = findStepPosition({
|
||||
steps: workflowVersion.steps,
|
||||
stepId: stepId,
|
||||
});
|
||||
if (!isDefined(selectedNodePosition)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'action',
|
||||
definition: selectedNodePosition.steps[selectedNodePosition.index],
|
||||
} as const;
|
||||
};
|
||||
@ -10,6 +10,7 @@ import { PageBody } from '@/ui/layout/page/PageBody';
|
||||
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { RecordShowPageWorkflowHeader } from '@/workflow/components/RecordShowPageWorkflowHeader';
|
||||
import { RecordShowPageWorkflowVersionHeader } from '@/workflow/components/RecordShowPageWorkflowVersionHeader';
|
||||
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
|
||||
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
|
||||
|
||||
@ -47,8 +48,11 @@ export const RecordShowPage = () => {
|
||||
>
|
||||
<>
|
||||
{objectNameSingular === CoreObjectNameSingular.Workflow ? (
|
||||
<RecordShowPageWorkflowHeader
|
||||
workflowId={parameters.objectRecordId}
|
||||
<RecordShowPageWorkflowHeader workflowId={objectRecordId} />
|
||||
) : objectNameSingular ===
|
||||
CoreObjectNameSingular.WorkflowVersion ? (
|
||||
<RecordShowPageWorkflowVersionHeader
|
||||
workflowVersionId={objectRecordId}
|
||||
/>
|
||||
) : (
|
||||
<RecordShowPageBaseHeader
|
||||
|
||||
Reference in New Issue
Block a user