From e49acae851c340f91a07ad4c3801f300ce1b8c1b Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Fri, 23 Aug 2024 17:50:13 +0200 Subject: [PATCH] Visualize Workflows (#6697) ## Features - Fetch a workflow and display it in a tree with the React Flow library - The nodes are positioned by an algorithm - The feature is put behind a feature flag. The `/workflow/:id` route is disabled if the flag is off. - I started implementing a right drawer. That's a big WIP and it will be finished in another PR. ## How to test this feature 1. Create a workflow instance in the database through a GraphQL query. See below for instructions. 2. After enabling the feature flag, you should be able to see the workflow you created in the workflows list. To visualize the workflow, go to the `/workflow/:id` page where the id is the id of the workflow. See the video for a quick way to do so. ```gql // First mutation createWorkflow($data: WorkflowCreateInput!) { createWorkflow(data: $data) { id } } // Result { "data": { "name": "test" } } // Second mutation createWorkflowVersion($data: WorkflowVersionCreateInput!) { createWorkflowVersion (data: $data) { id } } // Result { "data": { "name": "v1", "trigger": { "name": "trigger", "displayName": "New or Updated Row", "type": "DATABASE_EVENT", "settings": { "eventName": "company.created", "triggerName": "Company Created" }, "nextAction": { "name": "step_1", "displayName": "Code", "type": "CODE", "valid": true, "settings": { "serverlessFunctionId": "function_id", "errorHandlingOptions": { "retryOnFailure": { "value": false }, "continueOnFailure": { "value": false } } } } }, "workflowId": "workflow_id" } } ``` https://github.com/user-attachments/assets/42bbd98c-5e13-447c-9307-461a18ac2195 --- packages/twenty-front/package.json | 1 + packages/twenty-front/src/App.tsx | 11 ++ .../types/CoreObjectNameSingular.ts | 1 + .../twenty-front/src/modules/types/AppPath.ts | 2 + .../components/RightDrawerRouter.tsx | 2 + .../constants/RightDrawerPageIcons.ts | 1 + .../constants/RightDrawerPageTitles.ts | 1 + .../right-drawer/types/RightDrawerPages.ts | 1 + .../components/RightDrawerWorkflow.tsx | 40 ++++++ .../components/WorkflowShowPageDiagram.tsx | 86 ++++++++++++ ...kflowShowPageDiagramCreateStepNode.tsx.tsx | 26 ++++ .../WorkflowShowPageDiagramStepNode.tsx | 87 +++++++++++++ .../components/WorkflowShowPageEffect.tsx | 61 +++++++++ .../components/WorkflowShowPageHeader.tsx | 30 +++++ .../states/showPageWorkflowDiagramState.ts | 9 ++ .../states/showPageWorkflowErrorState.ts | 7 + .../states/showPageWorkflowLoadingState.ts | 6 + .../src/modules/workflow/types/Workflow.ts | 66 ++++++++++ .../modules/workflow/types/WorkflowDiagram.ts | 20 +++ .../__tests__/addCreateStepNodes.test.ts | 63 +++++++++ .../__tests__/generateWorkflowDiagram.test.ts | 122 ++++++++++++++++++ .../getWorkflowLastDiagramVersion.test.ts | 79 ++++++++++++ .../workflow/utils/addCreateStepNodes.ts | 41 ++++++ .../workflow/utils/generateWorkflowDiagram.ts | 84 ++++++++++++ .../workflow/utils/getOrganizedDiagram.ts | 36 ++++++ .../utils/getWorkflowLastDiagramVersion.ts | 27 ++++ .../modules/workspace/types/FeatureFlagKey.ts | 3 +- .../src/pages/workflows/WorkflowShowPage.tsx | 64 +++++++++ .../display/icon/components/TablerIcons.ts | 1 + yarn.lock | 40 +++++- 30 files changed, 1012 insertions(+), 6 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflow.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramStepNode.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowShowPageEffect.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowShowPageHeader.tsx create mode 100644 packages/twenty-front/src/modules/workflow/states/showPageWorkflowDiagramState.ts create mode 100644 packages/twenty-front/src/modules/workflow/states/showPageWorkflowErrorState.ts create mode 100644 packages/twenty-front/src/modules/workflow/states/showPageWorkflowLoadingState.ts create mode 100644 packages/twenty-front/src/modules/workflow/types/Workflow.ts create mode 100644 packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowLastDiagramVersion.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/addCreateStepNodes.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/getOrganizedDiagram.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/getWorkflowLastDiagramVersion.ts create mode 100644 packages/twenty-front/src/pages/workflows/WorkflowShowPage.tsx diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 3dd492559b75..0478db13abe5 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -30,6 +30,7 @@ "workerDirectory": "public" }, "dependencies": { + "@xyflow/react": "^12.0.4", "transliteration": "^2.3.5" } } diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index 4f1267000478..3b165129832e 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -84,6 +84,7 @@ import { SettingsBilling } from '~/pages/settings/SettingsBilling'; import { SettingsProfile } from '~/pages/settings/SettingsProfile'; import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace'; import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers'; +import { WorkflowShowPage } from '~/pages/workflows/WorkflowShowPage'; import { getPageTitleFromPath } from '~/utils/title-utils'; const ProvidersThatNeedRouterContext = () => { @@ -135,6 +136,7 @@ const createRouter = ( isBillingEnabled?: boolean, isCRMMigrationEnabled?: boolean, isServerlessFunctionSettingsEnabled?: boolean, + isWorkflowEnabled?: boolean, ) => createBrowserRouter( createRoutesFromElements( @@ -163,6 +165,13 @@ const createRouter = ( } /> } /> + {isWorkflowEnabled === true ? ( + } + /> + ) : null} + { const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled( 'IS_FUNCTION_SETTINGS_ENABLED', ); + const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); const isBillingPageEnabled = billing?.isBillingEnabled && !isFreeAccessEnabled; @@ -336,6 +346,7 @@ export const App = () => { isBillingPageEnabled, isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, + isWorkflowEnabled, )} /> ); diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts index f0b76f473928..2255c8cb56b4 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts @@ -28,4 +28,5 @@ export enum CoreObjectNameSingular { Webhook = 'webhook', WorkspaceMember = 'workspaceMember', MessageThreadSubscriber = 'messageThreadSubscriber', + Workflow = 'workflow', } diff --git a/packages/twenty-front/src/modules/types/AppPath.ts b/packages/twenty-front/src/modules/types/AppPath.ts index dbbab7ad239e..12d6f7c19e7f 100644 --- a/packages/twenty-front/src/modules/types/AppPath.ts +++ b/packages/twenty-front/src/modules/types/AppPath.ts @@ -26,6 +26,8 @@ export enum AppPath { Developers = `developers`, DevelopersCatchAll = `/${Developers}/*`, + WorkflowShowPage = `/workflow/:workflowId`, + // Impersonate Impersonate = '/impersonate/:userId', diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx index 6bcf5a056c7f..bf5ab29bebf6 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx @@ -12,6 +12,7 @@ import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/Compo import { isDefined } from 'twenty-ui'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { RightDrawerPages } from '../types/RightDrawerPages'; +import { RightDrawerWorkflow } from '@/workflow/components/RightDrawerWorkflow'; const StyledRightDrawerPage = styled.div` display: flex; @@ -35,6 +36,7 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = { [RightDrawerPages.ViewCalendarEvent]: , [RightDrawerPages.ViewRecord]: , [RightDrawerPages.Copilot]: , + [RightDrawerPages.Workflow]: , }; export const RightDrawerRouter = () => { diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts index 5b8c112be571..aed035783839 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts @@ -5,4 +5,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = { [RightDrawerPages.ViewCalendarEvent]: 'IconCalendarEvent', [RightDrawerPages.ViewRecord]: 'Icon123', [RightDrawerPages.Copilot]: 'IconSparkles', + [RightDrawerPages.Workflow]: 'IconSparkles', }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts index 9bf58e09c518..bb74c9da8153 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts @@ -5,4 +5,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = { [RightDrawerPages.ViewCalendarEvent]: 'Calendar Event', [RightDrawerPages.ViewRecord]: 'Record Editor', [RightDrawerPages.Copilot]: 'Copilot', + [RightDrawerPages.Workflow]: 'Workflow', }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts index 200602767ae6..2217d437f4fd 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts @@ -3,4 +3,5 @@ export enum RightDrawerPages { ViewCalendarEvent = 'view-calendar-event', ViewRecord = 'view-record', Copilot = 'copilot', + Workflow = 'workflow', } diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflow.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflow.tsx new file mode 100644 index 000000000000..b12e749a899e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflow.tsx @@ -0,0 +1,40 @@ +import styled from '@emotion/styled'; + +const StyledContainer = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + height: 100%; + justify-content: flex-start; + overflow-y: auto; + position: relative; +`; + +const StyledChatArea = styled.div` + flex: 1; + display: flex; + flex-direction: column; + overflow-y: scroll; + padding: ${({ theme }) => theme.spacing(6)}; + padding-bottom: 0px; +`; + +const StyledNewMessageArea = styled.div` + display: flex; + flex-direction: column; + padding: ${({ theme }) => theme.spacing(6)}; + padding-top: 0px; +`; + +export const RightDrawerWorkflow = () => { + const handleCreateCodeBlock = () => {}; + + return ( + + {/* TODO */} + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx new file mode 100644 index 000000000000..090d074ad131 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagram.tsx @@ -0,0 +1,86 @@ +import { WorkflowShowPageDiagramCreateStepNode } from '@/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx'; +import { WorkflowShowPageDiagramStepNode } from '@/workflow/components/WorkflowShowPageDiagramStepNode'; +import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState'; +import { + WorkflowDiagram, + WorkflowDiagramEdge, + WorkflowDiagramNode, +} from '@/workflow/types/WorkflowDiagram'; +import { getOrganizedDiagram } from '@/workflow/utils/getOrganizedDiagram'; +import { + applyEdgeChanges, + applyNodeChanges, + Background, + EdgeChange, + NodeChange, + ReactFlow, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { useMemo } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { GRAY_SCALE, isDefined } from 'twenty-ui'; + +export const WorkflowShowPageDiagram = ({ + diagram, +}: { + diagram: WorkflowDiagram; +}) => { + const { nodes, edges } = useMemo( + () => getOrganizedDiagram(diagram), + [diagram], + ); + + const setShowPageWorkflowDiagram = useSetRecoilState( + showPageWorkflowDiagramState, + ); + + const handleNodesChange = ( + nodeChanges: Array>, + ) => { + setShowPageWorkflowDiagram((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>, + ) => { + setShowPageWorkflowDiagram((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), + }; + }); + }; + + return ( + ({ ...node, draggable: false }))} + edges={edges} + onNodesChange={handleNodesChange} + onEdgesChange={handleEdgesChange} + > + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx.tsx new file mode 100644 index 000000000000..a799480e81ff --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramCreateStepNode.tsx.tsx @@ -0,0 +1,26 @@ +import { IconButton } from '@/ui/input/button/components/IconButton'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import styled from '@emotion/styled'; +import { Handle, Position } from '@xyflow/react'; +import { IconPlus } from 'twenty-ui'; + +export const StyledTargetHandle = styled(Handle)` + visibility: hidden; +`; + +export const WorkflowShowPageDiagramCreateStepNode = () => { + const { openRightDrawer } = useRightDrawer(); + + const handleCreateStepNodeButtonClick = () => { + openRightDrawer(RightDrawerPages.Workflow); + }; + + return ( +
+ + + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramStepNode.tsx new file mode 100644 index 000000000000..f8f9a3719f12 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageDiagramStepNode.tsx @@ -0,0 +1,87 @@ +import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; +import styled from '@emotion/styled'; +import { Handle, Position } from '@xyflow/react'; + +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.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + + padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)}; + position: absolute; + top: 0; + transform: translateY(-100%); + + .selectable.selected &, + .selectable:focus &, + .selectable:focus-visible & { + background-color: ${({ theme }) => theme.color.blue}; + color: ${({ theme }) => theme.font.color.inverted}; + } +`; + +const StyledStepNodeInnerContainer = styled.div` + background-color: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.md}; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; + + position: relative; + box-shadow: ${({ theme }) => 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` + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.medium}; +`; + +const StyledSourceHandle = styled(Handle)` + background-color: ${({ theme }) => theme.color.gray50}; +`; + +export const StyledTargetHandle = styled(Handle)` + visibility: hidden; +`; + +export const WorkflowShowPageDiagramStepNode = ({ + data, +}: { + data: WorkflowDiagramStepNodeData; +}) => { + return ( + + {data.nodeType !== 'trigger' ? ( + + ) : null} + + + {data.nodeType} + + {data.label} + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageEffect.tsx new file mode 100644 index 000000000000..6d41e5d7fd4b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageEffect.tsx @@ -0,0 +1,61 @@ +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 { showPageWorkflowLoadingState } from '@/workflow/states/showPageWorkflowLoadingState'; +import { Workflow } from '@/workflow/types/Workflow'; +import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes'; +import { getWorkflowLastDiagramVersion } from '@/workflow/utils/getWorkflowLastDiagramVersion'; +import { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +type WorkflowShowPageEffectProps = { + workflowId: string; +}; + +export const WorkflowShowPageEffect = ({ + workflowId, +}: WorkflowShowPageEffectProps) => { + const { + record: workflow, + loading, + error, + } = useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.Workflow, + objectRecordId: workflowId, + recordGqlFields: { + id: true, + name: true, + versions: true, + publishedVersionId: true, + }, + }); + + const setCurrentWorkflowData = useSetRecoilState( + showPageWorkflowDiagramState, + ); + const setCurrentWorkflowLoading = useSetRecoilState( + showPageWorkflowLoadingState, + ); + const setCurrentWorkflowError = useSetRecoilState(showPageWorkflowErrorState); + + useEffect(() => { + const flowLastVersion = getWorkflowLastDiagramVersion(workflow); + const flowWithCreateStepNodes = addCreateStepNodes(flowLastVersion); + + setCurrentWorkflowData( + isDefined(workflow) ? flowWithCreateStepNodes : undefined, + ); + }, [setCurrentWorkflowData, workflow]); + + useEffect(() => { + setCurrentWorkflowLoading(loading); + }, [loading, setCurrentWorkflowLoading]); + + useEffect(() => { + setCurrentWorkflowError(error); + }, [error, setCurrentWorkflowError]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageHeader.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageHeader.tsx new file mode 100644 index 000000000000..7534fce17f12 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowShowPageHeader.tsx @@ -0,0 +1,30 @@ +import { PageHeader } from '@/ui/layout/page/PageHeader'; +import { useNavigate } from 'react-router-dom'; +import { IconComponent } from 'twenty-ui'; + +export const WorkflowShowPageHeader = ({ + workflowName, + headerIcon, + children, +}: { + workflowName: string; + headerIcon: IconComponent; + children?: React.ReactNode; +}) => { + const navigate = useNavigate(); + + return ( + { + navigate({ + pathname: '/objects/workflows', + }); + }} + title={workflowName} + Icon={headerIcon} + > + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/states/showPageWorkflowDiagramState.ts b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowDiagramState.ts new file mode 100644 index 000000000000..614aca700b6a --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowDiagramState.ts @@ -0,0 +1,9 @@ +import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram'; +import { createState } from 'twenty-ui'; + +export const showPageWorkflowDiagramState = createState< + WorkflowDiagram | undefined +>({ + key: 'showPageWorkflowDiagramState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/workflow/states/showPageWorkflowErrorState.ts b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowErrorState.ts new file mode 100644 index 000000000000..f49f5cd90099 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowErrorState.ts @@ -0,0 +1,7 @@ +import { ApolloError } from '@apollo/client'; +import { createState } from 'twenty-ui'; + +export const showPageWorkflowErrorState = createState({ + key: 'showPageWorkflowErrorState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/workflow/states/showPageWorkflowLoadingState.ts b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowLoadingState.ts new file mode 100644 index 000000000000..0c5342abb3a6 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/states/showPageWorkflowLoadingState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const showPageWorkflowLoadingState = createState({ + key: 'showPageWorkflowLoadingState', + defaultValue: true, +}); diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts new file mode 100644 index 000000000000..ea5d39d1ec86 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -0,0 +1,66 @@ +type WorkflowBaseSettingsType = { + errorHandlingOptions: { + retryOnFailure: { + value: boolean; + }; + continueOnFailure: { + value: boolean; + }; + }; +}; + +export type WorkflowCodeSettingsType = WorkflowBaseSettingsType & { + serverlessFunctionId: string; +}; + +export type WorkflowActionType = 'CODE_ACTION'; + +type CommonWorkflowAction = { + id: string; + name: string; + valid: boolean; +}; + +type WorkflowCodeAction = CommonWorkflowAction & { + type: 'CODE_ACTION'; + settings: WorkflowCodeSettingsType; +}; + +export type WorkflowAction = WorkflowCodeAction; + +export type WorkflowStep = WorkflowAction; + +export type WorkflowTriggerType = 'DATABASE_EVENT'; + +type BaseTrigger = { + type: WorkflowTriggerType; + input?: object; +}; + +export type WorkflowDatabaseEventTrigger = BaseTrigger & { + type: 'DATABASE_EVENT'; + settings: { + eventName: string; + }; +}; + +export type WorkflowTrigger = WorkflowDatabaseEventTrigger; + +export type WorkflowVersion = { + id: string; + name: string; + createdAt: string; + updatedAt: string; + workflowId: string; + trigger: WorkflowTrigger; + steps: Array; + __typename: 'WorkflowVersion'; +}; + +export type Workflow = { + __typename: 'Workflow'; + id: string; + name: string; + versions: Array; + publishedVersionId: string; +}; diff --git a/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts new file mode 100644 index 000000000000..f97d5027ed0c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts @@ -0,0 +1,20 @@ +import { Edge, Node } from '@xyflow/react'; + +export type WorkflowDiagramNode = Node; +export type WorkflowDiagramEdge = Edge; + +export type WorkflowDiagram = { + nodes: Array; + edges: Array; +}; + +export type WorkflowDiagramStepNodeData = { + nodeType: 'trigger' | 'condition' | 'action'; + label: string; +}; + +export type WorkflowDiagramCreateStepNodeData = Record; + +export type WorkflowDiagramNodeData = + | WorkflowDiagramStepNodeData + | WorkflowDiagramCreateStepNodeData; diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts new file mode 100644 index 000000000000..e3326c795be0 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts @@ -0,0 +1,63 @@ +import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow'; +import { generateWorkflowDiagram } from '@/workflow/utils/generateWorkflowDiagram'; +import { addCreateStepNodes } from '../addCreateStepNodes'; + +describe('addCreateStepNodes', () => { + it("adds a create step node to the end of a single-branch flow and doesn't change the shape of other nodes", () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'CODE_ACTION', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + }, + { + id: 'step2', + name: 'Step 2', + type: 'CODE_ACTION', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + }, + ]; + + const diagramInitial = generateWorkflowDiagram({ trigger, steps }); + + expect(diagramInitial.nodes).toHaveLength(3); + expect(diagramInitial.edges).toHaveLength(2); + + const diagramWithCreateStepNodes = addCreateStepNodes(diagramInitial); + + expect(diagramWithCreateStepNodes.nodes).toHaveLength(4); + expect(diagramWithCreateStepNodes.edges).toHaveLength(3); + + expect(diagramWithCreateStepNodes.nodes[0].type).toBe(undefined); + expect(diagramWithCreateStepNodes.nodes[0].data.nodeType).toBe('trigger'); + + expect(diagramWithCreateStepNodes.nodes[1].type).toBe(undefined); + expect(diagramWithCreateStepNodes.nodes[1].data.nodeType).toBe('action'); + + expect(diagramWithCreateStepNodes.nodes[2].type).toBe(undefined); + expect(diagramWithCreateStepNodes.nodes[2].data.nodeType).toBe('action'); + + expect(diagramWithCreateStepNodes.nodes[3].type).toBe('create-step'); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts new file mode 100644 index 000000000000..0716aa29bb83 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts @@ -0,0 +1,122 @@ +import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow'; +import { generateWorkflowDiagram } from '../generateWorkflowDiagram'; + +describe('generateWorkflowDiagram', () => { + it('should generate a single trigger node when no step is provided', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + }, + }; + const steps: WorkflowStep[] = []; + + const result = generateWorkflowDiagram({ trigger, steps }); + + expect(result.nodes).toHaveLength(1); + expect(result.edges).toHaveLength(0); + + expect(result.nodes[0]).toMatchObject({ + data: { + label: trigger.settings.eventName, + nodeType: 'trigger', + }, + }); + }); + + it('should generate a diagram with nodes and edges corresponding to the steps', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'CODE_ACTION', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + }, + { + id: 'step2', + name: 'Step 2', + type: 'CODE_ACTION', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + }, + ]; + + const result = generateWorkflowDiagram({ trigger, steps }); + + expect(result.nodes).toHaveLength(steps.length + 1); // All steps + trigger + expect(result.edges).toHaveLength(steps.length - 1 + 1); // Edges are one less than nodes + the edge from the trigger to the first node + + expect(result.nodes[0].data.nodeType).toBe('trigger'); + + const stepNodes = result.nodes.slice(1); + + for (const [index, step] of steps.entries()) { + expect(stepNodes[index].data.nodeType).toBe('action'); + expect(stepNodes[index].data.label).toBe(step.name); + } + }); + + it('should correctly link nodes with edges', () => { + const trigger: WorkflowTrigger = { + type: 'DATABASE_EVENT', + settings: { + eventName: 'company.created', + }, + }; + const steps: WorkflowStep[] = [ + { + id: 'step1', + name: 'Step 1', + type: 'CODE_ACTION', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + }, + { + id: 'step2', + name: 'Step 2', + type: 'CODE_ACTION', + valid: true, + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + }, + ]; + + const result = generateWorkflowDiagram({ trigger, steps }); + + expect(result.edges[0].source).toEqual(result.nodes[0].id); + expect(result.edges[0].target).toEqual(result.nodes[1].id); + + expect(result.edges[1].source).toEqual(result.nodes[1].id); + expect(result.edges[1].target).toEqual(result.nodes[2].id); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowLastDiagramVersion.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowLastDiagramVersion.test.ts new file mode 100644 index 000000000000..20c41c085bb1 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowLastDiagramVersion.test.ts @@ -0,0 +1,79 @@ +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); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/utils/addCreateStepNodes.ts b/packages/twenty-front/src/modules/workflow/utils/addCreateStepNodes.ts new file mode 100644 index 000000000000..9d2941adb274 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/addCreateStepNodes.ts @@ -0,0 +1,41 @@ +import { + WorkflowDiagram, + WorkflowDiagramEdge, + WorkflowDiagramNode, +} from '@/workflow/types/WorkflowDiagram'; +import { MarkerType } from '@xyflow/react'; +import { v4 } from 'uuid'; + +export const addCreateStepNodes = ({ nodes, edges }: WorkflowDiagram) => { + const nodesWithoutTargets = nodes.filter((node) => + edges.every((edge) => edge.source !== node.id), + ); + + const updatedNodes: Array = nodes.slice(); + const updatedEdges: Array = edges.slice(); + + for (const node of nodesWithoutTargets) { + const newCreateStepNode: WorkflowDiagramNode = { + id: v4(), + type: 'create-step', + data: {}, + position: { x: 0, y: 0 }, + }; + + updatedNodes.push(newCreateStepNode); + + updatedEdges.push({ + id: v4(), + source: node.id, + target: newCreateStepNode.id, + markerEnd: { + type: MarkerType.ArrowClosed, + }, + }); + } + + return { + nodes: updatedNodes, + edges: updatedEdges, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts new file mode 100644 index 000000000000..6bb95f23cbba --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts @@ -0,0 +1,84 @@ +import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow'; +import { + WorkflowDiagram, + WorkflowDiagramEdge, + WorkflowDiagramNode, +} from '@/workflow/types/WorkflowDiagram'; +import { MarkerType } from '@xyflow/react'; +import { v4 } from 'uuid'; + +export const generateWorkflowDiagram = ({ + trigger, + steps, +}: { + trigger: WorkflowTrigger; + steps: Array; +}): WorkflowDiagram => { + const nodes: Array = []; + const edges: Array = []; + + // Helper function to generate nodes and edges recursively + const processNode = ( + step: WorkflowStep, + parentNodeId: string, + xPos: number, + yPos: number, + ) => { + const nodeId = v4(); + nodes.push({ + id: nodeId, + data: { + nodeType: 'action', + label: step.name, + }, + position: { + x: xPos, + y: yPos, + }, + }); + + // Create an edge from the parent node to this node + edges.push({ + id: v4(), + source: parentNodeId, + target: nodeId, + markerEnd: { + type: MarkerType.ArrowClosed, + }, + }); + + // Recursively generate flow for the next action if it exists + if (step.type !== 'CODE_ACTION') { + // processNode(action.nextAction, nodeId, xPos + 150, yPos + 100); + + throw new Error('Other types as code actions are not supported yet.'); + } + + return nodeId; + }; + + // Start with the trigger node + const triggerNodeId = v4(); + nodes.push({ + id: triggerNodeId, + data: { + nodeType: 'trigger', + label: trigger.settings.eventName, + }, + position: { + x: 0, + y: 0, + }, + }); + + let lastStepId = triggerNodeId; + + for (const step of steps) { + lastStepId = processNode(step, lastStepId, 150, 100); + } + + return { + nodes, + edges, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/getOrganizedDiagram.ts b/packages/twenty-front/src/modules/workflow/utils/getOrganizedDiagram.ts new file mode 100644 index 000000000000..0aa687d71054 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/getOrganizedDiagram.ts @@ -0,0 +1,36 @@ +import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram'; +import Dagre from '@dagrejs/dagre'; + +/** + * Set the position of the nodes in the diagram. The positions are computed with a layouting algorithm. + */ +export const getOrganizedDiagram = ( + diagram: WorkflowDiagram, +): WorkflowDiagram => { + const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + graph.setGraph({ rankdir: 'TB' }); + + diagram.edges.forEach((edge) => graph.setEdge(edge.source, edge.target)); + diagram.nodes.forEach((node) => + graph.setNode(node.id, { + ...node, + width: node.measured?.width ?? 0, + height: node.measured?.height ?? 0, + }), + ); + + Dagre.layout(graph); + + return { + nodes: diagram.nodes.map((node) => { + const position = graph.node(node.id); + // We are shifting the dagre node position (anchor=center center) to the top left + // so it matches the React Flow node anchor point (top left). + const x = position.x - (node.measured?.width ?? 0) / 2; + const y = position.y - (node.measured?.height ?? 0) / 2; + + return { ...node, position: { x, y } }; + }), + edges: diagram.edges, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastDiagramVersion.ts b/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastDiagramVersion.ts new file mode 100644 index 000000000000..448ed7fec4b6 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/getWorkflowLastDiagramVersion.ts @@ -0,0 +1,27 @@ +import { Workflow } 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 getWorkflowLastDiagramVersion = ( + workflow: Workflow | undefined, +): WorkflowDiagram => { + if (!isDefined(workflow)) { + return EMPTY_DIAGRAM; + } + + const lastVersion = workflow.versions.at(-1); + if (!isDefined(lastVersion) || !isDefined(lastVersion.trigger)) { + return EMPTY_DIAGRAM; + } + + return generateWorkflowDiagram({ + trigger: lastVersion.trigger, + steps: lastVersion.steps, + }); +}; diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index 6ed0dd7f1e05..e1dd39384ad7 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -8,4 +8,5 @@ export type FeatureFlagKey = | 'IS_COPILOT_ENABLED' | 'IS_CRM_MIGRATION_ENABLED' | 'IS_FREE_ACCESS_ENABLED' - | 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'; + | 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED' + | 'IS_WORKFLOW_ENABLED'; diff --git a/packages/twenty-front/src/pages/workflows/WorkflowShowPage.tsx b/packages/twenty-front/src/pages/workflows/WorkflowShowPage.tsx new file mode 100644 index 000000000000..0a5125ef9081 --- /dev/null +++ b/packages/twenty-front/src/pages/workflows/WorkflowShowPage.tsx @@ -0,0 +1,64 @@ +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 { WorkflowShowPageEffect } from '@/workflow/components/WorkflowShowPageEffect'; +import { WorkflowShowPageHeader } from '@/workflow/components/WorkflowShowPageHeader'; +import { showPageWorkflowDiagramState } from '@/workflow/states/showPageWorkflowDiagramState'; +import styled from '@emotion/styled'; +import '@xyflow/react/dist/style.css'; +import { useParams } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import { IconSettingsAutomation } from 'twenty-ui'; + +const StyledFlowContainer = styled.div` + height: 100%; + width: 100%; + + /* Below we reset the default styling of Reactflow */ + .react-flow__node-input, + .react-flow__node-default, + .react-flow__node-output, + .react-flow__node-group { + padding: 0; + } + + --xy-node-border-radius: none; + --xy-node-border: none; + --xy-node-background-color: none; + --xy-node-boxshadow-hover: none; + --xy-node-boxshadow-selected: none; +`; + +export const WorkflowShowPage = () => { + const parameters = useParams<{ + workflowId: string; + }>(); + + const workflowName = 'Test Workflow'; + + const showPageWorkflowDiagram = useRecoilValue(showPageWorkflowDiagramState); + + if (parameters.workflowId === undefined) { + return null; + } + + return ( + + + + + + + + {showPageWorkflowDiagram === undefined ? null : ( + + )} + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 9e05f1390ff2..aa95be3a50c6 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -153,6 +153,7 @@ export { IconSearch, IconSend, IconSettings, + IconSettingsAutomation, IconSortDescending, IconSparkles, IconSql, diff --git a/yarn.lock b/yarn.lock index da40f750f626..d6163be0f2c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15782,7 +15782,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-drag@npm:*, @types/d3-drag@npm:^3.0.1": +"@types/d3-drag@npm:*, @types/d3-drag@npm:^3.0.1, @types/d3-drag@npm:^3.0.7": version: 3.0.7 resolution: "@types/d3-drag@npm:3.0.7" dependencies: @@ -15920,7 +15920,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.3": +"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10, @types/d3-selection@npm:^3.0.3": version: 3.0.10 resolution: "@types/d3-selection@npm:3.0.10" checksum: 10c0/de1f99ab186a08999bf394a645fd76911add1b02316270d4c07616c8383903a2b068d7e02b73b6a99a1f26bb49a2e99ef4b55a5d2ddfa165f6f3c53144897920 @@ -15987,7 +15987,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-transition@npm:*": +"@types/d3-transition@npm:*, @types/d3-transition@npm:^3.0.8": version: 3.0.8 resolution: "@types/d3-transition@npm:3.0.8" dependencies: @@ -15996,7 +15996,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-zoom@npm:*, @types/d3-zoom@npm:^3.0.1": +"@types/d3-zoom@npm:*, @types/d3-zoom@npm:^3.0.1, @types/d3-zoom@npm:^3.0.8": version: 3.0.8 resolution: "@types/d3-zoom@npm:3.0.8" dependencies: @@ -18303,6 +18303,35 @@ __metadata: languageName: node linkType: hard +"@xyflow/react@npm:^12.0.4": + version: 12.0.4 + resolution: "@xyflow/react@npm:12.0.4" + dependencies: + "@xyflow/system": "npm:0.0.37" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.0" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/57b04024c3cca1b5d19b5625b92a5ca5015870a5b6adf2ab2c0bcfa701f93929805777ad081e7142b9c94846ad83d65abb65041b50134515b135b6514d74766e + languageName: node + linkType: hard + +"@xyflow/system@npm:0.0.37": + version: 0.0.37 + resolution: "@xyflow/system@npm:0.0.37" + dependencies: + "@types/d3-drag": "npm:^3.0.7" + "@types/d3-selection": "npm:^3.0.10" + "@types/d3-transition": "npm:^3.0.8" + "@types/d3-zoom": "npm:^3.0.8" + d3-drag: "npm:^3.0.0" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" + checksum: 10c0/60b2de70a53dc3f2b691d837f2adcd2324f2e3e19258d6928e58578ad896a7f9fa7dd20938b224e7054284542135e0d7519ab34c012d69a8ed0e15ecf452d1ee + languageName: node + linkType: hard + "@yarnpkg/esbuild-plugin-pnp@npm:^3.0.0-rc.10": version: 3.0.0-rc.15 resolution: "@yarnpkg/esbuild-plugin-pnp@npm:3.0.0-rc.15" @@ -47029,6 +47058,7 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-front@workspace:packages/twenty-front" dependencies: + "@xyflow/react": "npm:^12.0.4" transliteration: "npm:^2.3.5" languageName: unknown linkType: soft @@ -50710,7 +50740,7 @@ __metadata: languageName: node linkType: hard -"zustand@npm:^4.4.1": +"zustand@npm:^4.4.0, zustand@npm:^4.4.1": version: 4.5.4 resolution: "zustand@npm:4.5.4" dependencies: