diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index cdcaac42..73489e0c 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -34,17 +34,18 @@ Enhancements or new features can also be proposed by opening an issue. Please de git checkout -b feature/your-feature-name ``` 1. Make your changes. -1. Commit your changes, including a descriptive commit message: +1. Commit your changes, including a descriptive commit message following the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) style. Example: ```shell - git commit -m "Short description of the changes (Issue # if present)" + git commit -m "feat(ui): description of a new ui feature (Issue # if present)" ``` + See the project's commit log for more examples. + 1. Push to your fork and submit a pull request: ```shell git push origin feature/your-feature-name ``` -1. Ensure the pull request description clearly describes the problem and solution. Include the issue number if - applicable. +1. Ensure the pull request description clearly describes the problem and solution. Include the issue number if applicable. diff --git a/ui/src/components/canvas/FlowCanvas.tsx b/ui/src/components/canvas/FlowCanvas.tsx index eaec61a6..2a6ab85a 100644 --- a/ui/src/components/canvas/FlowCanvas.tsx +++ b/ui/src/components/canvas/FlowCanvas.tsx @@ -67,7 +67,13 @@ const ErrorHandler = ({ message, callback }: ErrorHandlerProps) => { const acceptDroppedFile = (file: File, importFlow: (json: string) => void) => { const reader = new FileReader() reader.onload = (e) => { - e.target && importFlow(e.target.result as string) + try { + e.target && importFlow(e.target.result as string) + } catch (e) { + // TODO: Display an error pop-up on failed import + // https://github.com/OctoConsulting/keip-canvas/issues/7 + console.error((e as Error).message) + } } reader.readAsText(file) } diff --git a/ui/src/components/options-menu/OptionsMenu.tsx b/ui/src/components/options-menu/OptionsMenu.tsx index 2593889d..a60e144d 100644 --- a/ui/src/components/options-menu/OptionsMenu.tsx +++ b/ui/src/components/options-menu/OptionsMenu.tsx @@ -1,23 +1,39 @@ -import { OverflowMenu } from "@carbon/react" +import { OverflowMenu, OverflowMenuItem } from "@carbon/react" import { Menu } from "@carbon/react/icons" +import { useState } from "react" import ExportPng from "./ExportPng" import SaveDiagram from "./SaveDiagram" +import { ImportFlowModal } from "./modals/ImportFlowModal" const OptionsMenu = () => { + const [importFlowModalOpen, setImportFlowModalOpen] = useState(false) + return ( - } - size="lg" - flipped - > - {/* OverflowMenu does not play nice when OverflowMenuItems are not direct children. Custom components will need to use forwardRef to avoid errors. */} - - - + <> + } + size="lg" + flipped + > + {/* OverflowMenu does not play nice when OverflowMenuItems are not direct children. Custom components will need to use forwardRef to avoid errors. */} + + + setImportFlowModalOpen(true)} + /> + + + {/* Modals */} + + ) } diff --git a/ui/src/components/options-menu/modals/ImportFlowModal.tsx b/ui/src/components/options-menu/modals/ImportFlowModal.tsx new file mode 100644 index 00000000..c209bf03 --- /dev/null +++ b/ui/src/components/options-menu/modals/ImportFlowModal.tsx @@ -0,0 +1,91 @@ +import { Modal } from "@carbon/react" +import { InlineLoadingStatus } from "carbon-components-react" +import hljs from "highlight.js/lib/core" +import json from "highlight.js/lib/languages/json" +import { useState } from "react" +import { createPortal } from "react-dom" +import Editor from "react-simple-code-editor" +import { importFlowFromJson } from "../../../singletons/store/appActions" + +hljs.registerLanguage("json", json) + +interface ImportFlowModalProps { + open: boolean + setOpen: (open: boolean) => void +} + +interface JsonEditorProps { + content: string + setContent: (content: string) => void +} + +const getLoadingDescription = (status: InlineLoadingStatus) => { + switch (status) { + case "active": + return "importing JSON" + case "error": + return "Invalid Flow JSON" + case "inactive": + case "finished": + return "" + } +} + +const FlowJsonEditor = ({ content, setContent }: JsonEditorProps) => { + return ( +
+ setContent(code)} + highlight={(code) => hljs.highlight(code, { language: "json" }).value} + padding={16} + textareaClassName="options-modal__editor-textarea" + /> +
+ ) +} + +export const ImportFlowModal = ({ open, setOpen }: ImportFlowModalProps) => { + const [loadingStatus, setLoadingStatus] = + useState("inactive") + const [content, setContent] = useState("") + + const resetAndCloseModal = () => { + setOpen(false) + setLoadingStatus("inactive") + setContent("") + } + + const doImport = () => { + setLoadingStatus("active") + try { + importFlowFromJson(content) + resetAndCloseModal() + } catch { + setLoadingStatus("error") + return + } + } + + const updateContent = (content: string) => { + loadingStatus !== "inactive" && setLoadingStatus("inactive") + setContent(content) + } + + return createPortal( + + + , + document.body + ) +} diff --git a/ui/src/singletons/store/appActions.test.ts b/ui/src/singletons/store/appActions.test.ts index 39e277ce..bed41624 100644 --- a/ui/src/singletons/store/appActions.test.ts +++ b/ui/src/singletons/store/appActions.test.ts @@ -603,8 +603,7 @@ describe("import flow from an exported JSON file", () => { const flow = JSON.parse(validExportedFlow) as Record delete flow[key] - act(() => importFlowFromJson(JSON.stringify(flow))) - + expect(() => importFlowFromJson(JSON.stringify(flow))).toThrowError() expect(getNodesView()).toEqual(initialNodes) expect(getEdgesView()).toEqual(initialEdges) } diff --git a/ui/src/singletons/store/appActions.ts b/ui/src/singletons/store/appActions.ts index 33f4c318..9e27e197 100644 --- a/ui/src/singletons/store/appActions.ts +++ b/ui/src/singletons/store/appActions.ts @@ -198,12 +198,10 @@ export const importFlowFromJson = (json: string) => { importFlowFromObject(flow) } -// TODO: Should a failed import throw an error on failure instead (for an error pop-up)? export const importFlowFromObject = (flow: SerializedFlow) => { useAppStore.setState(() => { if (!isStoreType(flow)) { - console.error("Failed to import an EIP flow JSON. Malformed input") - return {} + throw new Error("Failed to import an EIP flow JSON. Malformed input") } // Maintain backwards compatibility with older exported formats diff --git a/ui/src/styles.scss b/ui/src/styles.scss index 01ba4dc9..400a4489 100644 --- a/ui/src/styles.scss +++ b/ui/src/styles.scss @@ -48,6 +48,28 @@ } } +$options-modal-editor-height: 30vh; + +.options-modal__editor { + background-color: themes.$layer-02; + overflow-y: auto; + height: $options-modal-editor-height; + + @include type.type-style("code-02"); +} + +.options-modal__editor > div { + min-height: $options-modal-editor-height; +} + +.options-modal__editor:focus-within { + outline: 2px solid themes.$border-interactive; +} + +.options-modal__editor-textarea:focus { + outline: none; +} + .canvas { width: 100%; height: 100%;