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%;