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..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,6 +16,7 @@ import * as React from "react"; import { Diagram, DiagramRef } from "../react-flow/diagram/Diagram"; +import { DiagramEditorContextProvider } 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/DiagramEditorContext.tsx b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContext.tsx new file mode 100644 index 0000000..563317a --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContext.tsx @@ -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("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 new file mode 100644 index 0000000..43a5ec5 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/store/DiagramEditorContextProvider.tsx @@ -0,0 +1,58 @@ +/* + * 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 DiagramEditorContextProvider = ( + props: React.PropsWithChildren, +) => { + // Initialize states with props values + 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, + updateLocale, + }), + [isReadOnly, locale, updateIsReadOnly, updateLocale], + ); + + return ( + {props.children} + ); +}; 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..a4d1cfa --- /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 { DiagramEditorContextProvider } 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("DiagramEditorContextProvider 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); + }); +});