diff --git a/.changeset/read-only-mode.md b/.changeset/read-only-mode.md
new file mode 100644
index 00000000..e1c48b20
--- /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.css b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css
index 04188fdf..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
@@ -56,6 +56,15 @@
.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;
+ width: 0 !important;
+ height: 0 !important;
+ min-width: 0 !important;
+ min-height: 0 !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..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
@@ -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..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
@@ -21,9 +21,49 @@ 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
+ * @param options.isReadOnly - Whether the diagram should be in read-only mode
+ * @param options.content - The workflow content to render
+ * @param options.locale - The locale to use for i18n
+ */
+function renderDiagram({
+ isReadOnly = true,
+ content = "",
+ locale = "en",
+}: {
+ isReadOnly?: boolean;
+ content?: string;
+ locale?: string;
+} = {}) {
+ return render(
+
+
+
+
+
+
+
+
+ ,
+ );
+}
+
describe("Diagram Component", () => {
let applyAutoLayoutSpy: ReturnType;
@@ -33,6 +73,9 @@ describe("Diagram Component", () => {
nodes: [],
edges: [],
});
+
+ // Clear mock calls before each test
+ vi.mocked(ReactFlow).mockClear();
});
afterEach(() => {
@@ -40,17 +83,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");
@@ -63,4 +96,89 @@ describe("Diagram Component", () => {
expect(applyAutoLayoutSpy).toHaveBeenCalled();
});
});
+
+ it("should apply read-only class when isReadOnly is true", async () => {
+ renderDiagram({ isReadOnly: true });
+
+ 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 () => {
+ renderDiagram({ isReadOnly: false });
+
+ 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 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
+ // This class applies CSS rule: .read-only .react-flow__handle { visibility: hidden !important; }
+ expect(diagram).toHaveClass("read-only");
+
+ // Verify ReactFlow canvas is rendered
+ const canvas = screen.getByTestId("react-flow-canvas");
+ expect(canvas).toBeInTheDocument();
+
+ // 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 lastCall = mockReactFlow.mock.calls.at(-1);
+ expect(lastCall).toBeDefined();
+ const reactFlowProps = lastCall![0];
+ expect(reactFlowProps.nodesDraggable).toBe(false);
+ expect(reactFlowProps.nodesConnectable).toBe(false);
+
+ await waitFor(() => {
+ expect(applyAutoLayoutSpy).toHaveBeenCalled();
+ });
+ });
+
+ 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();
+
+ // 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();
+ });
+ });
});