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 { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
|
||||||
import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep';
|
import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep';
|
||||||
import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction';
|
import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction';
|
||||||
|
import { RightDrawerWorkflowViewStep } from '@/workflow/components/RightDrawerWorkflowViewStep';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
||||||
import { RightDrawerPages } from '../types/RightDrawerPages';
|
import { RightDrawerPages } from '../types/RightDrawerPages';
|
||||||
@ -41,6 +42,7 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
|
|||||||
<RightDrawerWorkflowSelectAction />
|
<RightDrawerWorkflowSelectAction />
|
||||||
),
|
),
|
||||||
[RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />,
|
[RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />,
|
||||||
|
[RightDrawerPages.WorkflowStepView]: <RightDrawerWorkflowViewStep />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RightDrawerRouter = () => {
|
export const RightDrawerRouter = () => {
|
||||||
|
|||||||
@ -7,4 +7,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = {
|
|||||||
[RightDrawerPages.Copilot]: 'IconSparkles',
|
[RightDrawerPages.Copilot]: 'IconSparkles',
|
||||||
[RightDrawerPages.WorkflowStepEdit]: 'IconSparkles',
|
[RightDrawerPages.WorkflowStepEdit]: 'IconSparkles',
|
||||||
[RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles',
|
[RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles',
|
||||||
|
[RightDrawerPages.WorkflowStepView]: 'IconSparkles',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,4 +7,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = {
|
|||||||
[RightDrawerPages.Copilot]: 'Copilot',
|
[RightDrawerPages.Copilot]: 'Copilot',
|
||||||
[RightDrawerPages.WorkflowStepEdit]: 'Workflow',
|
[RightDrawerPages.WorkflowStepEdit]: 'Workflow',
|
||||||
[RightDrawerPages.WorkflowStepSelectAction]: 'Workflow',
|
[RightDrawerPages.WorkflowStepSelectAction]: 'Workflow',
|
||||||
|
[RightDrawerPages.WorkflowStepView]: 'Workflow',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,5 +4,6 @@ export enum RightDrawerPages {
|
|||||||
ViewRecord = 'view-record',
|
ViewRecord = 'view-record',
|
||||||
Copilot = 'copilot',
|
Copilot = 'copilot',
|
||||||
WorkflowStepSelectAction = 'workflow-step-select-action',
|
WorkflowStepSelectAction = 'workflow-step-select-action',
|
||||||
|
WorkflowStepView = 'workflow-step-view',
|
||||||
WorkflowStepEdit = 'workflow-step-edit',
|
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 { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -130,6 +133,10 @@ export const ShowPageRightContainer = ({
|
|||||||
isWorkflowEnabled &&
|
isWorkflowEnabled &&
|
||||||
targetableObject.targetObjectNameSingular ===
|
targetableObject.targetObjectNameSingular ===
|
||||||
CoreObjectNameSingular.Workflow;
|
CoreObjectNameSingular.Workflow;
|
||||||
|
const isWorkflowVersion =
|
||||||
|
isWorkflowEnabled &&
|
||||||
|
targetableObject.targetObjectNameSingular ===
|
||||||
|
CoreObjectNameSingular.WorkflowVersion;
|
||||||
|
|
||||||
const shouldDisplayCalendarTab = isCompanyOrPerson;
|
const shouldDisplayCalendarTab = isCompanyOrPerson;
|
||||||
const shouldDisplayEmailsTab = emails && isCompanyOrPerson;
|
const shouldDisplayEmailsTab = emails && isCompanyOrPerson;
|
||||||
@ -162,7 +169,7 @@ export const ShowPageRightContainer = ({
|
|||||||
id: 'timeline',
|
id: 'timeline',
|
||||||
title: 'Timeline',
|
title: 'Timeline',
|
||||||
Icon: IconTimelineEvent,
|
Icon: IconTimelineEvent,
|
||||||
hide: !timeline || isInRightDrawer || isWorkflow,
|
hide: !timeline || isInRightDrawer || isWorkflow || isWorkflowVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tasks',
|
id: 'tasks',
|
||||||
@ -174,7 +181,8 @@ export const ShowPageRightContainer = ({
|
|||||||
CoreObjectNameSingular.Note ||
|
CoreObjectNameSingular.Note ||
|
||||||
targetableObject.targetObjectNameSingular ===
|
targetableObject.targetObjectNameSingular ===
|
||||||
CoreObjectNameSingular.Task ||
|
CoreObjectNameSingular.Task ||
|
||||||
isWorkflow,
|
isWorkflow ||
|
||||||
|
isWorkflowVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notes',
|
id: 'notes',
|
||||||
@ -186,13 +194,14 @@ export const ShowPageRightContainer = ({
|
|||||||
CoreObjectNameSingular.Note ||
|
CoreObjectNameSingular.Note ||
|
||||||
targetableObject.targetObjectNameSingular ===
|
targetableObject.targetObjectNameSingular ===
|
||||||
CoreObjectNameSingular.Task ||
|
CoreObjectNameSingular.Task ||
|
||||||
isWorkflow,
|
isWorkflow ||
|
||||||
|
isWorkflowVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'files',
|
id: 'files',
|
||||||
title: 'Files',
|
title: 'Files',
|
||||||
Icon: IconPaperclip,
|
Icon: IconPaperclip,
|
||||||
hide: !notes || isWorkflow,
|
hide: !notes || isWorkflow || isWorkflowVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'emails',
|
id: 'emails',
|
||||||
@ -212,6 +221,12 @@ export const ShowPageRightContainer = ({
|
|||||||
Icon: IconSettings,
|
Icon: IconSettings,
|
||||||
hide: !isWorkflow,
|
hide: !isWorkflow,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'workflowVersion',
|
||||||
|
title: 'Workflow Version',
|
||||||
|
Icon: IconSettings,
|
||||||
|
hide: !isWorkflowVersion,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const renderActiveTabContent = () => {
|
const renderActiveTabContent = () => {
|
||||||
switch (activeTabId) {
|
switch (activeTabId) {
|
||||||
@ -251,7 +266,25 @@ export const ShowPageRightContainer = ({
|
|||||||
case 'calendar':
|
case 'calendar':
|
||||||
return <Calendar targetableObject={targetableObject} />;
|
return <Calendar targetableObject={targetableObject} />;
|
||||||
case 'workflow':
|
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:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { assertWorkflowWithCurrentVersionIsDefined } from '../utils/assertWorkfl
|
|||||||
export const RecordShowPageWorkflowHeader = ({
|
export const RecordShowPageWorkflowHeader = ({
|
||||||
workflowId,
|
workflowId,
|
||||||
}: {
|
}: {
|
||||||
workflowId: string | undefined;
|
workflowId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
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 { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail';
|
||||||
import { WorkflowEditActionFormServerlessFunction } from '@/workflow/components/WorkflowEditActionFormServerlessFunction';
|
|
||||||
import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm';
|
|
||||||
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
|
||||||
import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep';
|
import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep';
|
||||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger';
|
import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
|
||||||
import { findStepPosition } from '@/workflow/utils/findStepPosition';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
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 = ({
|
export const RightDrawerWorkflowEditStepContent = ({
|
||||||
workflow,
|
workflow,
|
||||||
}: {
|
}: {
|
||||||
@ -75,47 +24,12 @@ export const RightDrawerWorkflowEditStepContent = ({
|
|||||||
stepId: workflowSelectedNode,
|
stepId: workflowSelectedNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stepDefinition = getStepDefinitionOrThrow({
|
return (
|
||||||
stepId: workflowSelectedNode,
|
<WorkflowStepDetail
|
||||||
workflow,
|
stepId={workflowSelectedNode}
|
||||||
});
|
workflowVersion={workflow.currentVersion}
|
||||||
if (!isDefined(stepDefinition)) {
|
onActionUpdate={updateStep}
|
||||||
return null;
|
onTriggerUpdate={updateTrigger}
|
||||||
}
|
/>
|
||||||
|
|
||||||
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)}`,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { WorkflowVersionStatusTag } from '@/workflow/components/WorkflowVersionStatusTag';
|
||||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||||
import {
|
import {
|
||||||
WorkflowDiagram,
|
WorkflowDiagram,
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
WorkflowDiagramNode,
|
WorkflowDiagramNode,
|
||||||
|
WorkflowDiagramNodeType,
|
||||||
} from '@/workflow/types/WorkflowDiagram';
|
} from '@/workflow/types/WorkflowDiagram';
|
||||||
import { getOrganizedDiagram } from '@/workflow/utils/getOrganizedDiagram';
|
import { getOrganizedDiagram } from '@/workflow/utils/getOrganizedDiagram';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@ -19,13 +16,34 @@ import {
|
|||||||
EdgeChange,
|
EdgeChange,
|
||||||
FitViewOptions,
|
FitViewOptions,
|
||||||
NodeChange,
|
NodeChange,
|
||||||
|
NodeProps,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { GRAY_SCALE, isDefined } from 'twenty-ui';
|
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`
|
const StyledStatusTagContainer = styled.div`
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -38,12 +56,26 @@ const defaultFitViewOptions: FitViewOptions = {
|
|||||||
maxZoom: 1.3,
|
maxZoom: 1.3,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowDiagramCanvas = ({
|
export const WorkflowDiagramCanvasBase = ({
|
||||||
diagram,
|
diagram,
|
||||||
workflowWithCurrentVersion,
|
status,
|
||||||
|
nodeTypes,
|
||||||
|
children,
|
||||||
}: {
|
}: {
|
||||||
diagram: WorkflowDiagram;
|
diagram: WorkflowDiagram;
|
||||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion;
|
status: WorkflowVersionStatus;
|
||||||
|
nodeTypes: Partial<
|
||||||
|
Record<
|
||||||
|
WorkflowDiagramNodeType,
|
||||||
|
React.ComponentType<
|
||||||
|
NodeProps & {
|
||||||
|
data: any;
|
||||||
|
type: any;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
children?: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const { nodes, edges } = useMemo(
|
const { nodes, edges } = useMemo(
|
||||||
() => getOrganizedDiagram(diagram),
|
() => getOrganizedDiagram(diagram),
|
||||||
@ -87,33 +119,26 @@ export const WorkflowDiagramCanvas = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StyledResetReactflowStyles>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
key={workflowWithCurrentVersion.currentVersion.id}
|
|
||||||
onInit={({ fitView }) => {
|
onInit={({ fitView }) => {
|
||||||
fitView(defaultFitViewOptions);
|
fitView(defaultFitViewOptions);
|
||||||
}}
|
}}
|
||||||
nodeTypes={{
|
nodeTypes={nodeTypes}
|
||||||
default: WorkflowDiagramStepNode,
|
|
||||||
'create-step': WorkflowDiagramCreateStepNode,
|
|
||||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
|
||||||
}}
|
|
||||||
fitView
|
fitView
|
||||||
nodes={nodes.map((node) => ({ ...node, draggable: false }))}
|
nodes={nodes.map((node) => ({ ...node, draggable: false }))}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={handleNodesChange}
|
onNodesChange={handleNodesChange}
|
||||||
onEdgesChange={handleEdgesChange}
|
onEdgesChange={handleEdgesChange}
|
||||||
>
|
>
|
||||||
<WorkflowDiagramCanvasEffect />
|
|
||||||
|
|
||||||
<Background color={GRAY_SCALE.gray25} size={2} />
|
<Background color={GRAY_SCALE.gray25} size={2} />
|
||||||
</ReactFlow>
|
|
||||||
|
|
||||||
<StyledStatusTagContainer>
|
{children}
|
||||||
<WorkflowVersionStatusTag
|
|
||||||
versionStatus={workflowWithCurrentVersion.currentVersion.status}
|
<StyledStatusTagContainer>
|
||||||
/>
|
<WorkflowVersionStatusTag versionStatus={status} />
|
||||||
</StyledStatusTagContainer>
|
</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 { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation';
|
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 { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||||
import {
|
import { WorkflowDiagramNode } from '@/workflow/types/WorkflowDiagram';
|
||||||
WorkflowDiagramEdge,
|
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||||
WorkflowDiagramNode,
|
import { useCallback } from 'react';
|
||||||
} from '@/workflow/types/WorkflowDiagram';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import {
|
|
||||||
OnSelectionChangeParams,
|
|
||||||
useOnSelectionChange,
|
|
||||||
useReactFlow,
|
|
||||||
} from '@xyflow/react';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const WorkflowDiagramCanvasEffect = () => {
|
export const WorkflowDiagramCanvasEditableEffect = () => {
|
||||||
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();
|
|
||||||
|
|
||||||
const { startNodeCreation } = useStartNodeCreation();
|
const { startNodeCreation } = useStartNodeCreation();
|
||||||
|
|
||||||
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
||||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||||
|
|
||||||
const workflowDiagramTriggerNodeSelection = useRecoilValue(
|
|
||||||
workflowDiagramTriggerNodeSelectionState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectionChange = useCallback(
|
const handleSelectionChange = useCallback(
|
||||||
({ nodes }: OnSelectionChangeParams) => {
|
({ nodes }: OnSelectionChangeParams) => {
|
||||||
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
||||||
@ -65,15 +52,7 @@ export const WorkflowDiagramCanvasEffect = () => {
|
|||||||
onChange: handleSelectionChange,
|
onChange: handleSelectionChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useTriggerNodeSelection();
|
||||||
if (!isDefined(workflowDiagramTriggerNodeSelection)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reactflow.updateNode(workflowDiagramTriggerNodeSelection, {
|
|
||||||
selected: true,
|
|
||||||
});
|
|
||||||
}, [reactflow, workflowDiagramTriggerNodeSelection]);
|
|
||||||
|
|
||||||
return null;
|
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 { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
|
||||||
import {
|
import {
|
||||||
WorkflowVersion,
|
WorkflowVersion,
|
||||||
WorkflowWithCurrentVersion,
|
WorkflowWithCurrentVersion,
|
||||||
@ -13,22 +12,13 @@ import { useEffect } from 'react';
|
|||||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
type WorkflowEffectProps = {
|
export const WorkflowDiagramEffect = ({
|
||||||
workflowId: string;
|
|
||||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkflowEffect = ({
|
|
||||||
workflowId,
|
|
||||||
workflowWithCurrentVersion,
|
workflowWithCurrentVersion,
|
||||||
}: WorkflowEffectProps) => {
|
}: {
|
||||||
const setWorkflowId = useSetRecoilState(workflowIdState);
|
workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined;
|
||||||
|
}) => {
|
||||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setWorkflowId(workflowId);
|
|
||||||
}, [setWorkflowId, workflowId]);
|
|
||||||
|
|
||||||
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
|
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
|
||||||
({ snapshot, set }) => {
|
({ snapshot, set }) => {
|
||||||
return (currentVersion: WorkflowVersion) => {
|
return (currentVersion: WorkflowVersion) => {
|
||||||
@ -1,15 +1,9 @@
|
|||||||
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
|
|
||||||
import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode';
|
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 { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||||
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { IconCode, IconMail, IconPlaylistAdd } from 'twenty-ui';
|
||||||
import { IconCode, IconMail, IconPlaylistAdd, IconTrash } from 'twenty-ui';
|
|
||||||
|
|
||||||
const StyledStepNodeLabelIconContainer = styled.div`
|
const StyledStepNodeLabelIconContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -20,27 +14,15 @@ const StyledStepNodeLabelIconContainer = styled.div`
|
|||||||
padding: ${({ theme }) => theme.spacing(1)};
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const WorkflowDiagramStepNode = ({
|
export const WorkflowDiagramStepNodeBase = ({
|
||||||
id,
|
|
||||||
data,
|
data,
|
||||||
selected,
|
RightFloatingElement,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
|
||||||
data: WorkflowDiagramStepNodeData;
|
data: WorkflowDiagramStepNodeData;
|
||||||
selected?: boolean;
|
RightFloatingElement?: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const workflowId = useRecoilValue(workflowIdState);
|
|
||||||
|
|
||||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
|
||||||
assertWorkflowWithCurrentVersionIsDefined(workflowWithCurrentVersion);
|
|
||||||
|
|
||||||
const { deleteOneStep } = useDeleteOneStep({
|
|
||||||
workflow: workflowWithCurrentVersion,
|
|
||||||
stepId: id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderStepIcon = () => {
|
const renderStepIcon = () => {
|
||||||
switch (data.nodeType) {
|
switch (data.nodeType) {
|
||||||
case 'trigger': {
|
case 'trigger': {
|
||||||
@ -87,16 +69,7 @@ export const WorkflowDiagramStepNode = ({
|
|||||||
nodeType={data.nodeType}
|
nodeType={data.nodeType}
|
||||||
label={data.label}
|
label={data.label}
|
||||||
Icon={renderStepIcon()}
|
Icon={renderStepIcon()}
|
||||||
RightFloatingElement={
|
RightFloatingElement={RightFloatingElement}
|
||||||
selected ? (
|
|
||||||
<FloatingIconButton
|
|
||||||
Icon={IconTrash}
|
|
||||||
onClick={() => {
|
|
||||||
return deleteOneStep();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -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)};
|
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type WorkflowEditActionFormSendEmailProps =
|
||||||
|
| {
|
||||||
|
action: WorkflowSendEmailStep;
|
||||||
|
readonly: true;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
action: WorkflowSendEmailStep;
|
||||||
|
readonly?: false;
|
||||||
|
onActionUpdate: (action: WorkflowSendEmailStep) => void;
|
||||||
|
};
|
||||||
|
|
||||||
type SendEmailFormData = {
|
type SendEmailFormData = {
|
||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowEditActionFormSendEmail = ({
|
export const WorkflowEditActionFormSendEmail = (
|
||||||
action,
|
props: WorkflowEditActionFormSendEmailProps,
|
||||||
onActionUpdate,
|
) => {
|
||||||
}: {
|
|
||||||
action: WorkflowSendEmailStep;
|
|
||||||
onActionUpdate: (action: WorkflowSendEmailStep) => void;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const form = useForm<SendEmailFormData>({
|
const form = useForm<SendEmailFormData>({
|
||||||
@ -35,18 +42,23 @@ export const WorkflowEditActionFormSendEmail = ({
|
|||||||
subject: '',
|
subject: '',
|
||||||
body: '',
|
body: '',
|
||||||
},
|
},
|
||||||
|
disabled: props.readonly,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.setValue('subject', action.settings.subject ?? '');
|
form.setValue('subject', props.action.settings.subject ?? '');
|
||||||
form.setValue('body', action.settings.template ?? '');
|
form.setValue('body', props.action.settings.template ?? '');
|
||||||
}, [action.settings.subject, action.settings.template, form]);
|
}, [props.action.settings.subject, props.action.settings.template, form]);
|
||||||
|
|
||||||
const saveAction = useDebouncedCallback((formData: SendEmailFormData) => {
|
const saveAction = useDebouncedCallback((formData: SendEmailFormData) => {
|
||||||
onActionUpdate({
|
if (props.readonly === true) {
|
||||||
...action,
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onActionUpdate({
|
||||||
|
...props.action,
|
||||||
settings: {
|
settings: {
|
||||||
...action.settings,
|
...props.action.settings,
|
||||||
title: formData.subject,
|
title: formData.subject,
|
||||||
subject: formData.subject,
|
subject: formData.subject,
|
||||||
template: formData.body,
|
template: formData.body,
|
||||||
@ -77,6 +89,7 @@ export const WorkflowEditActionFormSendEmail = ({
|
|||||||
label="Subject"
|
label="Subject"
|
||||||
placeholder="Thank you for building such an awesome CRM!"
|
placeholder="Thank you for building such an awesome CRM!"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
|
disabled={field.disabled}
|
||||||
onChange={(email) => {
|
onChange={(email) => {
|
||||||
field.onChange(email);
|
field.onChange(email);
|
||||||
|
|
||||||
@ -95,6 +108,7 @@ export const WorkflowEditActionFormSendEmail = ({
|
|||||||
placeholder="Thank you so much!"
|
placeholder="Thank you so much!"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
minRows={4}
|
minRows={4}
|
||||||
|
disabled={field.disabled}
|
||||||
onChange={(email) => {
|
onChange={(email) => {
|
||||||
field.onChange(email);
|
field.onChange(email);
|
||||||
|
|
||||||
|
|||||||
@ -13,13 +13,20 @@ const StyledTriggerSettings = styled.div`
|
|||||||
row-gap: ${({ theme }) => theme.spacing(4)};
|
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const WorkflowEditActionFormServerlessFunction = ({
|
type WorkflowEditActionFormServerlessFunctionProps =
|
||||||
action,
|
| {
|
||||||
onActionUpdate,
|
action: WorkflowCodeStep;
|
||||||
}: {
|
readonly: true;
|
||||||
action: WorkflowCodeStep;
|
}
|
||||||
onActionUpdate: (trigger: WorkflowCodeStep) => void;
|
| {
|
||||||
}) => {
|
action: WorkflowCodeStep;
|
||||||
|
readonly?: false;
|
||||||
|
onActionUpdate: (action: WorkflowCodeStep) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowEditActionFormServerlessFunction = (
|
||||||
|
props: WorkflowEditActionFormServerlessFunctionProps,
|
||||||
|
) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||||
@ -47,13 +54,18 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
dropdownId="workflow-edit-action-function"
|
dropdownId="workflow-edit-action-function"
|
||||||
label="Function"
|
label="Function"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={action.settings.serverlessFunctionId}
|
value={props.action.settings.serverlessFunctionId}
|
||||||
options={availableFunctions}
|
options={availableFunctions}
|
||||||
|
disabled={props.readonly}
|
||||||
onChange={(updatedFunction) => {
|
onChange={(updatedFunction) => {
|
||||||
onActionUpdate({
|
if (props.readonly === true) {
|
||||||
...action,
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onActionUpdate({
|
||||||
|
...props.action,
|
||||||
settings: {
|
settings: {
|
||||||
...action.settings,
|
...props.action.settings,
|
||||||
serverlessFunctionId: updatedFunction,
|
serverlessFunctionId: updatedFunction,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -45,13 +45,23 @@ const StyledTriggerSettings = styled.div`
|
|||||||
row-gap: ${({ theme }) => theme.spacing(4)};
|
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 = ({
|
export const WorkflowEditTriggerForm = ({
|
||||||
trigger,
|
trigger,
|
||||||
|
readonly,
|
||||||
onTriggerUpdate,
|
onTriggerUpdate,
|
||||||
}: {
|
}: WorkflowEditTriggerFormProps) => {
|
||||||
trigger: WorkflowTrigger | undefined;
|
|
||||||
onTriggerUpdate: (trigger: WorkflowTrigger) => void;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||||
@ -102,9 +112,14 @@ export const WorkflowEditTriggerForm = ({
|
|||||||
dropdownId="workflow-edit-trigger-record-type"
|
dropdownId="workflow-edit-trigger-record-type"
|
||||||
label="Record Type"
|
label="Record Type"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disabled={readonly}
|
||||||
value={triggerEvent?.objectType}
|
value={triggerEvent?.objectType}
|
||||||
options={availableMetadata}
|
options={availableMetadata}
|
||||||
onChange={(updatedRecordType) => {
|
onChange={(updatedRecordType) => {
|
||||||
|
if (readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onTriggerUpdate(
|
onTriggerUpdate(
|
||||||
isDefined(trigger) && isDefined(triggerEvent)
|
isDefined(trigger) && isDefined(triggerEvent)
|
||||||
? {
|
? {
|
||||||
@ -129,7 +144,12 @@ export const WorkflowEditTriggerForm = ({
|
|||||||
fullWidth
|
fullWidth
|
||||||
value={triggerEvent?.event}
|
value={triggerEvent?.event}
|
||||||
options={OBJECT_EVENT_TRIGGERS}
|
options={OBJECT_EVENT_TRIGGERS}
|
||||||
|
disabled={readonly}
|
||||||
onChange={(updatedEvent) => {
|
onChange={(updatedEvent) => {
|
||||||
|
if (readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onTriggerUpdate(
|
onTriggerUpdate(
|
||||||
isDefined(trigger) && isDefined(triggerEvent)
|
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 { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||||
|
|
||||||
export const useCreateNewWorkflowVersion = ({
|
export const useCreateNewWorkflowVersion = () => {
|
||||||
workflowId,
|
|
||||||
}: {
|
|
||||||
workflowId: string;
|
|
||||||
}) => {
|
|
||||||
const { createOneRecord: createOneWorkflowVersion } =
|
const { createOneRecord: createOneWorkflowVersion } =
|
||||||
useCreateOneRecord<WorkflowVersion>({
|
useCreateOneRecord<WorkflowVersion>({
|
||||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
@ -15,13 +11,10 @@ export const useCreateNewWorkflowVersion = ({
|
|||||||
const createNewWorkflowVersion = (
|
const createNewWorkflowVersion = (
|
||||||
workflowVersionData: Pick<
|
workflowVersionData: Pick<
|
||||||
WorkflowVersion,
|
WorkflowVersion,
|
||||||
'name' | 'status' | 'trigger' | 'steps'
|
'workflowId' | 'name' | 'status' | 'trigger' | 'steps'
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
return createOneWorkflowVersion({
|
return createOneWorkflowVersion(workflowVersionData);
|
||||||
workflowId,
|
|
||||||
...workflowVersionData,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -32,9 +32,7 @@ export const useCreateStep = ({
|
|||||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
|
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||||
workflowId: workflow.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const insertNodeAndSave = async ({
|
const insertNodeAndSave = async ({
|
||||||
parentNodeId,
|
parentNodeId,
|
||||||
@ -66,6 +64,7 @@ export const useCreateStep = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createNewWorkflowVersion({
|
await createNewWorkflowVersion({
|
||||||
|
workflowId: workflow.id,
|
||||||
name: `v${workflow.versions.length + 1}`,
|
name: `v${workflow.versions.length + 1}`,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
trigger: workflow.currentVersion.trigger,
|
trigger: workflow.currentVersion.trigger,
|
||||||
|
|||||||
@ -20,9 +20,7 @@ export const useDeleteOneStep = ({
|
|||||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
|
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||||
workflowId: workflow.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteOneStep = async () => {
|
const deleteOneStep = async () => {
|
||||||
if (workflow.currentVersion.status !== 'DRAFT') {
|
if (workflow.currentVersion.status !== 'DRAFT') {
|
||||||
@ -30,6 +28,7 @@ export const useDeleteOneStep = ({
|
|||||||
|
|
||||||
if (stepId === TRIGGER_STEP_ID) {
|
if (stepId === TRIGGER_STEP_ID) {
|
||||||
await createNewWorkflowVersion({
|
await createNewWorkflowVersion({
|
||||||
|
workflowId: workflow.id,
|
||||||
name: newVersionName,
|
name: newVersionName,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
trigger: null,
|
trigger: null,
|
||||||
@ -37,6 +36,7 @@ export const useDeleteOneStep = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createNewWorkflowVersion({
|
await createNewWorkflowVersion({
|
||||||
|
workflowId: workflow.id,
|
||||||
name: newVersionName,
|
name: newVersionName,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
trigger: workflow.currentVersion.trigger,
|
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,
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
|
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||||
workflowId: workflow.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
|
const updateStep = async <T extends WorkflowStep>(updatedStep: T) => {
|
||||||
if (!isDefined(workflow.currentVersion)) {
|
if (!isDefined(workflow.currentVersion)) {
|
||||||
@ -48,6 +46,7 @@ export const useUpdateWorkflowVersionStep = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createNewWorkflowVersion({
|
await createNewWorkflowVersion({
|
||||||
|
workflowId: workflow.id,
|
||||||
name: `v${workflow.versions.length + 1}`,
|
name: `v${workflow.versions.length + 1}`,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
trigger: workflow.currentVersion.trigger,
|
trigger: workflow.currentVersion.trigger,
|
||||||
|
|||||||
@ -18,9 +18,7 @@ export const useUpdateWorkflowVersionTrigger = ({
|
|||||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({
|
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||||
workflowId: workflow.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateTrigger = async (updatedTrigger: WorkflowTrigger) => {
|
const updateTrigger = async (updatedTrigger: WorkflowTrigger) => {
|
||||||
if (!isDefined(workflow.currentVersion)) {
|
if (!isDefined(workflow.currentVersion)) {
|
||||||
@ -39,6 +37,7 @@ export const useUpdateWorkflowVersionTrigger = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createNewWorkflowVersion({
|
await createNewWorkflowVersion({
|
||||||
|
workflowId: workflow.id,
|
||||||
name: `v${workflow.versions.length + 1}`,
|
name: `v${workflow.versions.length + 1}`,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
trigger: updatedTrigger,
|
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 =
|
export type WorkflowDiagramNodeData =
|
||||||
| WorkflowDiagramStepNodeData
|
| WorkflowDiagramStepNodeData
|
||||||
| WorkflowDiagramCreateStepNodeData;
|
| 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 { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||||
import { RecordShowPageWorkflowHeader } from '@/workflow/components/RecordShowPageWorkflowHeader';
|
import { RecordShowPageWorkflowHeader } from '@/workflow/components/RecordShowPageWorkflowHeader';
|
||||||
|
import { RecordShowPageWorkflowVersionHeader } from '@/workflow/components/RecordShowPageWorkflowVersionHeader';
|
||||||
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
|
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
|
||||||
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
|
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
|
||||||
|
|
||||||
@ -47,8 +48,11 @@ export const RecordShowPage = () => {
|
|||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{objectNameSingular === CoreObjectNameSingular.Workflow ? (
|
{objectNameSingular === CoreObjectNameSingular.Workflow ? (
|
||||||
<RecordShowPageWorkflowHeader
|
<RecordShowPageWorkflowHeader workflowId={objectRecordId} />
|
||||||
workflowId={parameters.objectRecordId}
|
) : objectNameSingular ===
|
||||||
|
CoreObjectNameSingular.WorkflowVersion ? (
|
||||||
|
<RecordShowPageWorkflowVersionHeader
|
||||||
|
workflowVersionId={objectRecordId}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RecordShowPageBaseHeader
|
<RecordShowPageBaseHeader
|
||||||
|
|||||||
Reference in New Issue
Block a user