From 1bfe6d4f028af1c4a6845bb1a043174ba00ba82e Mon Sep 17 00:00:00 2001 From: handreyrc Date: Wed, 1 Apr 2026 14:08:33 -0400 Subject: [PATCH 1/4] Add ContextProvider, Context and Hook Signed-off-by: handreyrc --- .../src/diagram-editor/DiagramEditor.tsx | 10 +- .../src/react-flow/diagram/Diagram.tsx | 1 - .../store/DiagramEditorContextProvider.tsx | 49 ++++++++ .../src/store/diagramEditorContext.ts | 39 +++++++ .../DiagramEditorContextProvider.test.tsx | 109 ++++++++++++++++++ 5 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx create mode 100644 packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts create mode 100644 packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx diff --git a/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx b/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx index 8373227..4f6ba53 100644 --- a/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx +++ b/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx @@ -16,6 +16,7 @@ import * as React from "react"; import { Diagram, DiagramRef } from "../react-flow/diagram/Diagram"; +import { DiagramContextProvider } from "../store/DiagramEditorContextProvider"; /** * DiagramEditor component API @@ -30,9 +31,8 @@ export type DiagramEditorProps = { ref?: React.Ref; }; -export const DiagramEditor = ({ ref }: DiagramEditorProps) => { +export const DiagramEditor = (props: DiagramEditorProps) => { // TODO: i18n - // TODO: store, context // TODO: ErrorBoundary / fallback // Refs @@ -41,7 +41,7 @@ export const DiagramEditor = ({ ref }: DiagramEditorProps) => { // Allow imperatively controlling the Editor React.useImperativeHandle( - ref, + props.ref, () => ({ doSomething: () => { // TODO: to be implemented, it is just a placeholder @@ -52,7 +52,9 @@ export const DiagramEditor = ({ ref }: DiagramEditorProps) => { return ( <> - + + + ); }; 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 068a5f6..dee9349 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 @@ -52,7 +52,6 @@ export type DiagramProps = { export const Diagram = ({ divRef, ref }: DiagramProps) => { const [minimapVisible, setMinimapVisible] = React.useState(false); - const [nodes, setNodes] = React.useState(initialNodes); const [edges, setEdges] = React.useState(initialEdges); diff --git a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx new file mode 100644 index 0000000..23dd465 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx @@ -0,0 +1,49 @@ +/* + * 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 { DiagramEditorProps } from "../diagram-editor/DiagramEditor"; +import { DiagramEditorContext, DiagramEditorContextType } from "./diagramEditorContext"; + +export type ContextProviderProps = Omit; + +export const DiagramContextProvider = (props: React.PropsWithChildren) => { + // Initialize states with props values + const [isReadOnly, setIsReadOnly] = React.useState(props.isReadOnly); + const [locale, setLocale] = React.useState(props.locale); + + // Memoize context value to prevent unnecessary re-renders of consumers + const context = React.useMemo( + () => ({ + isReadOnly, + locale, + updateIsReadOnly: (isReadOnly: boolean) => + setIsReadOnly((prev) => (prev !== isReadOnly ? isReadOnly : prev)), + updateLocale: (locale: string) => setLocale((prev) => (prev !== locale ? locale : prev)), + }), + [isReadOnly, locale], + ); + + // Update states on props changes + React.useEffect(() => { + context.updateIsReadOnly(props.isReadOnly); + context.updateLocale(props.locale); + }, [props, context]); + + return ( + {props.children} + ); +}; diff --git a/packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts b/packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts new file mode 100644 index 0000000..3a4c1c4 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts @@ -0,0 +1,39 @@ +/* + * 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"; + +export type DiagramEditorContextType = { + isReadOnly: boolean; + locale: string; + + updateIsReadOnly: (isReadOnly: boolean) => void; + updateLocale: (locale: string) => void; +}; + +export const DiagramEditorContext = React.createContext( + undefined, +); + +export const useDiagramEditorContext = () => { + const context = React.useContext(DiagramEditorContext); + + if (context === undefined) { + throw new Error("useDiagramContext must be used within an DiagramContextProvider"); + } + + return context; +}; diff --git a/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx b/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx new file mode 100644 index 0000000..bc2b599 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx @@ -0,0 +1,109 @@ +/* + * 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 { render, screen } from "@testing-library/react"; +import { vi, test, expect, afterEach, describe } from "vitest"; +import { useDiagramEditorContext } from "../../src/store/diagramEditorContext"; +import { DiagramContextProvider } from "../../src/store/DiagramEditorContextProvider"; + +const TestComponent: React.FC = () => { + const { isReadOnly, locale } = useDiagramEditorContext(); + const renderCount = React.useRef(0); + + // Increments on every render cycle + renderCount.current++; + + return ( +
+

{`${isReadOnly}`}

+

{`${locale}`}

+

{`${renderCount.current}`}

+
+ ); +}; + +describe("DiagramContextProvider Component", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("Consume properties from context", async () => { + render( + + + , + ); + + const readOnlyElement = screen.getByTestId("test-read-only"); + const readOnlyLocale = screen.getByTestId("test-locale"); + const renderCount = screen.getByTestId("test-render"); + + expect(readOnlyElement).toHaveTextContent(/true/i); + expect(readOnlyLocale).toHaveTextContent(/en/i); + + // Only one rendering cycle is expected + expect(renderCount).toHaveTextContent(/1/i); + }); + + test("Context provider props changes shall cause internal component to reload", async () => { + const { rerender } = render( + + + , + ); + + rerender( + + + , + ); + + const readOnlyElementChanged = screen.getByTestId("test-read-only"); + const readOnlyLocaleChanged = screen.getByTestId("test-locale"); + const renderCount = screen.getByTestId("test-render"); + + expect(readOnlyElementChanged).toHaveTextContent(/false/i); + expect(readOnlyLocaleChanged).toHaveTextContent(/pt/i); + + // 3 rendering cycles are expected 1- fist render, 2- forced by rerender and 3- caused by state updates + expect(renderCount).toHaveTextContent(/3/i); + }); + + test("Context provider same props shall not cause internal component to reload", async () => { + const { rerender } = render( + + + , + ); + + rerender( + + + , + ); + + const readOnlyElementChanged = screen.getByTestId("test-read-only"); + const readOnlyLocaleChanged = screen.getByTestId("test-locale"); + const renderCount = screen.getByTestId("test-render"); + + expect(readOnlyElementChanged).toHaveTextContent(/true/i); + expect(readOnlyLocaleChanged).toHaveTextContent(/en/i); + + // 2 rendering cycles are expected 1- fist render and 2- forced by rerender + expect(renderCount).toHaveTextContent(/2/i); + }); +}); From 81ef8b518c9fde497f7726b6c54825a347ea13e9 Mon Sep 17 00:00:00 2001 From: handreyrc Date: Thu, 2 Apr 2026 09:40:28 -0400 Subject: [PATCH 2/4] Fix copilot complaints Signed-off-by: handreyrc --- .../src/diagram-editor/DiagramEditor.tsx | 6 ++--- .../store/DiagramEditorContextProvider.tsx | 4 +++- .../src/store/diagramEditorContext.ts | 2 +- .../DiagramEditorContextProvider.test.tsx | 24 +++++++++---------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx b/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx index 4f6ba53..8b6ba6b 100644 --- a/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx +++ b/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx @@ -16,7 +16,7 @@ import * as React from "react"; import { Diagram, DiagramRef } from "../react-flow/diagram/Diagram"; -import { DiagramContextProvider } from "../store/DiagramEditorContextProvider"; +import { DiagramEditorContextProvider } from "../store/DiagramEditorContextProvider"; /** * DiagramEditor component API @@ -52,9 +52,9 @@ export const DiagramEditor = (props: DiagramEditorProps) => { return ( <> - + - + ); }; diff --git a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx index 23dd465..2f50b4c 100644 --- a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx +++ b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx @@ -20,7 +20,9 @@ import { DiagramEditorContext, DiagramEditorContextType } from "./diagramEditorC export type ContextProviderProps = Omit; -export const DiagramContextProvider = (props: React.PropsWithChildren) => { +export const DiagramEditorContextProvider = ( + props: React.PropsWithChildren, +) => { // Initialize states with props values const [isReadOnly, setIsReadOnly] = React.useState(props.isReadOnly); const [locale, setLocale] = React.useState(props.locale); diff --git a/packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts b/packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts index 3a4c1c4..86e33a4 100644 --- a/packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts +++ b/packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts @@ -32,7 +32,7 @@ export const useDiagramEditorContext = () => { const context = React.useContext(DiagramEditorContext); if (context === undefined) { - throw new Error("useDiagramContext must be used within an DiagramContextProvider"); + throw new Error("useDiagramContext must be used within an DiagramEditorContextProvider"); } return context; diff --git a/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx b/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx index bc2b599..d63384f 100644 --- a/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx @@ -18,7 +18,7 @@ import * as React from "react"; import { render, screen } from "@testing-library/react"; import { vi, test, expect, afterEach, describe } from "vitest"; import { useDiagramEditorContext } from "../../src/store/diagramEditorContext"; -import { DiagramContextProvider } from "../../src/store/DiagramEditorContextProvider"; +import { DiagramEditorContextProvider } from "../../src/store/DiagramEditorContextProvider"; const TestComponent: React.FC = () => { const { isReadOnly, locale } = useDiagramEditorContext(); @@ -36,16 +36,16 @@ const TestComponent: React.FC = () => { ); }; -describe("DiagramContextProvider Component", () => { +describe("DiagramEditorContextProvider Component", () => { afterEach(() => { vi.restoreAllMocks(); }); test("Consume properties from context", async () => { render( - + - , + , ); const readOnlyElement = screen.getByTestId("test-read-only"); @@ -61,15 +61,15 @@ describe("DiagramContextProvider Component", () => { test("Context provider props changes shall cause internal component to reload", async () => { const { rerender } = render( - + - , + , ); rerender( - + - , + , ); const readOnlyElementChanged = screen.getByTestId("test-read-only"); @@ -85,15 +85,15 @@ describe("DiagramContextProvider Component", () => { test("Context provider same props shall not cause internal component to reload", async () => { const { rerender } = render( - + - , + , ); rerender( - + - , + , ); const readOnlyElementChanged = screen.getByTestId("test-read-only"); From 5892d8e001a99c977598660af870dda92a4bb803 Mon Sep 17 00:00:00 2001 From: handreyrc Date: Thu, 2 Apr 2026 10:45:47 -0400 Subject: [PATCH 3/4] Rename Context Component Signed-off-by: handreyrc --- .../store/{diagramEditorContext.ts => DiagramEditorContext.tsx} | 0 .../src/store/DiagramEditorContextProvider.tsx | 2 +- .../tests/store/DiagramEditorContextProvider.test.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/serverless-workflow-diagram-editor/src/store/{diagramEditorContext.ts => DiagramEditorContext.tsx} (100%) diff --git a/packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContext.tsx similarity index 100% rename from packages/serverless-workflow-diagram-editor/src/store/diagramEditorContext.ts rename to packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContext.tsx diff --git a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx index 2f50b4c..989a765 100644 --- a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx +++ b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx @@ -16,7 +16,7 @@ import * as React from "react"; import { DiagramEditorProps } from "../diagram-editor/DiagramEditor"; -import { DiagramEditorContext, DiagramEditorContextType } from "./diagramEditorContext"; +import { DiagramEditorContext, DiagramEditorContextType } from "./DiagramEditorContext"; export type ContextProviderProps = Omit; diff --git a/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx b/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx index d63384f..a4d1cfa 100644 --- a/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/store/DiagramEditorContextProvider.test.tsx @@ -17,7 +17,7 @@ import * as React from "react"; import { render, screen } from "@testing-library/react"; import { vi, test, expect, afterEach, describe } from "vitest"; -import { useDiagramEditorContext } from "../../src/store/diagramEditorContext"; +import { useDiagramEditorContext } from "../../src/store/DiagramEditorContext"; import { DiagramEditorContextProvider } from "../../src/store/DiagramEditorContextProvider"; const TestComponent: React.FC = () => { From 217af1bd932974d4074cadad31e7a8a267da14e8 Mon Sep 17 00:00:00 2001 From: handreyrc Date: Thu, 2 Apr 2026 11:43:01 -0400 Subject: [PATCH 4/4] Fix copilot complaints Signed-off-by: handreyrc --- .../src/store/DiagramEditorContext.tsx | 2 +- .../store/DiagramEditorContextProvider.tsx | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContext.tsx b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContext.tsx index 86e33a4..563317a 100644 --- a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContext.tsx +++ b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContext.tsx @@ -32,7 +32,7 @@ export const useDiagramEditorContext = () => { const context = React.useContext(DiagramEditorContext); if (context === undefined) { - throw new Error("useDiagramContext must be used within an DiagramEditorContextProvider"); + throw new Error("useDiagramEditorContext must be used within a DiagramEditorContextProvider"); } return context; diff --git a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx index 989a765..43a5ec5 100644 --- a/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx +++ b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx @@ -27,24 +27,31 @@ export const DiagramEditorContextProvider = ( const [isReadOnly, setIsReadOnly] = React.useState(props.isReadOnly); const [locale, setLocale] = React.useState(props.locale); + const updateIsReadOnly = React.useCallback((isReadOnly: boolean) => { + setIsReadOnly((prev) => (prev !== isReadOnly ? isReadOnly : prev)); + }, []); + + const updateLocale = React.useCallback((locale: string) => { + setLocale((prev) => (prev !== locale ? locale : prev)); + }, []); + + // Update states on props changes + React.useEffect(() => { + updateIsReadOnly(props.isReadOnly); + updateLocale(props.locale); + }, [props, updateIsReadOnly, updateLocale]); + // Memoize context value to prevent unnecessary re-renders of consumers const context = React.useMemo( () => ({ isReadOnly, locale, - updateIsReadOnly: (isReadOnly: boolean) => - setIsReadOnly((prev) => (prev !== isReadOnly ? isReadOnly : prev)), - updateLocale: (locale: string) => setLocale((prev) => (prev !== locale ? locale : prev)), + updateIsReadOnly, + updateLocale, }), - [isReadOnly, locale], + [isReadOnly, locale, updateIsReadOnly, updateLocale], ); - // Update states on props changes - React.useEffect(() => { - context.updateIsReadOnly(props.isReadOnly); - context.updateLocale(props.locale); - }, [props, context]); - return ( {props.children} );