Refacto workflow folders (#9302)
- Create separated folders for sections - Add components - Add utils and clean old ones - Add constants - Rename search variables folder and components Next steps: - clean hooks - clean states
This commit is contained in:
@ -1,16 +0,0 @@
|
||||
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 workflowId = useRecoilValue(workflowIdState);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
if (!isDefined(workflow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RightDrawerWorkflowEditStepContent workflow={workflow} />;
|
||||
};
|
||||
@ -1,34 +0,0 @@
|
||||
import { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail';
|
||||
import { useUpdateStep } from '@/workflow/hooks/useUpdateStep';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
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 } = useUpdateStep({
|
||||
workflow,
|
||||
});
|
||||
|
||||
return (
|
||||
<WorkflowStepDetail
|
||||
stepId={workflowSelectedNode}
|
||||
workflowVersion={workflow.currentVersion}
|
||||
onActionUpdate={updateStep}
|
||||
onTriggerUpdate={updateTrigger}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { RightDrawerWorkflowSelectActionContent } from '@/workflow/components/RightDrawerWorkflowSelectActionContent';
|
||||
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 workflowId = useRecoilValue(workflowIdState);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
if (!isDefined(workflow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RightDrawerWorkflowSelectActionContent workflow={workflow} />;
|
||||
};
|
||||
@ -1,40 +0,0 @@
|
||||
import { ACTIONS } from '@/workflow/constants/Actions';
|
||||
import { useCreateStep } from '@/workflow/hooks/useCreateStep';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import styled from '@emotion/styled';
|
||||
import { MenuItem } from 'twenty-ui';
|
||||
|
||||
const StyledActionListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
padding-block: ${({ theme }) => theme.spacing(1)};
|
||||
padding-inline: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const RightDrawerWorkflowSelectActionContent = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const { createStep } = useCreateStep({
|
||||
workflow,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledActionListContainer>
|
||||
{ACTIONS.map((action) => (
|
||||
<MenuItem
|
||||
key={action.type}
|
||||
LeftIcon={action.icon}
|
||||
text={action.label}
|
||||
onClick={() => {
|
||||
return createStep(action.type);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</StyledActionListContainer>
|
||||
);
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { RightDrawerWorkflowSelectTriggerTypeContent } from '@/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const RightDrawerWorkflowSelectTriggerType = () => {
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
if (!isDefined(workflow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RightDrawerWorkflowSelectTriggerTypeContent workflow={workflow} />;
|
||||
};
|
||||
@ -1,59 +0,0 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId';
|
||||
import { TRIGGER_TYPES } from '@/workflow/constants/TriggerTypes';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { getTriggerDefaultDefinition } from '@/workflow/utils/getTriggerDefaultDefinition';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { MenuItem } from 'twenty-ui';
|
||||
|
||||
const StyledActionListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
padding-block: ${({ theme }) => theme.spacing(1)};
|
||||
padding-inline: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const RightDrawerWorkflowSelectTriggerTypeContent = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow });
|
||||
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
|
||||
const { openRightDrawer } = useRightDrawer();
|
||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||
|
||||
return (
|
||||
<StyledActionListContainer>
|
||||
{TRIGGER_TYPES.map((action) => (
|
||||
<MenuItem
|
||||
key={action.type}
|
||||
LeftIcon={action.icon}
|
||||
text={action.name}
|
||||
onClick={async () => {
|
||||
await updateTrigger(
|
||||
getTriggerDefaultDefinition({
|
||||
type: action.type,
|
||||
activeObjectMetadataItems,
|
||||
}),
|
||||
);
|
||||
|
||||
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
||||
|
||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</StyledActionListContainer>
|
||||
);
|
||||
};
|
||||
@ -1,22 +0,0 @@
|
||||
import { RightDrawerWorkflowViewStepContent } from '@/workflow/components/RightDrawerWorkflowViewStepContent';
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const RightDrawerWorkflowViewStep = () => {
|
||||
const workflowVersionId = useRecoilValue(workflowVersionIdState);
|
||||
if (!isDefined(workflowVersionId)) {
|
||||
throw new Error('Expected a workflow version id');
|
||||
}
|
||||
|
||||
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RightDrawerWorkflowViewStepContent workflowVersion={workflowVersion} />
|
||||
);
|
||||
};
|
||||
@ -1,26 +0,0 @@
|
||||
import { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const RightDrawerWorkflowViewStepContent = ({
|
||||
workflowVersion,
|
||||
}: {
|
||||
workflowVersion: WorkflowVersion;
|
||||
}) => {
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
if (!isDefined(workflowSelectedNode)) {
|
||||
throw new Error(
|
||||
'Expected a node to be selected. Selecting a node is mandatory to edit it.',
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowStepDetail
|
||||
stepId={workflowSelectedNode}
|
||||
workflowVersion={workflowVersion}
|
||||
readonly
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,130 +0,0 @@
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||
import styled from '@emotion/styled';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import React from 'react';
|
||||
import { isDefined, OverflowingTextWithTooltip } from 'twenty-ui';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type Variant = 'placeholder';
|
||||
|
||||
const StyledStepNodeContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding-bottom: 12px;
|
||||
padding-top: 6px;
|
||||
`;
|
||||
|
||||
const StyledStepNodeType = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm}
|
||||
${({ theme }) => theme.border.radius.sm} 0 0;
|
||||
|
||||
color: ${({ theme }) => theme.color.gray50};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
|
||||
align-self: flex-start;
|
||||
|
||||
.selectable.selected &,
|
||||
.selectable:focus &,
|
||||
.selectable:focus-visible & {
|
||||
background-color: ${({ theme }) => theme.color.blue};
|
||||
color: ${({ theme }) => theme.font.color.inverted};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledStepNodeInnerContainer = styled.div<{ variant?: Variant }>`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-style: ${({ variant }) =>
|
||||
variant === 'placeholder' ? 'dashed' : null};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
position: relative;
|
||||
box-shadow: ${({ variant, theme }) =>
|
||||
variant === 'placeholder' ? 'none' : theme.boxShadow.superHeavy};
|
||||
|
||||
.selectable.selected &,
|
||||
.selectable:focus &,
|
||||
.selectable:focus-visible & {
|
||||
background-color: ${({ theme }) => theme.color.blue10};
|
||||
border-color: ${({ theme }) => theme.color.blue};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledStepNodeLabel = styled.div<{ variant?: Variant }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
column-gap: ${({ theme }) => theme.spacing(3)};
|
||||
color: ${({ variant, theme }) =>
|
||||
variant === 'placeholder'
|
||||
? theme.font.color.extraLight
|
||||
: theme.font.color.primary};
|
||||
max-width: 200px;
|
||||
`;
|
||||
|
||||
const StyledSourceHandle = styled(Handle)`
|
||||
background-color: ${({ theme }) => theme.color.gray50};
|
||||
`;
|
||||
|
||||
export const StyledTargetHandle = styled(Handle)`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
const StyledRightFloatingElementContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(-3)};
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
transform: translateX(100%);
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramBaseStepNode = ({
|
||||
nodeType,
|
||||
name,
|
||||
variant,
|
||||
Icon,
|
||||
RightFloatingElement,
|
||||
}: {
|
||||
nodeType: WorkflowDiagramStepNodeData['nodeType'];
|
||||
name: string;
|
||||
variant?: Variant;
|
||||
Icon?: React.ReactNode;
|
||||
RightFloatingElement?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<StyledStepNodeContainer>
|
||||
{nodeType !== 'trigger' ? (
|
||||
<StyledTargetHandle type="target" position={Position.Top} />
|
||||
) : null}
|
||||
|
||||
<StyledStepNodeType>{capitalize(nodeType)}</StyledStepNodeType>
|
||||
|
||||
<StyledStepNodeInnerContainer variant={variant}>
|
||||
<StyledStepNodeLabel variant={variant}>
|
||||
{Icon}
|
||||
|
||||
<OverflowingTextWithTooltip text={name} />
|
||||
</StyledStepNodeLabel>
|
||||
|
||||
{isDefined(RightFloatingElement) ? (
|
||||
<StyledRightFloatingElementContainer>
|
||||
{RightFloatingElement}
|
||||
</StyledRightFloatingElementContainer>
|
||||
) : null}
|
||||
</StyledStepNodeInnerContainer>
|
||||
|
||||
<StyledSourceHandle type="source" position={Position.Bottom} />
|
||||
</StyledStepNodeContainer>
|
||||
);
|
||||
};
|
||||
@ -1,232 +0,0 @@
|
||||
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
|
||||
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { WorkflowVersionStatusTag } from '@/workflow/components/WorkflowVersionStatusTag';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||
import {
|
||||
WorkflowDiagram,
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramNodeType,
|
||||
} from '@/workflow/types/WorkflowDiagram';
|
||||
import { getOrganizedDiagram } from '@/workflow/utils/getOrganizedDiagram';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
Background,
|
||||
EdgeChange,
|
||||
FitViewOptions,
|
||||
getNodesBounds,
|
||||
NodeChange,
|
||||
NodeProps,
|
||||
ReactFlow,
|
||||
useReactFlow,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { GRAY_SCALE, isDefined, THEME_COMMON } from 'twenty-ui';
|
||||
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
|
||||
import { workflowReactFlowRefState } from '@/workflow/states/workflowReactFlowRefState';
|
||||
|
||||
const StyledResetReactflowStyles = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
/* Below we reset the default styling of Reactflow */
|
||||
.react-flow__node-input,
|
||||
.react-flow__node-default,
|
||||
.react-flow__node-output,
|
||||
.react-flow__node-group {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
--xy-node-border-radius: none;
|
||||
--xy-node-border: none;
|
||||
--xy-node-background-color: none;
|
||||
--xy-node-boxshadow-hover: none;
|
||||
--xy-node-boxshadow-selected: none;
|
||||
`;
|
||||
|
||||
const StyledStatusTagContainer = styled.div`
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const defaultFitViewOptions = {
|
||||
minZoom: 1,
|
||||
maxZoom: 1,
|
||||
} satisfies FitViewOptions;
|
||||
|
||||
export const WorkflowDiagramCanvasBase = ({
|
||||
diagram,
|
||||
status,
|
||||
nodeTypes,
|
||||
children,
|
||||
}: {
|
||||
diagram: WorkflowDiagram;
|
||||
status: WorkflowVersionStatus;
|
||||
nodeTypes: Partial<
|
||||
Record<
|
||||
WorkflowDiagramNodeType,
|
||||
React.ComponentType<
|
||||
NodeProps & {
|
||||
data: any;
|
||||
type: any;
|
||||
}
|
||||
>
|
||||
>
|
||||
>;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const reactflow = useReactFlow();
|
||||
const setWorkflowReactFlowRefState = useSetRecoilState(
|
||||
workflowReactFlowRefState,
|
||||
);
|
||||
|
||||
const { nodes, edges } = useMemo(
|
||||
() => getOrganizedDiagram(diagram),
|
||||
[diagram],
|
||||
);
|
||||
|
||||
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
|
||||
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const rightDrawerState = !isRightDrawerOpen
|
||||
? 'closed'
|
||||
: isRightDrawerMinimized
|
||||
? 'minimized'
|
||||
: isMobile
|
||||
? 'fullScreen'
|
||||
: 'normal';
|
||||
|
||||
const rightDrawerWidth = Number(
|
||||
THEME_COMMON.rightDrawerWidth.replace('px', ''),
|
||||
);
|
||||
|
||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||
|
||||
const handleNodesChange = (
|
||||
nodeChanges: Array<NodeChange<WorkflowDiagramNode>>,
|
||||
) => {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
nodes: applyNodeChanges(nodeChanges, diagram.nodes),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdgesChange = (
|
||||
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
||||
) => {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
edges: applyEdgeChanges(edgeChanges, diagram.edges),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useListenRightDrawerClose(() => {
|
||||
reactflow.setNodes((nodes) =>
|
||||
nodes.map((node) => ({ ...node, selected: false })),
|
||||
);
|
||||
});
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(containerRef.current) || !reactflow.viewportInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentViewport = reactflow.getViewport();
|
||||
|
||||
const flowBounds = getNodesBounds(reactflow.getNodes());
|
||||
|
||||
let visibleRightDrawerWidth = 0;
|
||||
if (rightDrawerState === 'normal') {
|
||||
visibleRightDrawerWidth = rightDrawerWidth;
|
||||
}
|
||||
|
||||
const viewportX =
|
||||
(containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 -
|
||||
flowBounds.width / 2;
|
||||
|
||||
reactflow.setViewport(
|
||||
{
|
||||
...currentViewport,
|
||||
x: viewportX - visibleRightDrawerWidth,
|
||||
},
|
||||
{ duration: 300 },
|
||||
);
|
||||
}, [reactflow, rightDrawerState, rightDrawerWidth]);
|
||||
|
||||
return (
|
||||
<StyledResetReactflowStyles ref={containerRef}>
|
||||
<ReactFlow
|
||||
ref={(node) => {
|
||||
if (isDefined(node)) {
|
||||
setWorkflowReactFlowRefState({ current: node });
|
||||
}
|
||||
}}
|
||||
onInit={() => {
|
||||
if (!isDefined(containerRef.current)) {
|
||||
throw new Error('Expect the container ref to be defined');
|
||||
}
|
||||
|
||||
const flowBounds = getNodesBounds(reactflow.getNodes());
|
||||
|
||||
reactflow.setViewport({
|
||||
x: containerRef.current.offsetWidth / 2 - flowBounds.width / 2,
|
||||
y: 150,
|
||||
zoom: defaultFitViewOptions.maxZoom,
|
||||
});
|
||||
}}
|
||||
minZoom={defaultFitViewOptions.minZoom}
|
||||
maxZoom={defaultFitViewOptions.maxZoom}
|
||||
nodeTypes={nodeTypes}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
multiSelectionKeyCode={null}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
nodesDraggable={false}
|
||||
paneClickDistance={10} // Fix small unwanted user dragging does not select node
|
||||
>
|
||||
<Background color={GRAY_SCALE.gray25} size={2} />
|
||||
|
||||
{children}
|
||||
</ReactFlow>
|
||||
|
||||
<StyledStatusTagContainer>
|
||||
<WorkflowVersionStatusTag versionStatus={status} />
|
||||
</StyledStatusTagContainer>
|
||||
</StyledResetReactflowStyles>
|
||||
);
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
import { WorkflowDiagramCanvasBase } from '@/workflow/components/WorkflowDiagramCanvasBase';
|
||||
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/components/WorkflowDiagramCanvasEditableEffect';
|
||||
import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode';
|
||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger';
|
||||
import { WorkflowDiagramStepNodeEditable } from '@/workflow/components/WorkflowDiagramStepNodeEditable';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
|
||||
export const WorkflowDiagramCanvasEditable = ({
|
||||
diagram,
|
||||
workflowWithCurrentVersion,
|
||||
}: {
|
||||
diagram: WorkflowDiagram;
|
||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowDiagramCanvasBase
|
||||
diagram={diagram}
|
||||
status={workflowWithCurrentVersion.currentVersion.status}
|
||||
nodeTypes={{
|
||||
default: WorkflowDiagramStepNodeEditable,
|
||||
'create-step': WorkflowDiagramCreateStepNode,
|
||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||
}}
|
||||
>
|
||||
<WorkflowDiagramCanvasEditableEffect />
|
||||
</WorkflowDiagramCanvasBase>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
@ -1,73 +0,0 @@
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { CREATE_STEP_STEP_ID } from '@/workflow/constants/CreateStepStepId';
|
||||
import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/constants/EmptyTriggerStepId';
|
||||
import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation';
|
||||
import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowDiagramNode } from '@/workflow/types/WorkflowDiagram';
|
||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowDiagramCanvasEditableEffect = () => {
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
|
||||
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
({ nodes }: OnSelectionChangeParams) => {
|
||||
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
||||
const isClosingStep = isDefined(selectedNode) === false;
|
||||
|
||||
if (isClosingStep) {
|
||||
closeRightDrawer();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isEmptyTriggerNode = selectedNode.type === EMPTY_TRIGGER_STEP_ID;
|
||||
if (isEmptyTriggerNode) {
|
||||
openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isCreateStepNode = selectedNode.type === CREATE_STEP_STEP_ID;
|
||||
if (isCreateStepNode) {
|
||||
if (selectedNode.data.nodeType !== 'create-step') {
|
||||
throw new Error('Expected selected node to be a create step node.');
|
||||
}
|
||||
|
||||
startNodeCreation(selectedNode.data.parentNodeId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(selectedNode.id);
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
||||
},
|
||||
[
|
||||
setHotkeyScope,
|
||||
closeRightDrawer,
|
||||
openRightDrawer,
|
||||
setWorkflowSelectedNode,
|
||||
startNodeCreation,
|
||||
],
|
||||
);
|
||||
|
||||
useOnSelectionChange({
|
||||
onChange: handleSelectionChange,
|
||||
});
|
||||
|
||||
useTriggerNodeSelection();
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,30 +0,0 @@
|
||||
import { WorkflowDiagramCanvasBase } from '@/workflow/components/WorkflowDiagramCanvasBase';
|
||||
import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/components/WorkflowDiagramCanvasReadonlyEffect';
|
||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger';
|
||||
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/components/WorkflowDiagramStepNodeReadonly';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
|
||||
export const WorkflowDiagramCanvasReadonly = ({
|
||||
diagram,
|
||||
workflowVersion,
|
||||
}: {
|
||||
diagram: WorkflowDiagram;
|
||||
workflowVersion: WorkflowVersion;
|
||||
}) => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowDiagramCanvasBase
|
||||
diagram={diagram}
|
||||
status={workflowVersion.status}
|
||||
nodeTypes={{
|
||||
default: WorkflowDiagramStepNodeReadonly,
|
||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||
}}
|
||||
>
|
||||
<WorkflowDiagramCanvasReadonlyEffect />
|
||||
</WorkflowDiagramCanvasBase>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
@ -1,48 +0,0 @@
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
import { WorkflowDiagramNode } from '@/workflow/types/WorkflowDiagram';
|
||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowDiagramCanvasReadonlyEffect = () => {
|
||||
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
({ nodes }: OnSelectionChangeParams) => {
|
||||
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
||||
const isClosingStep = isDefined(selectedNode) === false;
|
||||
|
||||
if (isClosingStep) {
|
||||
closeRightDrawer();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(selectedNode.id);
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
openRightDrawer(RightDrawerPages.WorkflowStepView);
|
||||
},
|
||||
[
|
||||
closeRightDrawer,
|
||||
openRightDrawer,
|
||||
setWorkflowSelectedNode,
|
||||
setHotkeyScope,
|
||||
],
|
||||
);
|
||||
|
||||
useOnSelectionChange({
|
||||
onChange: handleSelectionChange,
|
||||
});
|
||||
|
||||
useTriggerNodeSelection();
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { IconButton, IconPlus } from 'twenty-ui';
|
||||
|
||||
export const StyledTargetHandle = styled(Handle)`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramCreateStepNode = () => {
|
||||
return (
|
||||
<>
|
||||
<StyledTargetHandle type="target" position={Position.Top} />
|
||||
|
||||
<IconButton Icon={IconPlus} size="medium" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,85 +0,0 @@
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import { workflowLastCreatedStepIdState } from '@/workflow/states/workflowLastCreatedStepIdState';
|
||||
import {
|
||||
WorkflowVersion,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
|
||||
import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes';
|
||||
import { getWorkflowVersionDiagram } from '@/workflow/utils/getWorkflowVersionDiagram';
|
||||
import { mergeWorkflowDiagrams } from '@/workflow/utils/mergeWorkflowDiagrams';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowDiagramEffect = ({
|
||||
workflowWithCurrentVersion,
|
||||
}: {
|
||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined;
|
||||
}) => {
|
||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||
|
||||
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
|
||||
({ snapshot, set }) => {
|
||||
return (currentVersion: WorkflowVersion) => {
|
||||
const previousWorkflowDiagram = getSnapshotValue(
|
||||
snapshot,
|
||||
workflowDiagramState,
|
||||
);
|
||||
|
||||
const nextWorkflowDiagram = addCreateStepNodes(
|
||||
getWorkflowVersionDiagram(currentVersion),
|
||||
);
|
||||
|
||||
let mergedWorkflowDiagram = nextWorkflowDiagram;
|
||||
if (isDefined(previousWorkflowDiagram)) {
|
||||
mergedWorkflowDiagram = mergeWorkflowDiagrams(
|
||||
previousWorkflowDiagram,
|
||||
nextWorkflowDiagram,
|
||||
);
|
||||
}
|
||||
|
||||
const lastCreatedStepId = getSnapshotValue(
|
||||
snapshot,
|
||||
workflowLastCreatedStepIdState,
|
||||
);
|
||||
if (isDefined(lastCreatedStepId)) {
|
||||
mergedWorkflowDiagram.nodes = mergedWorkflowDiagram.nodes.map(
|
||||
(node) => {
|
||||
if (node.id === lastCreatedStepId) {
|
||||
return {
|
||||
...node,
|
||||
selected: true,
|
||||
};
|
||||
}
|
||||
return node;
|
||||
},
|
||||
);
|
||||
|
||||
set(workflowLastCreatedStepIdState, undefined);
|
||||
}
|
||||
|
||||
set(workflowDiagramState, mergedWorkflowDiagram);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const currentVersion = workflowWithCurrentVersion?.currentVersion;
|
||||
if (!isDefined(currentVersion)) {
|
||||
setWorkflowDiagram(undefined);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
computeAndMergeNewWorkflowDiagram(currentVersion);
|
||||
}, [
|
||||
computeAndMergeNewWorkflowDiagram,
|
||||
setWorkflowDiagram,
|
||||
workflowWithCurrentVersion?.currentVersion,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,30 +0,0 @@
|
||||
import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconPlaylistAdd } from 'twenty-ui';
|
||||
|
||||
const StyledStepNodeLabelIconContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramEmptyTrigger = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<WorkflowDiagramBaseStepNode
|
||||
name="Add a Trigger"
|
||||
nodeType="trigger"
|
||||
variant="placeholder"
|
||||
Icon={
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<IconPlaylistAdd size={16} color={theme.font.color.tertiary} />
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,107 +0,0 @@
|
||||
import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
IconAddressBook,
|
||||
IconCode,
|
||||
IconHandMove,
|
||||
IconMail,
|
||||
IconPlaylistAdd,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const StyledStepNodeLabelIconContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramStepNodeBase = ({
|
||||
data,
|
||||
RightFloatingElement,
|
||||
}: {
|
||||
data: WorkflowDiagramStepNodeData;
|
||||
RightFloatingElement?: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const renderStepIcon = () => {
|
||||
switch (data.nodeType) {
|
||||
case 'trigger': {
|
||||
switch (data.triggerType) {
|
||||
case 'DATABASE_EVENT': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<IconPlaylistAdd
|
||||
size={theme.icon.size.lg}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
case 'MANUAL': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<IconHandMove
|
||||
size={theme.icon.size.lg}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(data.triggerType);
|
||||
}
|
||||
case 'action': {
|
||||
switch (data.actionType) {
|
||||
case 'CODE': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<IconCode
|
||||
size={theme.icon.size.lg}
|
||||
color={theme.color.orange}
|
||||
/>
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<IconMail size={theme.icon.size.lg} color={theme.color.blue} />
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
case 'CREATE_RECORD':
|
||||
case 'UPDATE_RECORD':
|
||||
case 'DELETE_RECORD': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<IconAddressBook
|
||||
size={theme.icon.size.lg}
|
||||
color={theme.font.color.tertiary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkflowDiagramBaseStepNode
|
||||
name={data.name}
|
||||
nodeType={data.nodeType}
|
||||
Icon={renderStepIcon()}
|
||||
RightFloatingElement={RightFloatingElement}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,44 +0,0 @@
|
||||
import { WorkflowDiagramStepNodeBase } from '@/workflow/components/WorkflowDiagramStepNodeBase';
|
||||
import { useDeleteStep } from '@/workflow/hooks/useDeleteStep';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FloatingIconButton, IconTrash } from 'twenty-ui';
|
||||
|
||||
export const WorkflowDiagramStepNodeEditable = ({
|
||||
id,
|
||||
data,
|
||||
selected,
|
||||
}: {
|
||||
id: string;
|
||||
data: WorkflowDiagramStepNodeData;
|
||||
selected?: boolean;
|
||||
}) => {
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
assertWorkflowWithCurrentVersionIsDefined(workflowWithCurrentVersion);
|
||||
|
||||
const { deleteStep } = useDeleteStep({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
});
|
||||
|
||||
return (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
data={data}
|
||||
RightFloatingElement={
|
||||
selected ? (
|
||||
<FloatingIconButton
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
onClick={() => {
|
||||
deleteStep(id);
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
import { WorkflowDiagramStepNodeBase } from '@/workflow/components/WorkflowDiagramStepNodeBase';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
|
||||
|
||||
export const WorkflowDiagramStepNodeReadonly = ({
|
||||
data,
|
||||
}: {
|
||||
data: WorkflowDiagramStepNodeData;
|
||||
}) => {
|
||||
return <WorkflowDiagramStepNodeBase data={data} />;
|
||||
};
|
||||
@ -1,151 +0,0 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/constants/ObjectEventTriggers';
|
||||
import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconPlaylistAdd, isDefined } from 'twenty-ui';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
|
||||
type WorkflowEditTriggerDatabaseEventFormProps = {
|
||||
trigger: WorkflowDatabaseEventTrigger;
|
||||
triggerOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
onTriggerUpdate?: undefined;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onTriggerUpdate: (trigger: WorkflowDatabaseEventTrigger) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
trigger,
|
||||
triggerOptions,
|
||||
}: WorkflowEditTriggerDatabaseEventFormProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
|
||||
const triggerEvent = isDefined(trigger)
|
||||
? splitWorkflowTriggerEventName(trigger.settings.eventName)
|
||||
: undefined;
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeObjectMetadataItems.map((item) => ({
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
const recordTypeMetadata = isDefined(triggerEvent)
|
||||
? activeObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === triggerEvent.objectType,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const selectedEvent = isDefined(triggerEvent)
|
||||
? OBJECT_EVENT_TRIGGERS.find(
|
||||
(availableEvent) => availableEvent.value === triggerEvent.event,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const headerTitle = isDefined(trigger.name)
|
||||
? trigger.name
|
||||
: isDefined(recordTypeMetadata) && isDefined(selectedEvent)
|
||||
? `When a ${recordTypeMetadata.labelSingular} is ${selectedEvent.label}`
|
||||
: '-';
|
||||
|
||||
const headerType = isDefined(selectedEvent)
|
||||
? `Trigger · Record is ${selectedEvent.label}`
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconPlaylistAdd}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-trigger-record-type"
|
||||
label="Record Type"
|
||||
fullWidth
|
||||
disabled={triggerOptions.readonly}
|
||||
value={triggerEvent?.objectType}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedRecordType) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate(
|
||||
isDefined(trigger) && isDefined(triggerEvent)
|
||||
? {
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
eventName: `${updatedRecordType}.${triggerEvent.event}`,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: headerTitle,
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`,
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
dropdownId="workflow-edit-trigger-event-type"
|
||||
label="Event type"
|
||||
fullWidth
|
||||
value={triggerEvent?.event}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={OBJECT_EVENT_TRIGGERS}
|
||||
disabled={triggerOptions.readonly}
|
||||
onChange={(updatedEvent) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate(
|
||||
isDefined(trigger) && isDefined(triggerEvent)
|
||||
? {
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: headerTitle,
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: `${availableMetadata?.[0].value}.${updatedEvent}`,
|
||||
outputSchema: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,117 +0,0 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { WorkflowStepHeader } from '@/workflow/components/WorkflowStepHeader';
|
||||
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/constants/ManualTriggerAvailabilityOptions';
|
||||
import {
|
||||
WorkflowManualTrigger,
|
||||
WorkflowManualTriggerAvailability,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/utils/getManualTriggerDefaultSettings';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconHandMove, isDefined, useIcons } from 'twenty-ui';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
|
||||
type WorkflowEditTriggerManualFormProps = {
|
||||
trigger: WorkflowManualTrigger;
|
||||
triggerOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
onTriggerUpdate?: undefined;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onTriggerUpdate: (trigger: WorkflowManualTrigger) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const WorkflowEditTriggerManualForm = ({
|
||||
trigger,
|
||||
triggerOptions,
|
||||
}: WorkflowEditTriggerManualFormProps) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeObjectMetadataItems.map((item) => ({
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
Icon: getIcon(item.icon),
|
||||
}));
|
||||
|
||||
const manualTriggerAvailability: WorkflowManualTriggerAvailability =
|
||||
isDefined(trigger.settings.objectType)
|
||||
? 'WHEN_RECORD_SELECTED'
|
||||
: 'EVERYWHERE';
|
||||
|
||||
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Manual Trigger';
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={IconHandMove}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Trigger · Manual"
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-manual-trigger-availability"
|
||||
label="Available"
|
||||
fullWidth
|
||||
disabled={triggerOptions.readonly}
|
||||
value={manualTriggerAvailability}
|
||||
options={MANUAL_TRIGGER_AVAILABILITY_OPTIONS}
|
||||
onChange={(updatedTriggerType) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: getManualTriggerDefaultSettings({
|
||||
availability: updatedTriggerType,
|
||||
activeObjectMetadataItems,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{manualTriggerAvailability === 'WHEN_RECORD_SELECTED' ? (
|
||||
<Select
|
||||
dropdownId="workflow-edit-manual-trigger-object"
|
||||
label="Object"
|
||||
fullWidth
|
||||
value={trigger.settings.objectType}
|
||||
options={availableMetadata}
|
||||
disabled={triggerOptions.readonly}
|
||||
onChange={(updatedObject) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
objectType: updatedObject,
|
||||
outputSchema: {},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
|
||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
|
||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowRunVersionVisualizer = ({
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import {
|
||||
RecordId,
|
||||
Variable,
|
||||
} from '@/workflow/components/WorkflowSingleRecordPicker';
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledRecordChip = styled(RecordChip)`
|
||||
margin: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledPlaceholder = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
margin: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type WorkflowSingleRecordFieldChipProps = {
|
||||
draftValue:
|
||||
| {
|
||||
type: 'static';
|
||||
value: RecordId;
|
||||
}
|
||||
| {
|
||||
type: 'variable';
|
||||
value: Variable;
|
||||
};
|
||||
selectedRecord?: ObjectRecord;
|
||||
objectNameSingular: string;
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
export const WorkflowSingleRecordFieldChip = ({
|
||||
draftValue,
|
||||
selectedRecord,
|
||||
objectNameSingular,
|
||||
onRemove,
|
||||
}: WorkflowSingleRecordFieldChipProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
if (
|
||||
!!draftValue &&
|
||||
draftValue.type === 'variable' &&
|
||||
isStandaloneVariableString(draftValue.value)
|
||||
) {
|
||||
return (
|
||||
<VariableChip
|
||||
rawVariableName={objectMetadataItem.labelSingular}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!!draftValue && draftValue.type === 'static' && !!selectedRecord) {
|
||||
return (
|
||||
<StyledRecordChip
|
||||
record={selectedRecord}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <StyledPlaceholder>Select a {objectNameSingular}</StyledPlaceholder>;
|
||||
};
|
||||
@ -1,199 +0,0 @@
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconForbid,
|
||||
isDefined,
|
||||
LightIconButton,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||
import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect';
|
||||
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { WorkflowSingleRecordFieldChip } from '@/workflow/components/WorkflowSingleRecordFieldChip';
|
||||
import { SearchVariablesDropdown } from '@/workflow/search-variables/components/SearchVariablesDropdown';
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback } from 'react';
|
||||
import { isValidUuid } from '~/utils/isValidUuid';
|
||||
|
||||
const StyledFormSelectContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border-right: none;
|
||||
border-bottom-right-radius: none;
|
||||
border-top-right-radius: none;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
overflow: 'hidden';
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledSearchVariablesDropdownContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
${({ theme }) => css`
|
||||
:hover {
|
||||
background-color: ${theme.background.transparent.light};
|
||||
}
|
||||
`}
|
||||
${({ theme }) => css`
|
||||
background-color: ${theme.background.transparent.lighter};
|
||||
border-top-right-radius: ${theme.border.radius.sm};
|
||||
border-bottom-right-radius: ${theme.border.radius.sm};
|
||||
border: 1px solid ${theme.border.color.medium};
|
||||
`}
|
||||
`;
|
||||
|
||||
export type RecordId = string;
|
||||
export type Variable = string;
|
||||
|
||||
type WorkflowSingleRecordPickerValue =
|
||||
| {
|
||||
type: 'static';
|
||||
value: RecordId;
|
||||
}
|
||||
| {
|
||||
type: 'variable';
|
||||
value: Variable;
|
||||
};
|
||||
|
||||
export type WorkflowSingleRecordPickerProps = {
|
||||
label?: string;
|
||||
defaultValue: RecordId | Variable;
|
||||
onChange: (value: RecordId | Variable) => void;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const WorkflowSingleRecordPicker = ({
|
||||
label,
|
||||
defaultValue,
|
||||
objectNameSingular,
|
||||
onChange,
|
||||
}: WorkflowSingleRecordPickerProps) => {
|
||||
const draftValue: WorkflowSingleRecordPickerValue =
|
||||
isStandaloneVariableString(defaultValue)
|
||||
? {
|
||||
type: 'variable',
|
||||
value: defaultValue,
|
||||
}
|
||||
: {
|
||||
type: 'static',
|
||||
value: defaultValue || '',
|
||||
};
|
||||
|
||||
const { record: selectedRecord } = useFindOneRecord({
|
||||
objectRecordId:
|
||||
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
|
||||
? defaultValue
|
||||
: '',
|
||||
objectNameSingular,
|
||||
withSoftDeleted: true,
|
||||
skip: !isValidUuid(defaultValue),
|
||||
});
|
||||
|
||||
const dropdownId = `workflow-record-picker-${objectNameSingular}`;
|
||||
const variablesDropdownId = `workflow-record-picker-${objectNameSingular}-variables`;
|
||||
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const { setRecordPickerSearchFilter } = useRecordPicker({
|
||||
recordPickerInstanceId: dropdownId,
|
||||
});
|
||||
|
||||
const handleCloseRelationPickerDropdown = useCallback(() => {
|
||||
setRecordPickerSearchFilter('');
|
||||
}, [setRecordPickerSearchFilter]);
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedEntity: RecordForSelect | null | undefined,
|
||||
) => {
|
||||
onChange?.(selectedEntity?.record?.id ?? '');
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleVariableTagInsert = (variable: string) => {
|
||||
onChange?.(variable);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleUnlinkVariable = () => {
|
||||
closeDropdown();
|
||||
|
||||
onChange('');
|
||||
};
|
||||
|
||||
return (
|
||||
<FormFieldInputContainer>
|
||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||
<FormFieldInputRowContainer>
|
||||
<StyledFormSelectContainer>
|
||||
<WorkflowSingleRecordFieldChip
|
||||
draftValue={draftValue}
|
||||
selectedRecord={selectedRecord}
|
||||
objectNameSingular={objectNameSingular}
|
||||
onRemove={handleUnlinkVariable}
|
||||
/>
|
||||
<DropdownScope dropdownScopeId={dropdownId}>
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="left-start"
|
||||
onClose={handleCloseRelationPickerDropdown}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
Icon={IconChevronDown}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: dropdownId }}
|
||||
>
|
||||
<SingleRecordSelect
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + objectNameSingular}
|
||||
onCancel={() => closeDropdown()}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordPickerInstanceId={dropdownId}
|
||||
selectedRecordIds={
|
||||
draftValue?.value &&
|
||||
!isStandaloneVariableString(draftValue.value)
|
||||
? [draftValue.value]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
/>
|
||||
</DropdownScope>
|
||||
</StyledFormSelectContainer>
|
||||
<StyledSearchVariablesDropdownContainer>
|
||||
<SearchVariablesDropdown
|
||||
inputId={variablesDropdownId}
|
||||
onVariableSelect={handleVariableTagInsert}
|
||||
disabled={false}
|
||||
objectNameSingularToSelect={objectNameSingular}
|
||||
/>
|
||||
</StyledSearchVariablesDropdownContainer>
|
||||
</FormFieldInputRowContainer>
|
||||
</FormFieldInputContainer>
|
||||
);
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledWorkflowStepBody = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
row-gap: ${({ theme }) => theme.spacing(6)};
|
||||
flex: 1 1 auto;
|
||||
`;
|
||||
|
||||
export { StyledWorkflowStepBody as WorkflowStepBody };
|
||||
@ -1,143 +0,0 @@
|
||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/components/WorkflowEditTriggerDatabaseEventForm';
|
||||
import { WorkflowEditTriggerManualForm } from '@/workflow/components/WorkflowEditTriggerManualForm';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowTrigger,
|
||||
WorkflowVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowEditActionFormCreateRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormCreateRecord';
|
||||
import { WorkflowEditActionFormDeleteRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormDeleteRecord';
|
||||
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-actions/components/WorkflowEditActionFormSendEmail';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { WorkflowEditActionFormUpdateRecord } from '@/workflow/workflow-actions/components/WorkflowEditActionFormUpdateRecord';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
|
||||
|
||||
const WorkflowEditActionFormServerlessFunction = lazy(() =>
|
||||
import(
|
||||
'@/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction'
|
||||
).then((module) => ({
|
||||
default: module.WorkflowEditActionFormServerlessFunction,
|
||||
})),
|
||||
);
|
||||
|
||||
type WorkflowStepDetailProps =
|
||||
| {
|
||||
stepId: string;
|
||||
workflowVersion: WorkflowVersion;
|
||||
readonly: true;
|
||||
onTriggerUpdate?: undefined;
|
||||
onActionUpdate?: undefined;
|
||||
}
|
||||
| {
|
||||
stepId: string;
|
||||
workflowVersion: WorkflowVersion;
|
||||
readonly?: false;
|
||||
onTriggerUpdate: (trigger: WorkflowTrigger) => void;
|
||||
onActionUpdate: (action: WorkflowAction) => void;
|
||||
};
|
||||
|
||||
export const WorkflowStepDetail = ({
|
||||
stepId,
|
||||
workflowVersion,
|
||||
...props
|
||||
}: WorkflowStepDetailProps) => {
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId,
|
||||
workflowVersion,
|
||||
});
|
||||
if (!isDefined(stepDefinition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (stepDefinition.type) {
|
||||
case 'trigger': {
|
||||
if (!isDefined(stepDefinition.definition)) {
|
||||
throw new Error(
|
||||
'Expected the trigger to be defined at this point. Ensure the trigger has been set with a default value before trying to edit it.',
|
||||
);
|
||||
}
|
||||
|
||||
switch (stepDefinition.definition.type) {
|
||||
case 'DATABASE_EVENT': {
|
||||
return (
|
||||
<WorkflowEditTriggerDatabaseEventForm
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'MANUAL': {
|
||||
return (
|
||||
<WorkflowEditTriggerManualForm
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(
|
||||
stepDefinition.definition,
|
||||
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
|
||||
);
|
||||
}
|
||||
case 'action': {
|
||||
switch (stepDefinition.definition.type) {
|
||||
case 'CODE': {
|
||||
return (
|
||||
<Suspense fallback={<RightDrawerSkeletonLoader />}>
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return (
|
||||
<WorkflowEditActionFormSendEmail
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'CREATE_RECORD': {
|
||||
return (
|
||||
<WorkflowEditActionFormCreateRecord
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'UPDATE_RECORD': {
|
||||
return (
|
||||
<WorkflowEditActionFormUpdateRecord
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'DELETE_RECORD': {
|
||||
return (
|
||||
<WorkflowEditActionFormDeleteRecord
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(
|
||||
stepDefinition,
|
||||
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
|
||||
);
|
||||
};
|
||||
@ -1,94 +0,0 @@
|
||||
import { TextInput } from '@/ui/field/input/components/TextInput';
|
||||
import styled from '@emotion/styled';
|
||||
import React, { useState } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { IconComponent } from 'packages/twenty-ui';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledHeaderInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledHeaderTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
width: 420px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledHeaderType = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledHeaderIconContainer = 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.sm};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const WorkflowStepHeader = ({
|
||||
onTitleChange,
|
||||
Icon,
|
||||
iconColor,
|
||||
initialTitle,
|
||||
headerType,
|
||||
}: {
|
||||
onTitleChange: (newTitle: string) => void;
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
headerType: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
const debouncedOnTitleChange = useDebouncedCallback(onTitleChange, 100);
|
||||
const handleChange = (newTitle: string) => {
|
||||
setTitle(newTitle);
|
||||
debouncedOnTitleChange(newTitle);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeader>
|
||||
<StyledHeaderIconContainer>
|
||||
{
|
||||
<Icon
|
||||
color={iconColor}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
size={theme.icon.size.lg}
|
||||
/>
|
||||
}
|
||||
</StyledHeaderIconContainer>
|
||||
<StyledHeaderInfo>
|
||||
<StyledHeaderTitle>
|
||||
<TextInput
|
||||
value={title}
|
||||
copyButton={false}
|
||||
hotkeyScope="workflow-step-title"
|
||||
onEnter={onTitleChange}
|
||||
onEscape={onTitleChange}
|
||||
onChange={handleChange}
|
||||
shouldTrim={false}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
<StyledHeaderType>{headerType}</StyledHeaderType>
|
||||
</StyledHeaderInfo>
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
@ -1,57 +0,0 @@
|
||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
import { SearchVariablesDropdown } from '@/workflow/search-variables/components/SearchVariablesDropdown';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const StyledSearchVariablesDropdownContainer = styled.div<{
|
||||
multiline?: boolean;
|
||||
readonly?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
${({ theme, readonly }) =>
|
||||
!readonly &&
|
||||
css`
|
||||
:hover {
|
||||
background-color: ${theme.background.transparent.light};
|
||||
}
|
||||
`}
|
||||
|
||||
${({ theme, multiline }) =>
|
||||
multiline
|
||||
? css`
|
||||
border-radius: ${theme.border.radius.sm};
|
||||
padding: ${theme.spacing(0.5)} ${theme.spacing(0)};
|
||||
position: absolute;
|
||||
right: ${theme.spacing(0)};
|
||||
top: ${theme.spacing(0)};
|
||||
`
|
||||
: css`
|
||||
background-color: ${theme.background.transparent.lighter};
|
||||
border-top-right-radius: ${theme.border.radius.sm};
|
||||
border-bottom-right-radius: ${theme.border.radius.sm};
|
||||
border: 1px solid ${theme.border.color.medium};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const WorkflowVariablePicker: VariablePickerComponent = ({
|
||||
inputId,
|
||||
disabled,
|
||||
multiline,
|
||||
onVariableSelect,
|
||||
}) => {
|
||||
return (
|
||||
<StyledSearchVariablesDropdownContainer
|
||||
multiline={multiline}
|
||||
readonly={disabled}
|
||||
>
|
||||
<SearchVariablesDropdown
|
||||
inputId={inputId}
|
||||
onVariableSelect={onVariableSelect}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledSearchVariablesDropdownContainer>
|
||||
);
|
||||
};
|
||||
@ -1,22 +0,0 @@
|
||||
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||
import { Tag } from 'twenty-ui';
|
||||
|
||||
export const WorkflowVersionStatusTag = ({
|
||||
versionStatus,
|
||||
}: {
|
||||
versionStatus: WorkflowVersionStatus;
|
||||
}) => {
|
||||
if (versionStatus === 'ACTIVE') {
|
||||
return <Tag color="green" text="Active" />;
|
||||
}
|
||||
|
||||
if (versionStatus === 'DRAFT') {
|
||||
return <Tag color="yellow" text="Draft" />;
|
||||
}
|
||||
|
||||
if (versionStatus === 'ARCHIVED') {
|
||||
return <Tag color="gray" text="Archived" />;
|
||||
}
|
||||
|
||||
return <Tag color="gray" text="Deactivated" />;
|
||||
};
|
||||
@ -1,23 +0,0 @@
|
||||
import { WorkflowDiagramCanvasReadonly } from '@/workflow/components/WorkflowDiagramCanvasReadonly';
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowVersionVisualizer = ({
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||
|
||||
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
||||
|
||||
return isDefined(workflowDiagram) && isDefined(workflowVersion) ? (
|
||||
<WorkflowDiagramCanvasReadonly
|
||||
diagram={workflowDiagram}
|
||||
workflowVersion={workflowVersion}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
@ -1,36 +0,0 @@
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
|
||||
import { getWorkflowVersionDiagram } from '@/workflow/utils/getWorkflowVersionDiagram';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowVersionVisualizerEffect = ({
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||
|
||||
const setWorkflowVersionId = useSetRecoilState(workflowVersionIdState);
|
||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkflowVersionId(workflowVersionId);
|
||||
}, [setWorkflowVersionId, workflowVersionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(workflowVersion)) {
|
||||
setWorkflowDiagram(undefined);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nextWorkflowDiagram = getWorkflowVersionDiagram(workflowVersion);
|
||||
|
||||
setWorkflowDiagram(nextWorkflowDiagram);
|
||||
}, [setWorkflowDiagram, workflowVersion]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,34 +0,0 @@
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { WorkflowDiagramCanvasEditable } from '@/workflow/components/WorkflowDiagramCanvasEditable';
|
||||
import { WorkflowDiagramEffect } from '@/workflow/components/WorkflowDiagramEffect';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowDiagramState } from '@/workflow/states/workflowDiagramState';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const WorkflowVisualizer = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const workflowId = targetableObject.id;
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowDiagramEffect
|
||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
||||
/>
|
||||
|
||||
{isDefined(workflowDiagram) && isDefined(workflowWithCurrentVersion) ? (
|
||||
<WorkflowDiagramCanvasEditable
|
||||
diagram={workflowDiagram}
|
||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
export const WorkflowVisualizerEffect = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const setWorkflowId = useSetRecoilState(workflowIdState);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkflowId(workflowId);
|
||||
}, [setWorkflowId, workflowId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,43 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { CatalogDecorator, CatalogStory, ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||
import { WorkflowVersionStatusTag } from '../WorkflowVersionStatusTag';
|
||||
|
||||
const meta: Meta<typeof WorkflowVersionStatusTag> = {
|
||||
title: 'Modules/Workflow/WorkflowVersionStatusTag',
|
||||
component: WorkflowVersionStatusTag,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowVersionStatusTag>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
versionStatus: 'DRAFT',
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof WorkflowVersionStatusTag> = {
|
||||
argTypes: {
|
||||
versionStatus: { table: { disable: true } },
|
||||
},
|
||||
parameters: {
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'version status',
|
||||
values: [
|
||||
'DRAFT',
|
||||
'ACTIVE',
|
||||
'DEACTIVATED',
|
||||
'ARCHIVED',
|
||||
] satisfies WorkflowVersionStatus[],
|
||||
props: (versionStatus: WorkflowVersionStatus) => ({ versionStatus }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
Reference in New Issue
Block a user