mirror of
https://github.com/twentyhq/twenty.git
synced 2024-10-04 04:47:38 +03:00
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:
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 { isDefined } from 'twenty-ui';
|
||||
|
||||
export const RightDrawerWorkflowEditStep = () => {
|
||||
const showPageWorkflowSelectedNode = useRecoilValue(
|
||||
showPageWorkflowSelectedNodeState,
|
||||
);
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
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 { showPageWorkflowIdState } from '@/workflow/states/showPageWorkflowIdState';
|
||||
import { Workflow } from '@/workflow/types/Workflow';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const RightDrawerWorkflowSelectAction = () => {
|
||||
const showPageWorkflowId = useRecoilValue(showPageWorkflowIdState);
|
||||
|
||||
const { record: workflow } = useFindOneRecord<Workflow>({
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
objectRecordId: showPageWorkflowId,
|
||||
recordGqlFields: {
|
||||
id: true,
|
||||
name: true,
|
||||
versions: true,
|
||||
publishedVersionId: true,
|
||||
},
|
||||
});
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
if (!isDefined(workflow)) {
|
||||
return null;
|
||||
|
@ -1,19 +1,9 @@
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useRightDrawerWorkflowSelectAction } from '@/workflow/hooks/useRightDrawerWorkflowSelectAction';
|
||||
import { Workflow } from '@/workflow/types/Workflow';
|
||||
import { ACTIONS } from '@/workflow/constants/Actions';
|
||||
import { useCreateStep } from '@/workflow/hooks/useCreateStep';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
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`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -24,33 +14,24 @@ const StyledActionListContainer = styled.div`
|
||||
padding-inline: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const TAB_LIST_COMPONENT_ID =
|
||||
'workflow-select-action-page-right-tab-list';
|
||||
|
||||
export const RightDrawerWorkflowSelectActionContent = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: Workflow;
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const tabListId = `${TAB_LIST_COMPONENT_ID}`;
|
||||
|
||||
const { tabs, options, handleActionClick } =
|
||||
useRightDrawerWorkflowSelectAction({ tabListId, workflow });
|
||||
const { createStep } = useCreateStep({
|
||||
workflow,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTabListContainer>
|
||||
<TabList loading={false} tabListId={tabListId} tabs={tabs} />
|
||||
</StyledTabListContainer>
|
||||
|
||||
<StyledActionListContainer>
|
||||
{options.map((option) => (
|
||||
{ACTIONS.map((action) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
LeftIcon={option.icon}
|
||||
text={option.name}
|
||||
LeftIcon={action.icon}
|
||||
text={action.label}
|
||||
onClick={() => {
|
||||
handleActionClick(option.id);
|
||||
return createStep(action.type);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { WorkflowShowPageDiagramCreateStepNode } from '@/workflow/components/WorkflowShowPageDiagramCreateStepNode';
|
||||
import { WorkflowShowPageDiagramEffect } from '@/workflow/components/WorkflowShowPageDiagramEffect';
|
||||
import { WorkflowShowPageDiagramStepNode } from '@/workflow/components/WorkflowShowPageDiagramStepNode';
|
||||
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
|
||||
import { WorkflowDiagramCanvasEffect } from '@/workflow/components/WorkflowDiagramCanvasEffect';
|
||||
import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode';
|
||||
import { WorkflowDiagramStepNode } from '@/workflow/components/WorkflowDiagramStepNode';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import {
|
||||
WorkflowDiagram,
|
||||
WorkflowDiagramEdge,
|
||||
@ -21,7 +21,7 @@ import { useMemo } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { GRAY_SCALE, isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowShowPageDiagram = ({
|
||||
export const WorkflowDiagramCanvas = ({
|
||||
diagram,
|
||||
}: {
|
||||
diagram: WorkflowDiagram;
|
||||
@ -31,14 +31,12 @@ export const WorkflowShowPageDiagram = ({
|
||||
[diagram],
|
||||
);
|
||||
|
||||
const setShowPageWorkflowDiagram = useSetRecoilState(
|
||||
showPageWorkflowDiagramState,
|
||||
);
|
||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||
|
||||
const handleNodesChange = (
|
||||
nodeChanges: Array<NodeChange<WorkflowDiagramNode>>,
|
||||
) => {
|
||||
setShowPageWorkflowDiagram((diagram) => {
|
||||
setWorkflowDiagram((diagram) => {
|
||||
if (isDefined(diagram) === false) {
|
||||
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.',
|
||||
@ -55,7 +53,7 @@ export const WorkflowShowPageDiagram = ({
|
||||
const handleEdgesChange = (
|
||||
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
||||
) => {
|
||||
setShowPageWorkflowDiagram((diagram) => {
|
||||
setWorkflowDiagram((diagram) => {
|
||||
if (isDefined(diagram) === false) {
|
||||
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.',
|
||||
@ -72,8 +70,8 @@ export const WorkflowShowPageDiagram = ({
|
||||
return (
|
||||
<ReactFlow
|
||||
nodeTypes={{
|
||||
default: WorkflowShowPageDiagramStepNode,
|
||||
'create-step': WorkflowShowPageDiagramCreateStepNode,
|
||||
default: WorkflowDiagramStepNode,
|
||||
'create-step': WorkflowDiagramCreateStepNode,
|
||||
}}
|
||||
fitView
|
||||
nodes={nodes.map((node) => ({ ...node, draggable: false }))}
|
||||
@ -81,7 +79,7 @@ export const WorkflowShowPageDiagram = ({
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
>
|
||||
<WorkflowShowPageDiagramEffect />
|
||||
<WorkflowDiagramCanvasEffect />
|
||||
|
||||
<Background color={GRAY_SCALE.gray25} size={2} />
|
||||
</ReactFlow>
|
@ -1,8 +1,8 @@
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation';
|
||||
import { showPageWorkflowDiagramTriggerNodeSelectionState } from '@/workflow/states/showPageWorkflowDiagramTriggerNodeSelectionState';
|
||||
import { showPageWorkflowSelectedNodeState } from '@/workflow/states/showPageWorkflowSelectedNodeState';
|
||||
import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import {
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramNode,
|
||||
@ -16,18 +16,16 @@ import { useCallback, useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowShowPageDiagramEffect = () => {
|
||||
export const WorkflowDiagramCanvasEffect = () => {
|
||||
const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>();
|
||||
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
|
||||
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
||||
const setShowPageWorkflowSelectedNode = useSetRecoilState(
|
||||
showPageWorkflowSelectedNodeState,
|
||||
);
|
||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||
|
||||
const showPageWorkflowDiagramTriggerNodeSelection = useRecoilValue(
|
||||
showPageWorkflowDiagramTriggerNodeSelectionState,
|
||||
const workflowDiagramTriggerNodeSelection = useRecoilValue(
|
||||
workflowDiagramTriggerNodeSelectionState,
|
||||
);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
@ -52,13 +50,13 @@ export const WorkflowShowPageDiagramEffect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setShowPageWorkflowSelectedNode(selectedNode.id);
|
||||
setWorkflowSelectedNode(selectedNode.id);
|
||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
||||
},
|
||||
[
|
||||
closeRightDrawer,
|
||||
openRightDrawer,
|
||||
setShowPageWorkflowSelectedNode,
|
||||
setWorkflowSelectedNode,
|
||||
startNodeCreation,
|
||||
],
|
||||
);
|
||||
@ -68,14 +66,14 @@ export const WorkflowShowPageDiagramEffect = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(showPageWorkflowDiagramTriggerNodeSelection)) {
|
||||
if (!isDefined(workflowDiagramTriggerNodeSelection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactflow.updateNode(showPageWorkflowDiagramTriggerNodeSelection, {
|
||||
reactflow.updateNode(workflowDiagramTriggerNodeSelection, {
|
||||
selected: true,
|
||||
});
|
||||
}, [reactflow, showPageWorkflowDiagramTriggerNodeSelection]);
|
||||
}, [reactflow, workflowDiagramTriggerNodeSelection]);
|
||||
|
||||
return null;
|
||||
};
|
@ -7,7 +7,7 @@ export const StyledTargetHandle = styled(Handle)`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
export const WorkflowShowPageDiagramCreateStepNode = () => {
|
||||
export const WorkflowDiagramCreateStepNode = () => {
|
||||
return (
|
||||
<>
|
||||
<StyledTargetHandle type="target" position={Position.Top} />
|
@ -64,7 +64,7 @@ export const StyledTargetHandle = styled(Handle)`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
export const WorkflowShowPageDiagramStepNode = ({
|
||||
export const WorkflowDiagramStepNode = ({
|
||||
data,
|
||||
}: {
|
||||
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 { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
|
||||
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 { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes';
|
||||
import { getWorkflowLastDiagramVersion } from '@/workflow/utils/getWorkflowLastDiagramVersion';
|
||||
import { getWorkflowVersionDiagram } from '@/workflow/utils/getWorkflowVersionDiagram';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
@ -18,50 +14,29 @@ type WorkflowShowPageEffectProps = {
|
||||
export const WorkflowShowPageEffect = ({
|
||||
workflowId,
|
||||
}: WorkflowShowPageEffectProps) => {
|
||||
const {
|
||||
record: workflow,
|
||||
loading,
|
||||
error,
|
||||
} = useFindOneRecord<Workflow>({
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
objectRecordId: workflowId,
|
||||
recordGqlFields: {
|
||||
id: true,
|
||||
name: true,
|
||||
versions: true,
|
||||
publishedVersionId: true,
|
||||
},
|
||||
});
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const setShowPageWorkflowId = useSetRecoilState(showPageWorkflowIdState);
|
||||
const setCurrentWorkflowData = useSetRecoilState(
|
||||
showPageWorkflowDiagramState,
|
||||
);
|
||||
const setCurrentWorkflowLoading = useSetRecoilState(
|
||||
showPageWorkflowLoadingState,
|
||||
);
|
||||
const setCurrentWorkflowError = useSetRecoilState(showPageWorkflowErrorState);
|
||||
const setWorkflowId = useSetRecoilState(workflowIdState);
|
||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||
|
||||
useEffect(() => {
|
||||
setShowPageWorkflowId(workflowId);
|
||||
}, [setShowPageWorkflowId, workflowId]);
|
||||
setWorkflowId(workflowId);
|
||||
}, [setWorkflowId, workflowId]);
|
||||
|
||||
useEffect(() => {
|
||||
const flowLastVersion = getWorkflowLastDiagramVersion(workflow);
|
||||
const flowWithCreateStepNodes = addCreateStepNodes(flowLastVersion);
|
||||
const currentVersion = workflowWithCurrentVersion?.currentVersion;
|
||||
if (!isDefined(currentVersion)) {
|
||||
setWorkflowDiagram(undefined);
|
||||
|
||||
setCurrentWorkflowData(
|
||||
isDefined(workflow) ? flowWithCreateStepNodes : undefined,
|
||||
);
|
||||
}, [setCurrentWorkflowData, workflow]);
|
||||
return;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentWorkflowLoading(loading);
|
||||
}, [loading, setCurrentWorkflowLoading]);
|
||||
const lastWorkflowDiagram = getWorkflowVersionDiagram(currentVersion);
|
||||
const workflowDiagramWithCreateStepNodes =
|
||||
addCreateStepNodes(lastWorkflowDiagram);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentWorkflowError(error);
|
||||
}, [error, setCurrentWorkflowError]);
|
||||
setWorkflowDiagram(workflowDiagramWithCreateStepNodes);
|
||||
}, [setWorkflowDiagram, workflowWithCurrentVersion?.currentVersion]);
|
||||
|
||||
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 { createState } from 'twenty-ui';
|
||||
|
||||
export const showPageWorkflowDiagramState = createState<
|
||||
WorkflowDiagram | undefined
|
||||
>({
|
||||
key: 'showPageWorkflowDiagramState',
|
||||
export const workflowDiagramState = createState<WorkflowDiagram | undefined>({
|
||||
key: 'workflowDiagramState',
|
||||
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 WorkflowStepType = WorkflowStep['type'];
|
||||
|
||||
export type WorkflowTriggerType = 'DATABASE_EVENT';
|
||||
|
||||
type BaseTrigger = {
|
||||
@ -46,14 +48,23 @@ export type WorkflowDatabaseEventTrigger = BaseTrigger & {
|
||||
|
||||
export type WorkflowTrigger = WorkflowDatabaseEventTrigger;
|
||||
|
||||
export type WorkflowStatus = 'DRAFT' | 'ACTIVE' | 'DEACTIVATED';
|
||||
|
||||
export type WorkflowVersionStatus =
|
||||
| 'DRAFT'
|
||||
| 'ACTIVE'
|
||||
| 'DEACTIVATED'
|
||||
| 'ARCHIVED';
|
||||
|
||||
export type WorkflowVersion = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workflowId: string;
|
||||
trigger: WorkflowTrigger;
|
||||
steps: Array<WorkflowStep>;
|
||||
trigger: WorkflowTrigger | null;
|
||||
steps: Array<WorkflowStep> | null;
|
||||
status: WorkflowVersionStatus;
|
||||
__typename: 'WorkflowVersion';
|
||||
};
|
||||
|
||||
@ -62,5 +73,10 @@ export type Workflow = {
|
||||
id: string;
|
||||
name: string;
|
||||
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({
|
||||
data: {
|
||||
label: trigger.settings.eventName,
|
||||
label: 'Company is Created',
|
||||
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', () => {
|
||||
it('returns a deep copy of the provided steps array instead of mutating it', () => {
|
||||
const workflowVersionInitial: WorkflowVersion = {
|
||||
const workflowVersionInitial = {
|
||||
__typename: 'WorkflowVersion',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '',
|
||||
id: '1',
|
||||
name: '',
|
||||
@ -15,7 +16,7 @@ describe('insertStep', () => {
|
||||
},
|
||||
updatedAt: '',
|
||||
workflowId: '',
|
||||
};
|
||||
} satisfies WorkflowVersion;
|
||||
const stepToAdd: WorkflowStep = {
|
||||
id: 'step-1',
|
||||
name: '',
|
||||
@ -40,8 +41,9 @@ describe('insertStep', () => {
|
||||
});
|
||||
|
||||
it('adds the step when the steps array is empty', () => {
|
||||
const workflowVersionInitial: WorkflowVersion = {
|
||||
const workflowVersionInitial = {
|
||||
__typename: 'WorkflowVersion',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '',
|
||||
id: '1',
|
||||
name: '',
|
||||
@ -52,7 +54,7 @@ describe('insertStep', () => {
|
||||
},
|
||||
updatedAt: '',
|
||||
workflowId: '',
|
||||
};
|
||||
} satisfies WorkflowVersion;
|
||||
const stepToAdd: WorkflowStep = {
|
||||
id: 'step-1',
|
||||
name: '',
|
||||
@ -78,8 +80,9 @@ describe('insertStep', () => {
|
||||
});
|
||||
|
||||
it('adds the step at the end of a non-empty steps array', () => {
|
||||
const workflowVersionInitial: WorkflowVersion = {
|
||||
const workflowVersionInitial = {
|
||||
__typename: 'WorkflowVersion',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '',
|
||||
id: '1',
|
||||
name: '',
|
||||
@ -117,7 +120,7 @@ describe('insertStep', () => {
|
||||
},
|
||||
updatedAt: '',
|
||||
workflowId: '',
|
||||
};
|
||||
} satisfies WorkflowVersion;
|
||||
const stepToAdd: WorkflowStep = {
|
||||
id: 'step-3',
|
||||
name: '',
|
||||
@ -147,8 +150,9 @@ describe('insertStep', () => {
|
||||
});
|
||||
|
||||
it('adds the step in the middle of a non-empty steps array', () => {
|
||||
const workflowVersionInitial: WorkflowVersion = {
|
||||
const workflowVersionInitial = {
|
||||
__typename: 'WorkflowVersion',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '',
|
||||
id: '1',
|
||||
name: '',
|
||||
@ -186,7 +190,7 @@ describe('insertStep', () => {
|
||||
},
|
||||
updatedAt: '',
|
||||
workflowId: '',
|
||||
};
|
||||
} satisfies WorkflowVersion;
|
||||
const stepToAdd: WorkflowStep = {
|
||||
id: 'step-3',
|
||||
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 {
|
||||
WorkflowDiagram,
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramNode,
|
||||
} from '@/workflow/types/WorkflowDiagram';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { MarkerType } from '@xyflow/react';
|
||||
import { v4 } from 'uuid';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const generateWorkflowDiagram = ({
|
||||
trigger,
|
||||
@ -58,12 +61,15 @@ export const generateWorkflowDiagram = ({
|
||||
};
|
||||
|
||||
// Start with the trigger node
|
||||
const triggerNodeId = 'trigger';
|
||||
const triggerNodeId = TRIGGER_STEP_ID;
|
||||
const triggerEvent = splitWorkflowTriggerEventName(
|
||||
trigger.settings.eventName,
|
||||
);
|
||||
nodes.push({
|
||||
id: triggerNodeId,
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
label: trigger.settings.eventName,
|
||||
label: `${capitalize(triggerEvent.objectType)} is ${capitalize(triggerEvent.event)}`,
|
||||
},
|
||||
position: {
|
||||
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';
|
||||
|
||||
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}.`);
|
||||
};
|
||||
import { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow';
|
||||
|
||||
export const insertStep = ({
|
||||
steps: stepsInitial,
|
||||
@ -43,11 +10,10 @@ export const insertStep = ({
|
||||
parentStepId: string | undefined;
|
||||
stepToAdd: WorkflowStep;
|
||||
}): Array<WorkflowStep> => {
|
||||
// Make a deep copy of the nested object to prevent unwanted side effects.
|
||||
const steps = structuredClone(stepsInitial);
|
||||
|
||||
const parentStepPosition = findStepPositionOrThrow({
|
||||
steps: steps,
|
||||
steps,
|
||||
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 { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||
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 { WorkflowShowPageHeader } from '@/workflow/components/WorkflowShowPageHeader';
|
||||
import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import styled from '@emotion/styled';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useParams } from 'react-router-dom';
|
||||
@ -37,7 +37,7 @@ export const WorkflowShowPage = () => {
|
||||
|
||||
const workflowName = 'Test Workflow';
|
||||
|
||||
const showPageWorkflowDiagram = useRecoilValue(showPageWorkflowDiagramState);
|
||||
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
||||
|
||||
if (parameters.workflowId === undefined) {
|
||||
return null;
|
||||
@ -54,8 +54,8 @@ export const WorkflowShowPage = () => {
|
||||
/>
|
||||
<PageBody>
|
||||
<StyledFlowContainer>
|
||||
{showPageWorkflowDiagram === undefined ? null : (
|
||||
<WorkflowShowPageDiagram diagram={showPageWorkflowDiagram} />
|
||||
{workflowDiagram === undefined ? null : (
|
||||
<WorkflowDiagramCanvas diagram={workflowDiagram} />
|
||||
)}
|
||||
</StyledFlowContainer>
|
||||
</PageBody>
|
||||
|
@ -178,6 +178,7 @@ export {
|
||||
IconWand,
|
||||
IconWorld,
|
||||
IconX,
|
||||
IconPlaylistAdd,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
export type { TablerIconsProps } from '@tabler/icons-react';
|
||||
|
Loading…
Reference in New Issue
Block a user