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: