Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/serverless-workflow-diagram-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"start": "storybook dev -p 6006 --no-open",
"build:storybook": "pnpm clean:storybook && storybook build --output-dir ./dist-storybook"
},
"dependencies": {
"@xyflow/react": "catalog:"
},
"devDependencies": {
"@chromatic-com/storybook": "catalog:",
"@storybook/addon-a11y": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,45 @@
* limitations under the License.
*/

import type { CSSProperties } from "react";
import * as React from "react";
import { Diagram, DiagramRef } from "../react-flow/diagram/Diagram";

const clickmeBtnStyle: CSSProperties = {
border: "2px solid blue",
borderRadius: "10px",
fontSize: "large",
fontWeight: "500",
background: "blue",
color: "white",
/**
* DiagramEditor component API
*/
export type DiagramEditorRef = {
doSomething: () => void; // TODO: to be implemented, it is just a placeholder
};

export type DiagramEditorProps = {
content: string;
isReadOnly: boolean;
locale: string;
diagramEditorRef?: React.Ref<DiagramEditorRef>;
};

export const DiagramEditor = (props: DiagramEditorProps) => {
//TODO: Implement the actual component this is just a placeholder
export const DiagramEditor = ({ isReadOnly, locale, diagramEditorRef }: DiagramEditorProps) => {
Comment thread
handreyrc marked this conversation as resolved.
Outdated
// TODO: i18n
// TODO: store, context
// TODO: ErrorBounduary / fallback
Comment thread
handreyrc marked this conversation as resolved.
Outdated

// Refs
const diagramDivRef = React.useRef<HTMLDivElement>(null);
const diagramRef = React.useRef<DiagramRef>(null);
Comment thread
handreyrc marked this conversation as resolved.
Outdated

// Allow imperativelly controlling the Editor
Comment thread
handreyrc marked this conversation as resolved.
Outdated
React.useImperativeHandle(
diagramEditorRef,
() => ({
doSomething: () => {
// TODO: to be implemented, it is just a placeholder
},
}),
[],
);

return (
<>
<h1>Hello from DiagramEditor component!</h1>
<p>Read-only: {props.isReadOnly ? "true" : "false"}</p>
<p>Content: {props.content}</p>
<button style={clickmeBtnStyle} onClick={() => alert("Hello from Diagram!")}>
Click me!
</button>
<Diagram ref={diagramRef} divRef={diagramDivRef} />
</>
Comment thread
handreyrc marked this conversation as resolved.
Comment thread
handreyrc marked this conversation as resolved.
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
* limitations under the License.
*/

import { expect, describe, it } from "vitest";
.diagram-container {
height: 100%;
position: relative;
}

describe("MyComponent", () => {
it("Just a sample test", () => {
expect(true).toBeTruthy();
});
});
.diagram-background {
--xy-background-pattern-color: #ccc;
background-color: #E5E4E2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as React from "react";
import * as RF from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import "./Diagram.css";
Comment thread
handreyrc marked this conversation as resolved.

const FIT_VIEW_OPTIONS: RF.FitViewOptions = { maxZoom: 1, minZoom: 0.1, duration: 400 };

// TODO: Nodes and Edges are hardcoded for now to generate a renderable basic workflow
// It shall be replaced by the actual implementation based on graph structure
const initialNodes: RF.Node[] = [
{ id: "n1", position: { x: 100, y: 0 }, data: { label: "Node 1" } },
{ id: "n2", position: { x: 100, y: 100 }, data: { label: "Node 2" } },
{ id: "n3", position: { x: 0, y: 200 }, data: { label: "Node 3" } },
{ id: "n4", position: { x: 200, y: 200 }, data: { label: "Node 4" } },
{ id: "n5", position: { x: 100, y: 300 }, data: { label: "Node 5" } },
];
const initialEdges: RF.Edge[] = [
{ id: "n1-n2", source: "n1", target: "n2" },
{ id: "n2-n3", source: "n2", target: "n3" },
{ id: "n2-n4", source: "n2", target: "n4" },
{ id: "n3-n5", source: "n3", target: "n5" },
{ id: "n4-n5", source: "n4", target: "n5" },
];

/**
* Diagram component API
*/
export type DiagramRef = {
doSomething: () => void; // TODO: to be implemented, it is just a placeholder
};

export type DiagramProps = {
divRef?: React.RefObject<HTMLDivElement | null>;
ref?: React.Ref<DiagramRef>;
};

export const Diagram = ({ divRef, ref }: DiagramProps) => {
Comment thread
handreyrc marked this conversation as resolved.
const [minimapVisible, setMinimapVisible] = React.useState(false);

const [nodes, setNodes] = React.useState<RF.Node[]>(initialNodes);
const [edges, setEdges] = React.useState<RF.Edge[]>(initialEdges);

const onNodesChange = React.useCallback<RF.OnNodesChange>(
(changes) => setNodes((nodesSnapshot) => RF.applyNodeChanges(changes, nodesSnapshot)),
[],
);
const onEdgesChange = React.useCallback<RF.OnEdgesChange>(
(changes) => setEdges((edgesSnapshot) => RF.applyEdgeChanges(changes, edgesSnapshot)),
[],
);
const onConnect = React.useCallback<RF.OnConnect>(
Comment thread
handreyrc marked this conversation as resolved.
Outdated
(params) => setEdges((edgesSnapshot) => RF.addEdge(params, edgesSnapshot)),
[],
);

React.useImperativeHandle(
ref,
() => ({
doSomething: () => {
// TODO: to be implemented, it is just a placeholder
},
}),
[],
);

return (
<div ref={divRef} className={"diagram-container"} data-testid={"diagram-container"}>
<RF.ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
Comment thread
handreyrc marked this conversation as resolved.
Outdated
onlyRenderVisibleElements={true}
zoomOnDoubleClick={false}
elementsSelectable={true}
panOnScroll={true}
zoomOnScroll={false}
preventScrolling={true}
selectionOnDrag={true}
proOptions={{ hideAttribution: true }}
Comment thread
handreyrc marked this conversation as resolved.
Outdated
fitView
>
{minimapVisible && <RF.MiniMap pannable zoomable position={"top-right"} />}

<RF.Controls
fitViewOptions={FIT_VIEW_OPTIONS}
position={"bottom-right"}
showInteractive={false} // Remove lock screen from zoombar
>
{/* 3. Add custom button to Controls */}
Comment thread
handreyrc marked this conversation as resolved.
Outdated
<RF.ControlButton onClick={() => setMinimapVisible(!minimapVisible)}>M</RF.ControlButton>
</RF.Controls>
<RF.Background className="diagram-background" variant={RF.BackgroundVariant.Cross} />
</RF.ReactFlow>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ type Story = StoryObj<typeof meta>;
export const Component: Story = {
args: {
isReadOnly: true,
content: "Sample Content",
locale: "EN",
Comment thread
handreyrc marked this conversation as resolved.
Outdated
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import {
/** Primary UI component for user interaction */
export const DiagramEditor = ({ ...props }: DiagramEditorProps) => {
return (
<>
<SWDiagramEditor content={props.content} isReadOnly={props.isReadOnly} />
</>
<div style={{ height: "100vh" }}>
<SWDiagramEditor isReadOnly={props.isReadOnly} locale={props.locale} />
Comment thread
handreyrc marked this conversation as resolved.
Outdated
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,24 @@
import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/react-vite";
import * as stories from "../../stories/DiagramEditor.stories";
import userEvent from "@testing-library/user-event";
import { vi, test, expect, afterEach, describe } from "vitest";

// Composes all stories in the file
const { Component } = composeStories(stories);

describe("DiagramEditor component story", () => {
describe("Story - DiagramEditor component", () => {
afterEach(() => {
vi.restoreAllMocks();
});

test("Render DiagramEditor Component from story", async () => {
const content = "Sample Content";
test("Renders react flow Diagram component", async () => {
const locale = "EN";
const isReadOnly = true;
const alertMock = vi.spyOn(window, "alert").mockImplementation(() => {});

render(<Component content={content} isReadOnly={isReadOnly} />);
render(<Component locale={locale} isReadOnly={isReadOnly} />);

const user = userEvent.setup();
const button = screen.getByRole("button", { name: /Click me!/i });
const reactFlowContainer = screen.getByTestId("diagram-container");

await user.click(button);

expect(alertMock).toHaveBeenCalledWith("Hello from Diagram!");
expect(reactFlowContainer).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,21 @@

import { render, screen } from "@testing-library/react";
import { DiagramEditor } from "../../src/diagram-editor";
import userEvent from "@testing-library/user-event";
import { vi, test, expect, afterEach, describe } from "vitest";

describe("DiagramEditor Component", () => {
afterEach(() => {
vi.restoreAllMocks();
});

test("Render DiagramEditor Component", async () => {
const content = "Sample Content";
test("Renders react flow Diagram component", async () => {
const locale = "EN";
Comment thread
handreyrc marked this conversation as resolved.
Outdated
const isReadOnly = true;
const alertMock = vi.spyOn(window, "alert").mockImplementation(() => {});

render(<DiagramEditor content={content} isReadOnly={isReadOnly} />);
render(<DiagramEditor locale={locale} isReadOnly={isReadOnly} />);

const user = userEvent.setup();
const button = screen.getByRole("button", { name: /Click me!/i });
const reactFlowContainer = screen.getByTestId("diagram-container");

await user.click(button);

expect(alertMock).toHaveBeenCalledWith("Hello from Diagram!");
expect(reactFlowContainer).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { render, screen } from "@testing-library/react";
import { Diagram } from "../../../src/react-flow/diagram/Diagram";
import { vi, test, expect, afterEach, describe } from "vitest";

describe("Diagram Component", () => {
afterEach(() => {
vi.restoreAllMocks();
});

test("Renders react flow nodes", async () => {
render(<Diagram />);

const node1 = screen.getByText("Node 1");
const node2 = screen.getByText("Node 2");
const node3 = screen.getByText("Node 3");
const node4 = screen.getByText("Node 4");
const node5 = screen.getByText("Node 5");

expect(node1).toBeInTheDocument();
expect(node2).toBeInTheDocument();
expect(node3).toBeInTheDocument();
expect(node4).toBeInTheDocument();
expect(node5).toBeInTheDocument();
});
});
35 changes: 34 additions & 1 deletion packages/serverless-workflow-diagram-editor/tests/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,44 @@
* limitations under the License.
*/

import { afterEach } from "vitest";
import { afterEach, vi } from "vitest";
import { cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/vitest"; // This extends vitest's expect with jest-dom matchers

// Run cleanup after each test to unmount React components and clean up the DOM
afterEach(() => {
cleanup();
});

// Mock ResizeObserver
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
unobserve() {}
disconnect() {}
},
);

// Mock DOMMatrix (required for coordinate calculations)
vi.stubGlobal(
"DOMMatrixReadOnly",
class {
m22 = 1;
constructor(transform: string) {
/* logic to parse transform if needed */
}
},
);

Comment thread
handreyrc marked this conversation as resolved.
Outdated
// Mock PointerEvent (required for drag-and-drop actions)
if (!global.PointerEvent) {
vi.stubGlobal(
"PointerEvent",
class extends Event {
constructor(type: string, params: PointerEventInit = {}) {
super(type, params);
}
},
);
}
Loading
Loading