From a0a2e099c9ecec92e2e7f20d8fcbd1803f609bed Mon Sep 17 00:00:00 2001 From: handreyrc Date: Wed, 3 Jun 2026 13:37:56 -0400 Subject: [PATCH 1/5] Enable read only mode Signed-off-by: handreyrc --- .../src/react-flow/diagram/Diagram.css | 5 + .../src/react-flow/diagram/Diagram.tsx | 10 +- .../tests/react-flow/diagram/Diagram.test.tsx | 96 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css index 04188fdf..8c92ccda 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css @@ -56,6 +56,11 @@ .dec-root .react-flow__pane:active { cursor: grabbing !important; } + + /* hide handles in read-only mode */ + .dec-root .read-only .react-flow__handle { + visibility: hidden !important; + } } /* custom nodes */ diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx index 6cb41c3b..2d0e7cfa 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx @@ -47,7 +47,7 @@ export type DiagramProps = { export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { const reactFlowInstance: RF.ReactFlowInstance = RF.useReactFlow(); - const { model, nodes, edges, setNodes, setEdges } = useDiagramEditorContext(); + const { model, nodes, edges, isReadOnly, setNodes, setEdges } = useDiagramEditorContext(); const [minimapVisible, setMinimapVisible] = React.useState(false); @@ -126,7 +126,11 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { }, [model, reactFlowInstance, setNodes, setEdges]); return ( -
+
{ }, }} data-testid={"react-flow-canvas"} + nodesDraggable={!isReadOnly} + nodesConnectable={!isReadOnly} > {minimapVisible && } diff --git a/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx b/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx index bf1db18a..8f4107c2 100644 --- a/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx @@ -63,4 +63,100 @@ describe("Diagram Component", () => { expect(applyAutoLayoutSpy).toHaveBeenCalled(); }); }); + + it("should apply read-only class when isReadOnly is true", async () => { + render( + + + + + + + + + , + ); + + const diagram = screen.getByTestId("diagram-container"); + + // Verify that the read-only class is applied + expect(diagram).toHaveClass("read-only"); + + await waitFor(() => { + expect(applyAutoLayoutSpy).toHaveBeenCalled(); + }); + }); + + it("should not apply read-only class when isReadOnly is false", async () => { + render( + + + + + + + + + , + ); + + const diagram = screen.getByTestId("diagram-container"); + + // Verify that the read-only class is not applied + expect(diagram).not.toHaveClass("read-only"); + + await waitFor(() => { + expect(applyAutoLayoutSpy).toHaveBeenCalled(); + }); + }); + + it("should hide edge handles when isReadOnly is true", async () => { + const { container } = render( + + + + + + + + + , + ); + + const diagram = screen.getByTestId("diagram-container"); + + // Verify that the read-only class is applied (which hides handles via CSS) + expect(diagram).toHaveClass("read-only"); + + // Verify that the CSS rule for hiding handles exists + const styles = window.getComputedStyle(container); + expect(styles).toBeDefined(); + + await waitFor(() => { + expect(applyAutoLayoutSpy).toHaveBeenCalled(); + }); + }); + + it("should show edge handles when isReadOnly is false", async () => { + render( + + + + + + + + + , + ); + + const diagram = screen.getByTestId("diagram-container"); + + // Verify that the read-only class is not applied + expect(diagram).not.toHaveClass("read-only"); + + await waitFor(() => { + expect(applyAutoLayoutSpy).toHaveBeenCalled(); + }); + }); }); From d0dc62233ab9a0399b20a7c8a2645efc596b7dab Mon Sep 17 00:00:00 2001 From: handreyrc Date: Wed, 3 Jun 2026 15:05:16 -0400 Subject: [PATCH 2/5] Fix copilot complaints Signed-off-by: handreyrc --- .../src/react-flow/diagram/Diagram.css | 4 + .../src/react-flow/diagram/Diagram.tsx | 4 +- .../tests/react-flow/diagram/Diagram.test.tsx | 116 +++++++++--------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css index 8c92ccda..242c2cd2 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css @@ -60,6 +60,10 @@ /* hide handles in read-only mode */ .dec-root .read-only .react-flow__handle { visibility: hidden !important; + width: 0 !important; + height: 0 !important; + min-width: 0 !important; + min-height: 0 !important; } } diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx index 2d0e7cfa..eb64a56f 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx @@ -128,7 +128,9 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { return (
+ + + + + + + + , + ); +} + describe("Diagram Component", () => { let applyAutoLayoutSpy: ReturnType; @@ -40,17 +69,7 @@ describe("Diagram Component", () => { }); it("render Diagram component and canvas", async () => { - render( - - - - - - - - - , - ); + renderDiagram({ isReadOnly: true }); const diagram = screen.getByTestId("diagram-container"); const canvas = screen.getByTestId("react-flow-canvas"); @@ -65,17 +84,7 @@ describe("Diagram Component", () => { }); it("should apply read-only class when isReadOnly is true", async () => { - render( - - - - - - - - - , - ); + renderDiagram({ isReadOnly: true }); const diagram = screen.getByTestId("diagram-container"); @@ -88,17 +97,7 @@ describe("Diagram Component", () => { }); it("should not apply read-only class when isReadOnly is false", async () => { - render( - - - - - - - - - , - ); + renderDiagram({ isReadOnly: false }); const diagram = screen.getByTestId("diagram-container"); @@ -110,51 +109,46 @@ describe("Diagram Component", () => { }); }); - it("should hide edge handles when isReadOnly is true", async () => { - const { container } = render( - - - - - - - - - , - ); + it("should disable node interaction when isReadOnly is true", async () => { + renderDiagram({ isReadOnly: true }); const diagram = screen.getByTestId("diagram-container"); - // Verify that the read-only class is applied (which hides handles via CSS) + // Verify that the read-only class is applied + // This class applies CSS rule: .read-only .react-flow__handle { visibility: hidden !important; } expect(diagram).toHaveClass("read-only"); - // Verify that the CSS rule for hiding handles exists - const styles = window.getComputedStyle(container); - expect(styles).toBeDefined(); + // Verify ReactFlow canvas is rendered + const canvas = screen.getByTestId("react-flow-canvas"); + expect(canvas).toBeInTheDocument(); + + // Note: The actual CSS visibility of handles cannot be tested in JSDOM as it doesn't apply stylesheets. + // The read-only behavior is enforced through: + // 1. CSS class "read-only" which hides .react-flow__handle elements + // 2. ReactFlow props nodesDraggable={false} and nodesConnectable={false} + // For full verification of handle visibility, use e2e tests where CSS is applied. await waitFor(() => { expect(applyAutoLayoutSpy).toHaveBeenCalled(); }); }); - it("should show edge handles when isReadOnly is false", async () => { - render( - - - - - - - - - , - ); + it("should enable node interaction when isReadOnly is false", async () => { + renderDiagram({ isReadOnly: false }); const diagram = screen.getByTestId("diagram-container"); // Verify that the read-only class is not applied expect(diagram).not.toHaveClass("read-only"); + // Verify ReactFlow canvas is rendered + const canvas = screen.getByTestId("react-flow-canvas"); + expect(canvas).toBeInTheDocument(); + + // Note: When read-only class is not present, handles are visible via CSS + // and ReactFlow props nodesDraggable={true} and nodesConnectable={true} enable interaction. + // For full verification of handle visibility and interaction, use e2e tests. + await waitFor(() => { expect(applyAutoLayoutSpy).toHaveBeenCalled(); }); From 9478d71b1afd323b2f7fdc6cf83ffc58d577365d Mon Sep 17 00:00:00 2001 From: handreyrc Date: Thu, 4 Jun 2026 15:49:36 -0400 Subject: [PATCH 3/5] Fix review comments Signed-off-by: handreyrc --- .changeset/read-only-mode.md | 5 +++++ .../src/react-flow/diagram/Diagram.tsx | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 .changeset/read-only-mode.md diff --git a/.changeset/read-only-mode.md b/.changeset/read-only-mode.md new file mode 100644 index 00000000..fc54131f --- /dev/null +++ b/.changeset/read-only-mode.md @@ -0,0 +1,5 @@ +--- +"@serverlessworkflow/diagram-editor": minor +--- + +Enable read only mode locking nodes and edges on the canvas. diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx index eb64a56f..9c9df833 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx @@ -128,9 +128,7 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { return (
Date: Thu, 4 Jun 2026 16:06:34 -0400 Subject: [PATCH 4/5] fix copilot coplaints Signed-off-by: handreyrc --- .changeset/read-only-mode.md | 2 +- .../tests/react-flow/diagram/Diagram.test.tsx | 44 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.changeset/read-only-mode.md b/.changeset/read-only-mode.md index fc54131f..e1c48b20 100644 --- a/.changeset/read-only-mode.md +++ b/.changeset/read-only-mode.md @@ -2,4 +2,4 @@ "@serverlessworkflow/diagram-editor": minor --- -Enable read only mode locking nodes and edges on the canvas. +Enable read-only mode locking nodes and edges on the canvas. diff --git a/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx b/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx index faa38dfd..80422a8c 100644 --- a/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx @@ -21,9 +21,20 @@ import { DiagramEditorContextProvider } from "../../../src/store/DiagramEditorCo import { SidebarProvider } from "../../../src/components/ui/sidebar"; import { I18nProvider } from "@serverlessworkflow/i18n"; import { en } from "../../../src/i18n/locales/en"; -import { ReactFlowProvider } from "@xyflow/react"; +import { ReactFlowProvider, ReactFlow } from "@xyflow/react"; import * as autoLayoutModule from "../../../src/react-flow/diagram/autoLayout"; +// Mock ReactFlow to capture props +vi.mock("@xyflow/react", async () => { + const actual = await vi.importActual("@xyflow/react"); + return { + ...actual, + ReactFlow: vi.fn((props) => { + return
; + }), + }; +}); + /** * Helper function to render the Diagram component with all required providers * @param options - Configuration options for the diagram @@ -62,6 +73,9 @@ describe("Diagram Component", () => { nodes: [], edges: [], }); + + // Clear mock calls before each test + vi.mocked(ReactFlow).mockClear(); }); afterEach(() => { @@ -122,11 +136,16 @@ describe("Diagram Component", () => { const canvas = screen.getByTestId("react-flow-canvas"); expect(canvas).toBeInTheDocument(); - // Note: The actual CSS visibility of handles cannot be tested in JSDOM as it doesn't apply stylesheets. - // The read-only behavior is enforced through: - // 1. CSS class "read-only" which hides .react-flow__handle elements - // 2. ReactFlow props nodesDraggable={false} and nodesConnectable={false} - // For full verification of handle visibility, use e2e tests where CSS is applied. + // Wait for ReactFlow to be called + await waitFor(() => { + expect(ReactFlow).toHaveBeenCalled(); + }); + + // Verify that ReactFlow was called with nodesDraggable={false} and nodesConnectable={false} + const mockReactFlow = vi.mocked(ReactFlow); + const reactFlowProps = mockReactFlow.mock.calls[mockReactFlow.mock.calls.length - 1][0]; + expect(reactFlowProps.nodesDraggable).toBe(false); + expect(reactFlowProps.nodesConnectable).toBe(false); await waitFor(() => { expect(applyAutoLayoutSpy).toHaveBeenCalled(); @@ -145,9 +164,16 @@ describe("Diagram Component", () => { const canvas = screen.getByTestId("react-flow-canvas"); expect(canvas).toBeInTheDocument(); - // Note: When read-only class is not present, handles are visible via CSS - // and ReactFlow props nodesDraggable={true} and nodesConnectable={true} enable interaction. - // For full verification of handle visibility and interaction, use e2e tests. + // Wait for ReactFlow to be called + await waitFor(() => { + expect(ReactFlow).toHaveBeenCalled(); + }); + + // Verify that ReactFlow was called with nodesDraggable={true} and nodesConnectable={true} + const mockReactFlow = vi.mocked(ReactFlow); + const reactFlowProps = mockReactFlow.mock.calls[mockReactFlow.mock.calls.length - 1][0]; + expect(reactFlowProps.nodesDraggable).toBe(true); + expect(reactFlowProps.nodesConnectable).toBe(true); await waitFor(() => { expect(applyAutoLayoutSpy).toHaveBeenCalled(); From 2e86f14622bd554fb7198e02541e5ba884d42efd Mon Sep 17 00:00:00 2001 From: handreyrc Date: Fri, 5 Jun 2026 09:18:21 -0400 Subject: [PATCH 5/5] Fix review comments Signed-off-by: handreyrc --- .../tests/react-flow/diagram/Diagram.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx b/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx index 80422a8c..c93c7528 100644 --- a/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx @@ -30,7 +30,7 @@ vi.mock("@xyflow/react", async () => { return { ...actual, ReactFlow: vi.fn((props) => { - return
; + return
; }), }; }); @@ -143,7 +143,9 @@ describe("Diagram Component", () => { // Verify that ReactFlow was called with nodesDraggable={false} and nodesConnectable={false} const mockReactFlow = vi.mocked(ReactFlow); - const reactFlowProps = mockReactFlow.mock.calls[mockReactFlow.mock.calls.length - 1][0]; + const lastCall = mockReactFlow.mock.calls.at(-1); + expect(lastCall).toBeDefined(); + const reactFlowProps = lastCall![0]; expect(reactFlowProps.nodesDraggable).toBe(false); expect(reactFlowProps.nodesConnectable).toBe(false);