Update workflow nodes configuration (#6861)
- Improve the design of the right drawer - Allow to update the trigger of the workflow: the object and the event listened to - Allow to update the selected serverless function that a code action should execute - Change how we determine which workflow version to display in the visualizer. We fetch the selected workflow's data, including whether it has a draft or a published version. If the workflow has a draft version, it gets displayed; otherwise, we display the last published version. - I used the type `WorkflowWithCurrentVersion` to forward the currently edited workflow with its _current_ version embedded across the app. - I created single-responsibility hooks like `useFindWorkflowWithCurrentVersion`, `useFindShowPageWorkflow`, `useUpdateWorkflowVersionTrigger` or `useUpdateWorkflowVersionStep`. - I updated the types for workflow related objects, like `Workflow` and `WorkflowVersion`. See `packages/twenty-front/src/modules/workflow/types/Workflow.ts`. - This introduced the possibility to have `null` values for triggers and steps. I made the according changes in the codebase and in the tests. - I created a utility function to extract both parts of object-event format (`company.created`): `packages/twenty-front/src/modules/workflow/utils/splitWorkflowTriggerEventName.ts`
This commit is contained in:
committed by
GitHub
parent
c55dfbde6e
commit
a2b1062db6
@ -1,10 +1,16 @@
|
|||||||
import { showPageWorkflowSelectedNodeState } from '@/workflow/states/showPageWorkflowSelectedNodeState';
|
import { RightDrawerWorkflowEditStepContent } from '@/workflow/components/RightDrawerWorkflowEditStepContent';
|
||||||
|
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||||
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const RightDrawerWorkflowEditStep = () => {
|
export const RightDrawerWorkflowEditStep = () => {
|
||||||
const showPageWorkflowSelectedNode = useRecoilValue(
|
const workflowId = useRecoilValue(workflowIdState);
|
||||||
showPageWorkflowSelectedNodeState,
|
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||||
);
|
|
||||||
|
|
||||||
return <p>{showPageWorkflowSelectedNode}</p>;
|
if (!isDefined(workflow)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RightDrawerWorkflowEditStepContent workflow={workflow} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
import { WorkflowEditActionForm } from '@/workflow/components/WorkflowEditActionForm';
|
||||||
|
import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
||||||
|
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 { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow';
|
||||||
|
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)) {
|
||||||
|
throw new Error('Expected to find the definition of the trigger');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'trigger',
|
||||||
|
definition: currentVersion.trigger,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDefined(currentVersion.steps)) {
|
||||||
|
throw new Error('Expected to find an array of steps');
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedNodePosition = findStepPositionOrThrow({
|
||||||
|
steps: currentVersion.steps,
|
||||||
|
stepId: stepId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'action',
|
||||||
|
definition: selectedNodePosition.steps[selectedNodePosition.index],
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightDrawerWorkflowEditStepContent = ({
|
||||||
|
workflow,
|
||||||
|
}: {
|
||||||
|
workflow: WorkflowWithCurrentVersion;
|
||||||
|
}) => {
|
||||||
|
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||||
|
if (!isDefined(workflowSelectedNode)) {
|
||||||
|
throw new Error(
|
||||||
|
'Expected a node to be selected. Selecting a node is mandatory to edit it.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow });
|
||||||
|
const { updateStep } = useUpdateWorkflowVersionStep({
|
||||||
|
workflow,
|
||||||
|
stepId: workflowSelectedNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepDefinition = getStepDefinitionOrThrow({
|
||||||
|
stepId: workflowSelectedNode,
|
||||||
|
workflow,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stepDefinition.type === 'trigger') {
|
||||||
|
return (
|
||||||
|
<WorkflowEditTriggerForm
|
||||||
|
trigger={stepDefinition.definition}
|
||||||
|
onUpdateTrigger={updateTrigger}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowEditActionForm
|
||||||
|
action={stepDefinition.definition}
|
||||||
|
onUpdateAction={updateStep}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,24 +1,12 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
|
||||||
import { RightDrawerWorkflowSelectActionContent } from '@/workflow/components/RightDrawerWorkflowSelectActionContent';
|
import { RightDrawerWorkflowSelectActionContent } from '@/workflow/components/RightDrawerWorkflowSelectActionContent';
|
||||||
import { showPageWorkflowIdState } from '@/workflow/states/showPageWorkflowIdState';
|
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||||
import { Workflow } from '@/workflow/types/Workflow';
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const RightDrawerWorkflowSelectAction = () => {
|
export const RightDrawerWorkflowSelectAction = () => {
|
||||||
const showPageWorkflowId = useRecoilValue(showPageWorkflowIdState);
|
const workflowId = useRecoilValue(workflowIdState);
|
||||||
|
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||||
const { record: workflow } = useFindOneRecord<Workflow>({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
|
||||||
objectRecordId: showPageWorkflowId,
|
|
||||||
recordGqlFields: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
versions: true,
|
|
||||||
publishedVersionId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(workflow)) {
|
if (!isDefined(workflow)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -1,19 +1,9 @@
|
|||||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { useRightDrawerWorkflowSelectAction } from '@/workflow/hooks/useRightDrawerWorkflowSelectAction';
|
import { ACTIONS } from '@/workflow/constants/Actions';
|
||||||
import { Workflow } from '@/workflow/types/Workflow';
|
import { useCreateStep } from '@/workflow/hooks/useCreateStep';
|
||||||
|
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
// FIXME: copy-pasted
|
|
||||||
const StyledTabListContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
|
||||||
height: 40px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledActionListContainer = styled.div`
|
const StyledActionListContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -24,33 +14,24 @@ const StyledActionListContainer = styled.div`
|
|||||||
padding-inline: ${({ theme }) => theme.spacing(2)};
|
padding-inline: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TAB_LIST_COMPONENT_ID =
|
|
||||||
'workflow-select-action-page-right-tab-list';
|
|
||||||
|
|
||||||
export const RightDrawerWorkflowSelectActionContent = ({
|
export const RightDrawerWorkflowSelectActionContent = ({
|
||||||
workflow,
|
workflow,
|
||||||
}: {
|
}: {
|
||||||
workflow: Workflow;
|
workflow: WorkflowWithCurrentVersion;
|
||||||
}) => {
|
}) => {
|
||||||
const tabListId = `${TAB_LIST_COMPONENT_ID}`;
|
const { createStep } = useCreateStep({
|
||||||
|
workflow,
|
||||||
const { tabs, options, handleActionClick } =
|
});
|
||||||
useRightDrawerWorkflowSelectAction({ tabListId, workflow });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledTabListContainer>
|
|
||||||
<TabList loading={false} tabListId={tabListId} tabs={tabs} />
|
|
||||||
</StyledTabListContainer>
|
|
||||||
|
|
||||||
<StyledActionListContainer>
|
<StyledActionListContainer>
|
||||||
{options.map((option) => (
|
{ACTIONS.map((action) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={option.id}
|
LeftIcon={action.icon}
|
||||||
LeftIcon={option.icon}
|
text={action.label}
|
||||||
text={option.name}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleActionClick(option.id);
|
return createStep(action.type);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { WorkflowShowPageDiagramCreateStepNode } from '@/workflow/components/WorkflowShowPageDiagramCreateStepNode';
|
import { WorkflowDiagramCanvasEffect } from '@/workflow/components/WorkflowDiagramCanvasEffect';
|
||||||
import { WorkflowShowPageDiagramEffect } from '@/workflow/components/WorkflowShowPageDiagramEffect';
|
import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode';
|
||||||
import { WorkflowShowPageDiagramStepNode } from '@/workflow/components/WorkflowShowPageDiagramStepNode';
|
import { WorkflowDiagramStepNode } from '@/workflow/components/WorkflowDiagramStepNode';
|
||||||
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
|
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||||
import {
|
import {
|
||||||
WorkflowDiagram,
|
WorkflowDiagram,
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
@ -21,7 +21,7 @@ import { 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';
|
||||||
|
|
||||||
export const WorkflowShowPageDiagram = ({
|
export const WorkflowDiagramCanvas = ({
|
||||||
diagram,
|
diagram,
|
||||||
}: {
|
}: {
|
||||||
diagram: WorkflowDiagram;
|
diagram: WorkflowDiagram;
|
||||||
@ -31,14 +31,12 @@ export const WorkflowShowPageDiagram = ({
|
|||||||
[diagram],
|
[diagram],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setShowPageWorkflowDiagram = useSetRecoilState(
|
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||||
showPageWorkflowDiagramState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleNodesChange = (
|
const handleNodesChange = (
|
||||||
nodeChanges: Array<NodeChange<WorkflowDiagramNode>>,
|
nodeChanges: Array<NodeChange<WorkflowDiagramNode>>,
|
||||||
) => {
|
) => {
|
||||||
setShowPageWorkflowDiagram((diagram) => {
|
setWorkflowDiagram((diagram) => {
|
||||||
if (isDefined(diagram) === false) {
|
if (isDefined(diagram) === false) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'It must be impossible for the nodes to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
|
'It must be impossible for the nodes to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
|
||||||
@ -55,7 +53,7 @@ export const WorkflowShowPageDiagram = ({
|
|||||||
const handleEdgesChange = (
|
const handleEdgesChange = (
|
||||||
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
||||||
) => {
|
) => {
|
||||||
setShowPageWorkflowDiagram((diagram) => {
|
setWorkflowDiagram((diagram) => {
|
||||||
if (isDefined(diagram) === false) {
|
if (isDefined(diagram) === false) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'It must be impossible for the edges to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
|
'It must be impossible for the edges to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
|
||||||
@ -72,8 +70,8 @@ export const WorkflowShowPageDiagram = ({
|
|||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodeTypes={{
|
nodeTypes={{
|
||||||
default: WorkflowShowPageDiagramStepNode,
|
default: WorkflowDiagramStepNode,
|
||||||
'create-step': WorkflowShowPageDiagramCreateStepNode,
|
'create-step': WorkflowDiagramCreateStepNode,
|
||||||
}}
|
}}
|
||||||
fitView
|
fitView
|
||||||
nodes={nodes.map((node) => ({ ...node, draggable: false }))}
|
nodes={nodes.map((node) => ({ ...node, draggable: false }))}
|
||||||
@ -81,7 +79,7 @@ export const WorkflowShowPageDiagram = ({
|
|||||||
onNodesChange={handleNodesChange}
|
onNodesChange={handleNodesChange}
|
||||||
onEdgesChange={handleEdgesChange}
|
onEdgesChange={handleEdgesChange}
|
||||||
>
|
>
|
||||||
<WorkflowShowPageDiagramEffect />
|
<WorkflowDiagramCanvasEffect />
|
||||||
|
|
||||||
<Background color={GRAY_SCALE.gray25} size={2} />
|
<Background color={GRAY_SCALE.gray25} size={2} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
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 { showPageWorkflowDiagramTriggerNodeSelectionState } from '@/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState';
|
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
|
||||||
import { showPageWorkflowSelectedNodeState } from '@/workflow/states/showPageWorkflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||||
import {
|
import {
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
WorkflowDiagramNode,
|
WorkflowDiagramNode,
|
||||||
@ -16,18 +16,16 @@ import { useCallback, useEffect } from 'react';
|
|||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const WorkflowShowPageDiagramEffect = () => {
|
export const WorkflowDiagramCanvasEffect = () => {
|
||||||
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();
|
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();
|
||||||
|
|
||||||
const { startNodeCreation } = useStartNodeCreation();
|
const { startNodeCreation } = useStartNodeCreation();
|
||||||
|
|
||||||
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
||||||
const setShowPageWorkflowSelectedNode = useSetRecoilState(
|
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||||
showPageWorkflowSelectedNodeState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const showPageWorkflowDiagramTriggerNodeSelection = useRecoilValue(
|
const workflowDiagramTriggerNodeSelection = useRecoilValue(
|
||||||
showPageWorkflowDiagramTriggerNodeSelectionState,
|
workflowDiagramTriggerNodeSelectionState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectionChange = useCallback(
|
const handleSelectionChange = useCallback(
|
||||||
@ -52,13 +50,13 @@ export const WorkflowShowPageDiagramEffect = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowPageWorkflowSelectedNode(selectedNode.id);
|
setWorkflowSelectedNode(selectedNode.id);
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
closeRightDrawer,
|
closeRightDrawer,
|
||||||
openRightDrawer,
|
openRightDrawer,
|
||||||
setShowPageWorkflowSelectedNode,
|
setWorkflowSelectedNode,
|
||||||
startNodeCreation,
|
startNodeCreation,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -68,14 +66,14 @@ export const WorkflowShowPageDiagramEffect = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDefined(showPageWorkflowDiagramTriggerNodeSelection)) {
|
if (!isDefined(workflowDiagramTriggerNodeSelection)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
reactflow.updateNode(showPageWorkflowDiagramTriggerNodeSelection, {
|
reactflow.updateNode(workflowDiagramTriggerNodeSelection, {
|
||||||
selected: true,
|
selected: true,
|
||||||
});
|
});
|
||||||
}, [reactflow, showPageWorkflowDiagramTriggerNodeSelection]);
|
}, [reactflow, workflowDiagramTriggerNodeSelection]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@ -7,7 +7,7 @@ export const StyledTargetHandle = styled(Handle)`
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const WorkflowShowPageDiagramCreateStepNode = () => {
|
export const WorkflowDiagramCreateStepNode = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledTargetHandle type="target" position={Position.Top} />
|
<StyledTargetHandle type="target" position={Position.Top} />
|
||||||
@ -64,7 +64,7 @@ export const StyledTargetHandle = styled(Handle)`
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const WorkflowShowPageDiagramStepNode = ({
|
export const WorkflowDiagramStepNode = ({
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
data: WorkflowDiagramStepNodeData;
|
data: WorkflowDiagramStepNodeData;
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||||
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
|
import { WorkflowAction } from '@/workflow/types/Workflow';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconCode, isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledTriggerHeader = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: ${({ theme }) => theme.spacing(6)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTriggerHeaderTitle = styled.p`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xl};
|
||||||
|
|
||||||
|
margin: ${({ theme }) => theme.spacing(3)} 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTriggerHeaderType = styled.p`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTriggerHeaderIconContainer = styled.div`
|
||||||
|
align-self: flex-start;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTriggerSettings = styled.div`
|
||||||
|
padding: ${({ theme }) => theme.spacing(6)};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowEditActionForm = ({
|
||||||
|
action,
|
||||||
|
onUpdateAction,
|
||||||
|
}: {
|
||||||
|
action: WorkflowAction;
|
||||||
|
onUpdateAction: (trigger: WorkflowAction) => void;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||||
|
|
||||||
|
const availableFunctions: Array<SelectOption<string>> = [
|
||||||
|
{ label: 'None', value: '' },
|
||||||
|
...serverlessFunctions
|
||||||
|
.filter((serverlessFunction) =>
|
||||||
|
isDefined(serverlessFunction.latestVersion),
|
||||||
|
)
|
||||||
|
.map((serverlessFunction) => ({
|
||||||
|
label: serverlessFunction.name,
|
||||||
|
value: serverlessFunction.id,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledTriggerHeader>
|
||||||
|
<StyledTriggerHeaderIconContainer>
|
||||||
|
<IconCode color={theme.color.orange} />
|
||||||
|
</StyledTriggerHeaderIconContainer>
|
||||||
|
|
||||||
|
<StyledTriggerHeaderTitle>
|
||||||
|
Code - Serverless Function
|
||||||
|
</StyledTriggerHeaderTitle>
|
||||||
|
|
||||||
|
<StyledTriggerHeaderType>Code</StyledTriggerHeaderType>
|
||||||
|
</StyledTriggerHeader>
|
||||||
|
|
||||||
|
<StyledTriggerSettings>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-action-function"
|
||||||
|
label="Function"
|
||||||
|
fullWidth
|
||||||
|
value={action.settings.serverlessFunctionId}
|
||||||
|
options={availableFunctions}
|
||||||
|
onChange={(updatedFunction) => {
|
||||||
|
onUpdateAction({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
serverlessFunctionId: updatedFunction,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledTriggerSettings>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
|
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/constants/ObjectEventTriggers';
|
||||||
|
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||||
|
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconPlaylistAdd, isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledTriggerHeader = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: ${({ theme }) => theme.spacing(6)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTriggerHeaderTitle = styled.p`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xl};
|
||||||
|
|
||||||
|
margin: ${({ theme }) => theme.spacing(3)} 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTriggerHeaderType = styled.p`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTriggerHeaderIconContainer = styled.div`
|
||||||
|
align-self: flex-start;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTriggerSettings = styled.div`
|
||||||
|
padding: ${({ theme }) => theme.spacing(6)};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowEditTriggerForm = ({
|
||||||
|
trigger,
|
||||||
|
onUpdateTrigger,
|
||||||
|
}: {
|
||||||
|
trigger: WorkflowTrigger;
|
||||||
|
onUpdateTrigger: (trigger: WorkflowTrigger) => void;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const triggerEvent = splitWorkflowTriggerEventName(
|
||||||
|
trigger.settings.eventName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableMetadata: Array<SelectOption<string>> =
|
||||||
|
activeObjectMetadataItems.map((item) => ({
|
||||||
|
label: item.labelPlural,
|
||||||
|
value: item.nameSingular,
|
||||||
|
}));
|
||||||
|
const recordTypeMetadata = activeObjectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === triggerEvent.objectType,
|
||||||
|
);
|
||||||
|
if (!isDefined(recordTypeMetadata)) {
|
||||||
|
throw new Error(
|
||||||
|
'Expected to find the metadata configuration for the currently selected record type of the trigger.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedEvent = OBJECT_EVENT_TRIGGERS.find(
|
||||||
|
(availableEvent) => availableEvent.value === triggerEvent.event,
|
||||||
|
);
|
||||||
|
if (!isDefined(selectedEvent)) {
|
||||||
|
throw new Error('Expected to find the currently selected event type.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledTriggerHeader>
|
||||||
|
<StyledTriggerHeaderIconContainer>
|
||||||
|
<IconPlaylistAdd color={theme.font.color.tertiary} />
|
||||||
|
</StyledTriggerHeaderIconContainer>
|
||||||
|
|
||||||
|
<StyledTriggerHeaderTitle>
|
||||||
|
When a {recordTypeMetadata.labelSingular} is {selectedEvent.label}
|
||||||
|
</StyledTriggerHeaderTitle>
|
||||||
|
|
||||||
|
<StyledTriggerHeaderType>
|
||||||
|
Trigger . Record is {selectedEvent.label}
|
||||||
|
</StyledTriggerHeaderType>
|
||||||
|
</StyledTriggerHeader>
|
||||||
|
|
||||||
|
<StyledTriggerSettings>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-trigger-record-type"
|
||||||
|
label="Record Type"
|
||||||
|
fullWidth
|
||||||
|
value={triggerEvent.objectType}
|
||||||
|
options={availableMetadata}
|
||||||
|
onChange={(updatedRecordType) => {
|
||||||
|
onUpdateTrigger({
|
||||||
|
...trigger,
|
||||||
|
settings: {
|
||||||
|
...trigger.settings,
|
||||||
|
eventName: `${updatedRecordType}.${triggerEvent.event}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-trigger-event-type"
|
||||||
|
label="Event type"
|
||||||
|
fullWidth
|
||||||
|
value={triggerEvent.event}
|
||||||
|
options={OBJECT_EVENT_TRIGGERS}
|
||||||
|
onChange={(updatedEvent) => {
|
||||||
|
onUpdateTrigger({
|
||||||
|
...trigger,
|
||||||
|
settings: {
|
||||||
|
...trigger.settings,
|
||||||
|
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledTriggerSettings>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,12 +1,8 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||||
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
import { showPageWorkflowErrorState } from '@/workflow/states/showPageWorkflowErrorState';
|
|
||||||
import { showPageWorkflowIdState } from '@/workflow/states/showPageWorkflowIdState';
|
|
||||||
import { showPageWorkflowLoadingState } from '@/workflow/states/showPageWorkflowLoadingState';
|
|
||||||
import { Workflow } from '@/workflow/types/Workflow';
|
|
||||||
import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes';
|
import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes';
|
||||||
import { getWorkflowLastDiagramVersion } from '@/workflow/utils/getWorkflowLastDiagramVersion';
|
import { getWorkflowVersionDiagram } from '@/workflow/utils/getWorkflowVersionDiagram';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
@ -18,50 +14,29 @@ type WorkflowShowPageEffectProps = {
|
|||||||
export const WorkflowShowPageEffect = ({
|
export const WorkflowShowPageEffect = ({
|
||||||
workflowId,
|
workflowId,
|
||||||
}: WorkflowShowPageEffectProps) => {
|
}: WorkflowShowPageEffectProps) => {
|
||||||
const {
|
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||||
record: workflow,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
} = useFindOneRecord<Workflow>({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
|
||||||
objectRecordId: workflowId,
|
|
||||||
recordGqlFields: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
versions: true,
|
|
||||||
publishedVersionId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const setShowPageWorkflowId = useSetRecoilState(showPageWorkflowIdState);
|
const setWorkflowId = useSetRecoilState(workflowIdState);
|
||||||
const setCurrentWorkflowData = useSetRecoilState(
|
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||||
showPageWorkflowDiagramState,
|
|
||||||
);
|
|
||||||
const setCurrentWorkflowLoading = useSetRecoilState(
|
|
||||||
showPageWorkflowLoadingState,
|
|
||||||
);
|
|
||||||
const setCurrentWorkflowError = useSetRecoilState(showPageWorkflowErrorState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowPageWorkflowId(workflowId);
|
setWorkflowId(workflowId);
|
||||||
}, [setShowPageWorkflowId, workflowId]);
|
}, [setWorkflowId, workflowId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const flowLastVersion = getWorkflowLastDiagramVersion(workflow);
|
const currentVersion = workflowWithCurrentVersion?.currentVersion;
|
||||||
const flowWithCreateStepNodes = addCreateStepNodes(flowLastVersion);
|
if (!isDefined(currentVersion)) {
|
||||||
|
setWorkflowDiagram(undefined);
|
||||||
|
|
||||||
setCurrentWorkflowData(
|
return;
|
||||||
isDefined(workflow) ? flowWithCreateStepNodes : undefined,
|
}
|
||||||
);
|
|
||||||
}, [setCurrentWorkflowData, workflow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const lastWorkflowDiagram = getWorkflowVersionDiagram(currentVersion);
|
||||||
setCurrentWorkflowLoading(loading);
|
const workflowDiagramWithCreateStepNodes =
|
||||||
}, [loading, setCurrentWorkflowLoading]);
|
addCreateStepNodes(lastWorkflowDiagram);
|
||||||
|
|
||||||
useEffect(() => {
|
setWorkflowDiagram(workflowDiagramWithCreateStepNodes);
|
||||||
setCurrentWorkflowError(error);
|
}, [setWorkflowDiagram, workflowWithCurrentVersion?.currentVersion]);
|
||||||
}, [error, setCurrentWorkflowError]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { WorkflowStepType } from '@/workflow/types/Workflow';
|
||||||
|
import { IconComponent, IconSettingsAutomation } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const ACTIONS: Array<{
|
||||||
|
label: string;
|
||||||
|
type: WorkflowStepType;
|
||||||
|
icon: IconComponent;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
label: 'Serverless Function',
|
||||||
|
type: 'CODE_ACTION',
|
||||||
|
icon: IconSettingsAutomation,
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { SelectOption } from '@/ui/input/components/Select';
|
||||||
|
|
||||||
|
export const OBJECT_EVENT_TRIGGERS: Array<SelectOption<string>> = [
|
||||||
|
{
|
||||||
|
label: 'Created',
|
||||||
|
value: 'created',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Updated',
|
||||||
|
value: 'updated',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deleted',
|
||||||
|
value: 'deleted',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const TRIGGER_STEP_ID = 'trigger';
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
|
||||||
import {
|
|
||||||
Workflow,
|
|
||||||
WorkflowStep,
|
|
||||||
WorkflowVersion,
|
|
||||||
} from '@/workflow/types/Workflow';
|
|
||||||
import { getWorkflowLastVersion } from '@/workflow/utils/getWorkflowLastVersion';
|
|
||||||
import { insertStep } from '@/workflow/utils/insertStep';
|
|
||||||
import { isDefined } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const useCreateNode = ({ workflow }: { workflow: Workflow }) => {
|
|
||||||
const { updateOneRecord: updateOneWorkflowVersion } =
|
|
||||||
useUpdateOneRecord<WorkflowVersion>({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createNode = ({
|
|
||||||
parentNodeId,
|
|
||||||
nodeToAdd,
|
|
||||||
}: {
|
|
||||||
parentNodeId: string;
|
|
||||||
nodeToAdd: WorkflowStep;
|
|
||||||
}) => {
|
|
||||||
const lastVersion = getWorkflowLastVersion(workflow);
|
|
||||||
if (!isDefined(lastVersion)) {
|
|
||||||
throw new Error(
|
|
||||||
"Can't add a node when no version exists yet. Create a first workflow version before trying to add a node.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateOneWorkflowVersion({
|
|
||||||
idToUpdate: lastVersion.id,
|
|
||||||
updateOneRecordInput: {
|
|
||||||
steps: insertStep({
|
|
||||||
steps: lastVersion.steps,
|
|
||||||
parentStepId: parentNodeId,
|
|
||||||
stepToAdd: nodeToAdd,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
createNode,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState';
|
||||||
|
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
|
||||||
|
import {
|
||||||
|
WorkflowStep,
|
||||||
|
WorkflowStepType,
|
||||||
|
WorkflowVersion,
|
||||||
|
WorkflowWithCurrentVersion,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
import { getStepDefaultDefinition } from '@/workflow/utils/getStepDefaultDefinition';
|
||||||
|
import { insertStep } from '@/workflow/utils/insertStep';
|
||||||
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const useCreateStep = ({
|
||||||
|
workflow,
|
||||||
|
}: {
|
||||||
|
workflow: WorkflowWithCurrentVersion;
|
||||||
|
}) => {
|
||||||
|
const workflowCreateStepFromParentStepId = useRecoilValue(
|
||||||
|
workflowCreateStepFromParentStepIdState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
|
||||||
|
workflowDiagramTriggerNodeSelectionState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||||
|
useUpdateOneRecord<WorkflowVersion>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertNodeAndSave = ({
|
||||||
|
parentNodeId,
|
||||||
|
nodeToAdd,
|
||||||
|
}: {
|
||||||
|
parentNodeId: string;
|
||||||
|
nodeToAdd: WorkflowStep;
|
||||||
|
}) => {
|
||||||
|
const currentVersion = workflow.currentVersion;
|
||||||
|
if (!isDefined(currentVersion)) {
|
||||||
|
throw new Error("Can't add a node when there is no current version.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateOneWorkflowVersion({
|
||||||
|
idToUpdate: currentVersion.id,
|
||||||
|
updateOneRecordInput: {
|
||||||
|
steps: insertStep({
|
||||||
|
steps: currentVersion.steps ?? [],
|
||||||
|
parentStepId: parentNodeId,
|
||||||
|
stepToAdd: nodeToAdd,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStep = async (newStepType: WorkflowStepType) => {
|
||||||
|
if (!isDefined(workflowCreateStepFromParentStepId)) {
|
||||||
|
throw new Error('Select a step to create a new step from first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStep = getStepDefaultDefinition(newStepType);
|
||||||
|
|
||||||
|
await insertNodeAndSave({
|
||||||
|
parentNodeId: workflowCreateStepFromParentStepId,
|
||||||
|
nodeToAdd: newStep,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After the step has been created, select it.
|
||||||
|
* As the `insertNodeAndSave` function mutates the cached workflow before resolving,
|
||||||
|
* we are sure that the new node will have been created at this stage.
|
||||||
|
*
|
||||||
|
* Selecting the node will cause a right drawer to open in order to edit the step.
|
||||||
|
*/
|
||||||
|
setWorkflowDiagramTriggerNodeSelection(newStep.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createStep,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,117 +0,0 @@
|
|||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
|
||||||
import { useCreateNode } from '@/workflow/hooks/useCreateNode';
|
|
||||||
import { showPageWorkflowDiagramTriggerNodeSelectionState } from '@/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState';
|
|
||||||
import { workflowCreateStepFromParentStepIdState } from '@/workflow/states/workflowCreateStepFromParentStepIdState';
|
|
||||||
import { Workflow } from '@/workflow/types/Workflow';
|
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import {
|
|
||||||
IconPlaystationSquare,
|
|
||||||
IconPlug,
|
|
||||||
IconPlus,
|
|
||||||
IconSearch,
|
|
||||||
IconSettingsAutomation,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
export const useRightDrawerWorkflowSelectAction = ({
|
|
||||||
tabListId,
|
|
||||||
workflow,
|
|
||||||
}: {
|
|
||||||
tabListId: string;
|
|
||||||
workflow: Workflow;
|
|
||||||
}) => {
|
|
||||||
const workflowCreateStepFromParentStepId = useRecoilValue(
|
|
||||||
workflowCreateStepFromParentStepIdState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const setShowPageWorkflowDiagramTriggerNodeSelection = useSetRecoilState(
|
|
||||||
showPageWorkflowDiagramTriggerNodeSelectionState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { createNode } = useCreateNode({ workflow });
|
|
||||||
|
|
||||||
const allOptions: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: 'standard' | 'custom';
|
|
||||||
icon: any;
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
id: 'create-record',
|
|
||||||
name: 'Create Record',
|
|
||||||
type: 'standard',
|
|
||||||
icon: IconPlus,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'find-records',
|
|
||||||
name: 'Find Records',
|
|
||||||
type: 'standard',
|
|
||||||
icon: IconSearch,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
id: 'all',
|
|
||||||
title: 'All',
|
|
||||||
Icon: IconSettingsAutomation,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'standard',
|
|
||||||
title: 'Standard',
|
|
||||||
Icon: IconPlaystationSquare,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'custom',
|
|
||||||
title: 'Custom',
|
|
||||||
Icon: IconPlug,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { activeTabIdState } = useTabList(tabListId);
|
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
|
||||||
|
|
||||||
const options = allOptions.filter(
|
|
||||||
(option) => activeTabId === 'all' || option.type === activeTabId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleActionClick = async (actionId: string) => {
|
|
||||||
if (workflowCreateStepFromParentStepId === undefined) {
|
|
||||||
throw new Error('Select a step to create a new step from first.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNodeId = v4();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FIXME: For now, the data of the node to create are mostly static.
|
|
||||||
*/
|
|
||||||
await createNode({
|
|
||||||
parentNodeId: workflowCreateStepFromParentStepId,
|
|
||||||
nodeToAdd: {
|
|
||||||
id: newNodeId,
|
|
||||||
name: actionId,
|
|
||||||
type: 'CODE_ACTION',
|
|
||||||
valid: true,
|
|
||||||
settings: {
|
|
||||||
serverlessFunctionId: '111',
|
|
||||||
errorHandlingOptions: {
|
|
||||||
continueOnFailure: {
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
retryOnFailure: {
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setShowPageWorkflowDiagramTriggerNodeSelection(newNodeId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
tabs,
|
|
||||||
options,
|
|
||||||
handleActionClick,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
import {
|
||||||
|
WorkflowStep,
|
||||||
|
WorkflowVersion,
|
||||||
|
WorkflowWithCurrentVersion,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
import { replaceStep } from '@/workflow/utils/replaceStep';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const useUpdateWorkflowVersionStep = ({
|
||||||
|
workflow,
|
||||||
|
stepId,
|
||||||
|
}: {
|
||||||
|
workflow: WorkflowWithCurrentVersion;
|
||||||
|
stepId: string;
|
||||||
|
}) => {
|
||||||
|
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||||
|
useUpdateOneRecord<WorkflowVersion>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateStep = async (updatedStep: WorkflowStep) => {
|
||||||
|
if (!isDefined(workflow.currentVersion)) {
|
||||||
|
throw new Error('Can not update an undefined workflow version.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateOneWorkflowVersion({
|
||||||
|
idToUpdate: workflow.currentVersion.id,
|
||||||
|
updateOneRecordInput: {
|
||||||
|
steps: replaceStep({
|
||||||
|
steps: workflow.currentVersion.steps ?? [],
|
||||||
|
stepId,
|
||||||
|
stepToReplace: updatedStep,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateStep,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
import {
|
||||||
|
WorkflowTrigger,
|
||||||
|
WorkflowVersion,
|
||||||
|
WorkflowWithCurrentVersion,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const useUpdateWorkflowVersionTrigger = ({
|
||||||
|
workflow,
|
||||||
|
}: {
|
||||||
|
workflow: WorkflowWithCurrentVersion;
|
||||||
|
}) => {
|
||||||
|
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||||
|
useUpdateOneRecord<WorkflowVersion>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTrigger = async (updatedTrigger: WorkflowTrigger) => {
|
||||||
|
if (!isDefined(workflow.currentVersion)) {
|
||||||
|
throw new Error('Can not update an undefined workflow version.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateOneWorkflowVersion({
|
||||||
|
idToUpdate: workflow.currentVersion.id,
|
||||||
|
updateOneRecordInput: {
|
||||||
|
trigger: updatedTrigger,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateTrigger,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
|
import {
|
||||||
|
Workflow,
|
||||||
|
WorkflowVersion,
|
||||||
|
WorkflowWithCurrentVersion,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const useWorkflowWithCurrentVersion = (
|
||||||
|
workflowId: string | undefined,
|
||||||
|
): WorkflowWithCurrentVersion | undefined => {
|
||||||
|
const { record: workflow } = useFindOneRecord<Workflow>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||||
|
objectRecordId: workflowId,
|
||||||
|
recordGqlFields: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
statuses: true,
|
||||||
|
},
|
||||||
|
skip: !isDefined(workflowId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { records: mostRecentWorkflowVersions } =
|
||||||
|
useFindManyRecords<WorkflowVersion>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||||
|
filter: {
|
||||||
|
workflowId: {
|
||||||
|
eq: workflowId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
createdAt: 'DescNullsLast',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
limit: 1,
|
||||||
|
skip: !isDefined(workflowId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDefined(workflow)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = mostRecentWorkflowVersions?.[0];
|
||||||
|
if (!isDefined(currentVersion)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...workflow,
|
||||||
|
currentVersion,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const showPageWorkflowDiagramTriggerNodeSelectionState = createState<
|
|
||||||
string | undefined
|
|
||||||
>({
|
|
||||||
key: 'showPageWorkflowDiagramTriggerNodeSelectionState',
|
|
||||||
defaultValue: undefined,
|
|
||||||
});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { ApolloError } from '@apollo/client';
|
|
||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const showPageWorkflowErrorState = createState<ApolloError | undefined>({
|
|
||||||
key: 'showPageWorkflowErrorState',
|
|
||||||
defaultValue: undefined,
|
|
||||||
});
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const showPageWorkflowIdState = createState<string | undefined>({
|
|
||||||
key: 'showPageWorkflowIdState',
|
|
||||||
defaultValue: undefined,
|
|
||||||
});
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const showPageWorkflowLoadingState = createState<boolean>({
|
|
||||||
key: 'showPageWorkflowLoadingState',
|
|
||||||
defaultValue: true,
|
|
||||||
});
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const showPageWorkflowSelectedNodeState = createState<
|
|
||||||
string | undefined
|
|
||||||
>({
|
|
||||||
key: 'showPageWorkflowSelectedNodeState',
|
|
||||||
defaultValue: undefined,
|
|
||||||
});
|
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
|
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
|
||||||
import { createState } from 'twenty-ui';
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
export const showPageWorkflowDiagramState = createState<
|
export const workflowDiagramState = createState<WorkflowDiagram | undefined>({
|
||||||
WorkflowDiagram | undefined
|
key: 'workflowDiagramState',
|
||||||
>({
|
|
||||||
key: 'showPageWorkflowDiagramState',
|
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
});
|
});
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const workflowDiagramTriggerNodeSelectionState = createState<
|
||||||
|
string | undefined
|
||||||
|
>({
|
||||||
|
key: 'workflowDiagramTriggerNodeSelectionState',
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const workflowIdState = createState<string | undefined>({
|
||||||
|
key: 'workflowIdState',
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const workflowSelectedNodeState = createState<string | undefined>({
|
||||||
|
key: 'workflowSelectedNodeState',
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
@ -30,6 +30,8 @@ export type WorkflowAction = WorkflowCodeAction;
|
|||||||
|
|
||||||
export type WorkflowStep = WorkflowAction;
|
export type WorkflowStep = WorkflowAction;
|
||||||
|
|
||||||
|
export type WorkflowStepType = WorkflowStep['type'];
|
||||||
|
|
||||||
export type WorkflowTriggerType = 'DATABASE_EVENT';
|
export type WorkflowTriggerType = 'DATABASE_EVENT';
|
||||||
|
|
||||||
type BaseTrigger = {
|
type BaseTrigger = {
|
||||||
@ -46,14 +48,23 @@ export type WorkflowDatabaseEventTrigger = BaseTrigger & {
|
|||||||
|
|
||||||
export type WorkflowTrigger = WorkflowDatabaseEventTrigger;
|
export type WorkflowTrigger = WorkflowDatabaseEventTrigger;
|
||||||
|
|
||||||
|
export type WorkflowStatus = 'DRAFT' | 'ACTIVE' | 'DEACTIVATED';
|
||||||
|
|
||||||
|
export type WorkflowVersionStatus =
|
||||||
|
| 'DRAFT'
|
||||||
|
| 'ACTIVE'
|
||||||
|
| 'DEACTIVATED'
|
||||||
|
| 'ARCHIVED';
|
||||||
|
|
||||||
export type WorkflowVersion = {
|
export type WorkflowVersion = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
trigger: WorkflowTrigger;
|
trigger: WorkflowTrigger | null;
|
||||||
steps: Array<WorkflowStep>;
|
steps: Array<WorkflowStep> | null;
|
||||||
|
status: WorkflowVersionStatus;
|
||||||
__typename: 'WorkflowVersion';
|
__typename: 'WorkflowVersion';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,5 +73,10 @@ export type Workflow = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
versions: Array<WorkflowVersion>;
|
versions: Array<WorkflowVersion>;
|
||||||
publishedVersionId: string;
|
lastPublishedVersionId: string;
|
||||||
|
statuses: Array<WorkflowStatus> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowWithCurrentVersion = Workflow & {
|
||||||
|
currentVersion: WorkflowVersion | undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,7 +18,7 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
|
|
||||||
expect(result.nodes[0]).toMatchObject({
|
expect(result.nodes[0]).toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
label: trigger.settings.eventName,
|
label: 'Company is Created',
|
||||||
nodeType: 'trigger',
|
nodeType: 'trigger',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
import { Workflow } from '@/workflow/types/Workflow';
|
|
||||||
import { getWorkflowLastDiagramVersion } from '../getWorkflowLastDiagramVersion';
|
|
||||||
|
|
||||||
describe('getWorkflowLastDiagramVersion', () => {
|
|
||||||
it('returns an empty diagram if the provided workflow is undefined', () => {
|
|
||||||
const result = getWorkflowLastDiagramVersion(undefined);
|
|
||||||
|
|
||||||
expect(result).toEqual({ nodes: [], edges: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an empty diagram if the provided workflow has no versions', () => {
|
|
||||||
const result = getWorkflowLastDiagramVersion({
|
|
||||||
__typename: 'Workflow',
|
|
||||||
id: 'aa',
|
|
||||||
name: 'aa',
|
|
||||||
publishedVersionId: '',
|
|
||||||
versions: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual({ nodes: [], edges: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the diagram for the last version', () => {
|
|
||||||
const workflow: Workflow = {
|
|
||||||
__typename: 'Workflow',
|
|
||||||
id: 'aa',
|
|
||||||
name: 'aa',
|
|
||||||
publishedVersionId: '',
|
|
||||||
versions: [
|
|
||||||
{
|
|
||||||
__typename: 'WorkflowVersion',
|
|
||||||
createdAt: '',
|
|
||||||
id: '1',
|
|
||||||
name: '',
|
|
||||||
steps: [],
|
|
||||||
trigger: {
|
|
||||||
settings: { eventName: 'company.created' },
|
|
||||||
type: 'DATABASE_EVENT',
|
|
||||||
},
|
|
||||||
updatedAt: '',
|
|
||||||
workflowId: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
__typename: 'WorkflowVersion',
|
|
||||||
createdAt: '',
|
|
||||||
id: '1',
|
|
||||||
name: '',
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
id: 'step-1',
|
|
||||||
name: '',
|
|
||||||
settings: {
|
|
||||||
errorHandlingOptions: {
|
|
||||||
retryOnFailure: { value: true },
|
|
||||||
continueOnFailure: { value: false },
|
|
||||||
},
|
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
|
||||||
},
|
|
||||||
type: 'CODE_ACTION',
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
trigger: {
|
|
||||||
settings: { eventName: 'company.created' },
|
|
||||||
type: 'DATABASE_EVENT',
|
|
||||||
},
|
|
||||||
updatedAt: '',
|
|
||||||
workflowId: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = getWorkflowLastDiagramVersion(workflow);
|
|
||||||
|
|
||||||
// Corresponds to the trigger + 1 step
|
|
||||||
expect(result.nodes).toHaveLength(2);
|
|
||||||
expect(result.edges).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
import { getWorkflowVersionDiagram } from '../getWorkflowVersionDiagram';
|
||||||
|
|
||||||
|
describe('getWorkflowVersionDiagram', () => {
|
||||||
|
it('returns an empty diagram if the provided workflow version', () => {
|
||||||
|
const result = getWorkflowVersionDiagram(undefined);
|
||||||
|
|
||||||
|
expect(result).toEqual({ nodes: [], edges: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty diagram if the provided workflow version has no trigger', () => {
|
||||||
|
const result = getWorkflowVersionDiagram({
|
||||||
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
createdAt: '',
|
||||||
|
id: '1',
|
||||||
|
name: '',
|
||||||
|
steps: [],
|
||||||
|
trigger: null,
|
||||||
|
updatedAt: '',
|
||||||
|
workflowId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ nodes: [], edges: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty diagram if the provided workflow version has no steps', () => {
|
||||||
|
const result = getWorkflowVersionDiagram({
|
||||||
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
createdAt: '',
|
||||||
|
id: '1',
|
||||||
|
name: '',
|
||||||
|
steps: null,
|
||||||
|
trigger: {
|
||||||
|
settings: { eventName: 'company.created' },
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
},
|
||||||
|
updatedAt: '',
|
||||||
|
workflowId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ nodes: [], edges: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the diagram for the last version', () => {
|
||||||
|
const result = getWorkflowVersionDiagram({
|
||||||
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
createdAt: '',
|
||||||
|
id: '1',
|
||||||
|
name: '',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: 'step-1',
|
||||||
|
name: '',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
},
|
||||||
|
type: 'CODE_ACTION',
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
settings: { eventName: 'company.created' },
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
},
|
||||||
|
updatedAt: '',
|
||||||
|
workflowId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Corresponds to the trigger + 1 step
|
||||||
|
expect(result.nodes).toHaveLength(2);
|
||||||
|
expect(result.edges).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,8 +3,9 @@ import { insertStep } from '../insertStep';
|
|||||||
|
|
||||||
describe('insertStep', () => {
|
describe('insertStep', () => {
|
||||||
it('returns a deep copy of the provided steps array instead of mutating it', () => {
|
it('returns a deep copy of the provided steps array instead of mutating it', () => {
|
||||||
const workflowVersionInitial: WorkflowVersion = {
|
const workflowVersionInitial = {
|
||||||
__typename: 'WorkflowVersion',
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '',
|
name: '',
|
||||||
@ -15,7 +16,7 @@ describe('insertStep', () => {
|
|||||||
},
|
},
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
workflowId: '',
|
workflowId: '',
|
||||||
};
|
} satisfies WorkflowVersion;
|
||||||
const stepToAdd: WorkflowStep = {
|
const stepToAdd: WorkflowStep = {
|
||||||
id: 'step-1',
|
id: 'step-1',
|
||||||
name: '',
|
name: '',
|
||||||
@ -40,8 +41,9 @@ describe('insertStep', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds the step when the steps array is empty', () => {
|
it('adds the step when the steps array is empty', () => {
|
||||||
const workflowVersionInitial: WorkflowVersion = {
|
const workflowVersionInitial = {
|
||||||
__typename: 'WorkflowVersion',
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '',
|
name: '',
|
||||||
@ -52,7 +54,7 @@ describe('insertStep', () => {
|
|||||||
},
|
},
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
workflowId: '',
|
workflowId: '',
|
||||||
};
|
} satisfies WorkflowVersion;
|
||||||
const stepToAdd: WorkflowStep = {
|
const stepToAdd: WorkflowStep = {
|
||||||
id: 'step-1',
|
id: 'step-1',
|
||||||
name: '',
|
name: '',
|
||||||
@ -78,8 +80,9 @@ describe('insertStep', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds the step at the end of a non-empty steps array', () => {
|
it('adds the step at the end of a non-empty steps array', () => {
|
||||||
const workflowVersionInitial: WorkflowVersion = {
|
const workflowVersionInitial = {
|
||||||
__typename: 'WorkflowVersion',
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '',
|
name: '',
|
||||||
@ -117,7 +120,7 @@ describe('insertStep', () => {
|
|||||||
},
|
},
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
workflowId: '',
|
workflowId: '',
|
||||||
};
|
} satisfies WorkflowVersion;
|
||||||
const stepToAdd: WorkflowStep = {
|
const stepToAdd: WorkflowStep = {
|
||||||
id: 'step-3',
|
id: 'step-3',
|
||||||
name: '',
|
name: '',
|
||||||
@ -147,8 +150,9 @@ describe('insertStep', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds the step in the middle of a non-empty steps array', () => {
|
it('adds the step in the middle of a non-empty steps array', () => {
|
||||||
const workflowVersionInitial: WorkflowVersion = {
|
const workflowVersionInitial = {
|
||||||
__typename: 'WorkflowVersion',
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '',
|
name: '',
|
||||||
@ -186,7 +190,7 @@ describe('insertStep', () => {
|
|||||||
},
|
},
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
workflowId: '',
|
workflowId: '',
|
||||||
};
|
} satisfies WorkflowVersion;
|
||||||
const stepToAdd: WorkflowStep = {
|
const stepToAdd: WorkflowStep = {
|
||||||
id: 'step-3',
|
id: 'step-3',
|
||||||
name: '',
|
name: '',
|
||||||
|
|||||||
@ -0,0 +1,127 @@
|
|||||||
|
import { WorkflowStep, WorkflowVersion } from '@/workflow/types/Workflow';
|
||||||
|
import { replaceStep } from '../replaceStep';
|
||||||
|
|
||||||
|
describe('replaceStep', () => {
|
||||||
|
it('returns a deep copy of the provided steps array instead of mutating it', () => {
|
||||||
|
const stepToBeReplaced = {
|
||||||
|
id: 'step-1',
|
||||||
|
name: '',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
serverlessFunctionId: 'first',
|
||||||
|
},
|
||||||
|
type: 'CODE_ACTION',
|
||||||
|
valid: true,
|
||||||
|
} satisfies WorkflowStep;
|
||||||
|
const workflowVersionInitial = {
|
||||||
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
createdAt: '',
|
||||||
|
id: '1',
|
||||||
|
name: '',
|
||||||
|
steps: [stepToBeReplaced],
|
||||||
|
trigger: {
|
||||||
|
settings: { eventName: 'company.created' },
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
},
|
||||||
|
updatedAt: '',
|
||||||
|
workflowId: '',
|
||||||
|
} satisfies WorkflowVersion;
|
||||||
|
|
||||||
|
const stepsUpdated = replaceStep({
|
||||||
|
steps: workflowVersionInitial.steps,
|
||||||
|
stepToReplace: {
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
serverlessFunctionId: 'second',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stepId: stepToBeReplaced.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(workflowVersionInitial.steps).not.toBe(stepsUpdated);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces a step in a non-empty steps array', () => {
|
||||||
|
const stepToBeReplaced: WorkflowStep = {
|
||||||
|
id: 'step-2',
|
||||||
|
name: '',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
},
|
||||||
|
type: 'CODE_ACTION',
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
const workflowVersionInitial = {
|
||||||
|
__typename: 'WorkflowVersion',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
createdAt: '',
|
||||||
|
id: '1',
|
||||||
|
name: '',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: 'step-1',
|
||||||
|
name: '',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
},
|
||||||
|
type: 'CODE_ACTION',
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
stepToBeReplaced,
|
||||||
|
{
|
||||||
|
id: 'step-3',
|
||||||
|
name: '',
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
},
|
||||||
|
type: 'CODE_ACTION',
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
settings: { eventName: 'company.created' },
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
},
|
||||||
|
updatedAt: '',
|
||||||
|
workflowId: '',
|
||||||
|
} satisfies WorkflowVersion;
|
||||||
|
|
||||||
|
const updatedStepName = "that's another name";
|
||||||
|
const stepsUpdated = replaceStep({
|
||||||
|
stepId: stepToBeReplaced.id,
|
||||||
|
steps: workflowVersionInitial.steps,
|
||||||
|
stepToReplace: {
|
||||||
|
name: updatedStepName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedUpdatedSteps: Array<WorkflowStep> = [
|
||||||
|
workflowVersionInitial.steps[0],
|
||||||
|
{
|
||||||
|
...stepToBeReplaced,
|
||||||
|
name: updatedStepName,
|
||||||
|
},
|
||||||
|
workflowVersionInitial.steps[2],
|
||||||
|
];
|
||||||
|
expect(stepsUpdated).toEqual(expectedUpdatedSteps);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
||||||
|
import { WorkflowStep } from '@/workflow/types/Workflow';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns the reference of the array where the step should be positioned
|
||||||
|
* and at which index.
|
||||||
|
*/
|
||||||
|
export const findStepPositionOrThrow = ({
|
||||||
|
steps,
|
||||||
|
stepId,
|
||||||
|
}: {
|
||||||
|
steps: Array<WorkflowStep>;
|
||||||
|
stepId: string | undefined;
|
||||||
|
}): { steps: Array<WorkflowStep>; index: number } => {
|
||||||
|
if (!isDefined(stepId) || stepId === TRIGGER_STEP_ID) {
|
||||||
|
return {
|
||||||
|
steps,
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, step] of steps.entries()) {
|
||||||
|
if (step.id === stepId) {
|
||||||
|
return {
|
||||||
|
steps,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: When condition will have been implemented, put recursivity here.
|
||||||
|
// if (step.type === "CONDITION") {
|
||||||
|
// return findNodePosition({
|
||||||
|
// workflowSteps: step.conditions,
|
||||||
|
// stepId,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Couldn't locate the step. Unreachable step id: ${stepId}.`);
|
||||||
|
};
|
||||||
@ -1,11 +1,14 @@
|
|||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
||||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||||
import {
|
import {
|
||||||
WorkflowDiagram,
|
WorkflowDiagram,
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
WorkflowDiagramNode,
|
WorkflowDiagramNode,
|
||||||
} from '@/workflow/types/WorkflowDiagram';
|
} from '@/workflow/types/WorkflowDiagram';
|
||||||
|
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||||
import { MarkerType } from '@xyflow/react';
|
import { MarkerType } from '@xyflow/react';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
export const generateWorkflowDiagram = ({
|
export const generateWorkflowDiagram = ({
|
||||||
trigger,
|
trigger,
|
||||||
@ -58,12 +61,15 @@ export const generateWorkflowDiagram = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Start with the trigger node
|
// Start with the trigger node
|
||||||
const triggerNodeId = 'trigger';
|
const triggerNodeId = TRIGGER_STEP_ID;
|
||||||
|
const triggerEvent = splitWorkflowTriggerEventName(
|
||||||
|
trigger.settings.eventName,
|
||||||
|
);
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: triggerNodeId,
|
id: triggerNodeId,
|
||||||
data: {
|
data: {
|
||||||
nodeType: 'trigger',
|
nodeType: 'trigger',
|
||||||
label: trigger.settings.eventName,
|
label: `${capitalize(triggerEvent.objectType)} is ${capitalize(triggerEvent.event)}`,
|
||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { WorkflowStep, WorkflowStepType } from '@/workflow/types/Workflow';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const getStepDefaultDefinition = (
|
||||||
|
type: WorkflowStepType,
|
||||||
|
): WorkflowStep => {
|
||||||
|
const newStepId = v4();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'CODE_ACTION': {
|
||||||
|
return {
|
||||||
|
id: newStepId,
|
||||||
|
name: 'Code',
|
||||||
|
type: 'CODE_ACTION',
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
serverlessFunctionId: '',
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
retryOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unknown type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { Workflow } from '@/workflow/types/Workflow';
|
|
||||||
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
|
|
||||||
import { generateWorkflowDiagram } from '@/workflow/utils/generateWorkflowDiagram';
|
|
||||||
import { getWorkflowLastVersion } from '@/workflow/utils/getWorkflowLastVersion';
|
|
||||||
import { isDefined } from 'twenty-ui';
|
|
||||||
|
|
||||||
const EMPTY_DIAGRAM: WorkflowDiagram = {
|
|
||||||
nodes: [],
|
|
||||||
edges: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getWorkflowLastDiagramVersion = (
|
|
||||||
workflow: Workflow | undefined,
|
|
||||||
): WorkflowDiagram => {
|
|
||||||
if (!isDefined(workflow)) {
|
|
||||||
return EMPTY_DIAGRAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastVersion = getWorkflowLastVersion(workflow);
|
|
||||||
if (!isDefined(lastVersion) || !isDefined(lastVersion.trigger)) {
|
|
||||||
return EMPTY_DIAGRAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
return generateWorkflowDiagram({
|
|
||||||
trigger: lastVersion.trigger,
|
|
||||||
steps: lastVersion.steps,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow';
|
|
||||||
|
|
||||||
export const getWorkflowLastVersion = (
|
|
||||||
workflow: Workflow,
|
|
||||||
): WorkflowVersion | undefined => {
|
|
||||||
return workflow.versions
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1))
|
|
||||||
.at(-1);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||||
|
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
|
||||||
|
import { generateWorkflowDiagram } from '@/workflow/utils/generateWorkflowDiagram';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
const EMPTY_DIAGRAM: WorkflowDiagram = {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWorkflowVersionDiagram = (
|
||||||
|
workflowVersion: WorkflowVersion | undefined,
|
||||||
|
): WorkflowDiagram => {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
isDefined(workflowVersion) &&
|
||||||
|
isDefined(workflowVersion.trigger) &&
|
||||||
|
isDefined(workflowVersion.steps)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return EMPTY_DIAGRAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateWorkflowDiagram({
|
||||||
|
trigger: workflowVersion.trigger,
|
||||||
|
steps: workflowVersion.steps,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,38 +1,5 @@
|
|||||||
import { WorkflowStep } from '@/workflow/types/Workflow';
|
import { WorkflowStep } from '@/workflow/types/Workflow';
|
||||||
|
import { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow';
|
||||||
const findStepPositionOrThrow = ({
|
|
||||||
steps,
|
|
||||||
stepId,
|
|
||||||
}: {
|
|
||||||
steps: Array<WorkflowStep>;
|
|
||||||
stepId: string | undefined;
|
|
||||||
}): { steps: Array<WorkflowStep>; index: number } => {
|
|
||||||
if (stepId === undefined) {
|
|
||||||
return {
|
|
||||||
steps,
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [index, step] of steps.entries()) {
|
|
||||||
if (step.id === stepId) {
|
|
||||||
return {
|
|
||||||
steps,
|
|
||||||
index,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: When condition will have been implemented, put recursivity here.
|
|
||||||
// if (step.type === "CONDITION") {
|
|
||||||
// return findNodePosition({
|
|
||||||
// workflowSteps: step.conditions,
|
|
||||||
// stepId,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Couldn't locate the step. Unreachable step id: ${stepId}.`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const insertStep = ({
|
export const insertStep = ({
|
||||||
steps: stepsInitial,
|
steps: stepsInitial,
|
||||||
@ -43,11 +10,10 @@ export const insertStep = ({
|
|||||||
parentStepId: string | undefined;
|
parentStepId: string | undefined;
|
||||||
stepToAdd: WorkflowStep;
|
stepToAdd: WorkflowStep;
|
||||||
}): Array<WorkflowStep> => {
|
}): Array<WorkflowStep> => {
|
||||||
// Make a deep copy of the nested object to prevent unwanted side effects.
|
|
||||||
const steps = structuredClone(stepsInitial);
|
const steps = structuredClone(stepsInitial);
|
||||||
|
|
||||||
const parentStepPosition = findStepPositionOrThrow({
|
const parentStepPosition = findStepPositionOrThrow({
|
||||||
steps: steps,
|
steps,
|
||||||
stepId: parentStepId,
|
stepId: parentStepId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { WorkflowStep } from '@/workflow/types/Workflow';
|
||||||
|
import { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow';
|
||||||
|
|
||||||
|
export const replaceStep = ({
|
||||||
|
steps: stepsInitial,
|
||||||
|
stepId,
|
||||||
|
stepToReplace,
|
||||||
|
}: {
|
||||||
|
steps: Array<WorkflowStep>;
|
||||||
|
stepId: string;
|
||||||
|
stepToReplace: Partial<Omit<WorkflowStep, 'id'>>;
|
||||||
|
}) => {
|
||||||
|
const steps = structuredClone(stepsInitial);
|
||||||
|
|
||||||
|
const parentStepPosition = findStepPositionOrThrow({
|
||||||
|
steps,
|
||||||
|
stepId,
|
||||||
|
});
|
||||||
|
|
||||||
|
parentStepPosition.steps[parentStepPosition.index] = {
|
||||||
|
...parentStepPosition.steps[parentStepPosition.index],
|
||||||
|
...stepToReplace,
|
||||||
|
};
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
};
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
export const splitWorkflowTriggerEventName = (eventName: string) => {
|
||||||
|
const [objectType, event] = eventName.split('.');
|
||||||
|
|
||||||
|
return {
|
||||||
|
objectType,
|
||||||
|
event,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
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 { WorkflowShowPageDiagram } from '@/workflow/components/WorkflowShowPageDiagram';
|
import { WorkflowDiagramCanvas } from '@/workflow/components/WorkflowDiagramCanvas';
|
||||||
import { WorkflowShowPageEffect } from '@/workflow/components/WorkflowShowPageEffect';
|
import { WorkflowShowPageEffect } from '@/workflow/components/WorkflowShowPageEffect';
|
||||||
import { WorkflowShowPageHeader } from '@/workflow/components/WorkflowShowPageHeader';
|
import { WorkflowShowPageHeader } from '@/workflow/components/WorkflowShowPageHeader';
|
||||||
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
|
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@ -37,7 +37,7 @@ export const WorkflowShowPage = () => {
|
|||||||
|
|
||||||
const workflowName = 'Test Workflow';
|
const workflowName = 'Test Workflow';
|
||||||
|
|
||||||
const showPageWorkflowDiagram = useRecoilValue(showPageWorkflowDiagramState);
|
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
||||||
|
|
||||||
if (parameters.workflowId === undefined) {
|
if (parameters.workflowId === undefined) {
|
||||||
return null;
|
return null;
|
||||||
@ -54,8 +54,8 @@ export const WorkflowShowPage = () => {
|
|||||||
/>
|
/>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<StyledFlowContainer>
|
<StyledFlowContainer>
|
||||||
{showPageWorkflowDiagram === undefined ? null : (
|
{workflowDiagram === undefined ? null : (
|
||||||
<WorkflowShowPageDiagram diagram={showPageWorkflowDiagram} />
|
<WorkflowDiagramCanvas diagram={workflowDiagram} />
|
||||||
)}
|
)}
|
||||||
</StyledFlowContainer>
|
</StyledFlowContainer>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|||||||
@ -178,6 +178,7 @@ export {
|
|||||||
IconWand,
|
IconWand,
|
||||||
IconWorld,
|
IconWorld,
|
||||||
IconX,
|
IconX,
|
||||||
|
IconPlaylistAdd,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
export type { TablerIconsProps } from '@tabler/icons-react';
|
export type { TablerIconsProps } from '@tabler/icons-react';
|
||||||
|
|||||||
Reference in New Issue
Block a user