Skip to content

Commit 69c3b20

Browse files
authored
feat(ui): add a modal for importing flow JSON strings (#5)
1 parent 4900448 commit 69c3b20

File tree

7 files changed

+157
-24
lines changed

7 files changed

+157
-24
lines changed

docs/CONTRIBUTING.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,18 @@ Enhancements or new features can also be proposed by opening an issue. Please de
3434
git checkout -b feature/your-feature-name
3535
```
3636
1. Make your changes.
37-
1. Commit your changes, including a descriptive commit message:
37+
1. Commit your changes, including a descriptive commit message following the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) style. Example:
3838

3939
```shell
40-
git commit -m "Short description of the changes (Issue # if present)"
40+
git commit -m "feat(ui): description of a new ui feature (Issue # if present)"
4141
```
4242

43+
See the project's commit log for more examples.
44+
4345
1. Push to your fork and submit a pull request:
4446

4547
```shell
4648
git push origin feature/your-feature-name
4749
```
4850

49-
1. Ensure the pull request description clearly describes the problem and solution. Include the issue number if
50-
applicable.
51+
1. Ensure the pull request description clearly describes the problem and solution. Include the issue number if applicable.

ui/src/components/canvas/FlowCanvas.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,13 @@ const ErrorHandler = ({ message, callback }: ErrorHandlerProps) => {
6767
const acceptDroppedFile = (file: File, importFlow: (json: string) => void) => {
6868
const reader = new FileReader()
6969
reader.onload = (e) => {
70-
e.target && importFlow(e.target.result as string)
70+
try {
71+
e.target && importFlow(e.target.result as string)
72+
} catch (e) {
73+
// TODO: Display an error pop-up on failed import
74+
// https://github.com/OctoConsulting/keip-canvas/issues/7
75+
console.error((e as Error).message)
76+
}
7177
}
7278
reader.readAsText(file)
7379
}

ui/src/components/options-menu/OptionsMenu.tsx

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
1-
import { OverflowMenu } from "@carbon/react"
1+
import { OverflowMenu, OverflowMenuItem } from "@carbon/react"
22
import { Menu } from "@carbon/react/icons"
3+
import { useState } from "react"
34
import ExportPng from "./ExportPng"
45
import SaveDiagram from "./SaveDiagram"
6+
import { ImportFlowModal } from "./modals/ImportFlowModal"
57

68
const OptionsMenu = () => {
9+
const [importFlowModalOpen, setImportFlowModalOpen] = useState(false)
10+
711
return (
8-
<OverflowMenu
9-
className="options-menu"
10-
aria-label="options-menu"
11-
iconDescription="options"
12-
align="bottom-end"
13-
renderIcon={() => <Menu size={24} />}
14-
size="lg"
15-
flipped
16-
>
17-
{/* OverflowMenu does not play nice when OverflowMenuItems are not direct children. Custom components will need to use forwardRef to avoid errors. */}
18-
<SaveDiagram />
19-
<ExportPng />
20-
</OverflowMenu>
12+
<>
13+
<OverflowMenu
14+
className="options-menu"
15+
aria-label="options-menu"
16+
iconDescription="options"
17+
align="bottom-end"
18+
renderIcon={() => <Menu size={24} />}
19+
size="lg"
20+
flipped
21+
>
22+
{/* OverflowMenu does not play nice when OverflowMenuItems are not direct children. Custom components will need to use forwardRef to avoid errors. */}
23+
<SaveDiagram />
24+
<ExportPng />
25+
<OverflowMenuItem
26+
itemText="Import Flow JSON"
27+
onClick={() => setImportFlowModalOpen(true)}
28+
/>
29+
</OverflowMenu>
30+
31+
{/* Modals */}
32+
<ImportFlowModal
33+
open={importFlowModalOpen}
34+
setOpen={setImportFlowModalOpen}
35+
/>
36+
</>
2137
)
2238
}
2339

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Modal } from "@carbon/react"
2+
import { InlineLoadingStatus } from "carbon-components-react"
3+
import hljs from "highlight.js/lib/core"
4+
import json from "highlight.js/lib/languages/json"
5+
import { useState } from "react"
6+
import { createPortal } from "react-dom"
7+
import Editor from "react-simple-code-editor"
8+
import { importFlowFromJson } from "../../../singletons/store/appActions"
9+
10+
hljs.registerLanguage("json", json)
11+
12+
interface ImportFlowModalProps {
13+
open: boolean
14+
setOpen: (open: boolean) => void
15+
}
16+
17+
interface JsonEditorProps {
18+
content: string
19+
setContent: (content: string) => void
20+
}
21+
22+
const getLoadingDescription = (status: InlineLoadingStatus) => {
23+
switch (status) {
24+
case "active":
25+
return "importing JSON"
26+
case "error":
27+
return "Invalid Flow JSON"
28+
case "inactive":
29+
case "finished":
30+
return ""
31+
}
32+
}
33+
34+
const FlowJsonEditor = ({ content, setContent }: JsonEditorProps) => {
35+
return (
36+
<div className="options-modal__editor">
37+
<Editor
38+
value={content}
39+
onValueChange={(code) => setContent(code)}
40+
highlight={(code) => hljs.highlight(code, { language: "json" }).value}
41+
padding={16}
42+
textareaClassName="options-modal__editor-textarea"
43+
/>
44+
</div>
45+
)
46+
}
47+
48+
export const ImportFlowModal = ({ open, setOpen }: ImportFlowModalProps) => {
49+
const [loadingStatus, setLoadingStatus] =
50+
useState<InlineLoadingStatus>("inactive")
51+
const [content, setContent] = useState("")
52+
53+
const resetAndCloseModal = () => {
54+
setOpen(false)
55+
setLoadingStatus("inactive")
56+
setContent("")
57+
}
58+
59+
const doImport = () => {
60+
setLoadingStatus("active")
61+
try {
62+
importFlowFromJson(content)
63+
resetAndCloseModal()
64+
} catch {
65+
setLoadingStatus("error")
66+
return
67+
}
68+
}
69+
70+
const updateContent = (content: string) => {
71+
loadingStatus !== "inactive" && setLoadingStatus("inactive")
72+
setContent(content)
73+
}
74+
75+
return createPortal(
76+
<Modal
77+
open={open}
78+
onRequestClose={resetAndCloseModal}
79+
size="md"
80+
modalHeading="Import a Flow JSON"
81+
primaryButtonText="Import"
82+
secondaryButtonText="Cancel"
83+
loadingStatus={loadingStatus}
84+
loadingDescription={getLoadingDescription(loadingStatus)}
85+
onRequestSubmit={doImport}
86+
>
87+
<FlowJsonEditor content={content} setContent={updateContent} />
88+
</Modal>,
89+
document.body
90+
)
91+
}

ui/src/singletons/store/appActions.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -603,8 +603,7 @@ describe("import flow from an exported JSON file", () => {
603603
const flow = JSON.parse(validExportedFlow) as Record<string, object>
604604
delete flow[key]
605605

606-
act(() => importFlowFromJson(JSON.stringify(flow)))
607-
606+
expect(() => importFlowFromJson(JSON.stringify(flow))).toThrowError()
608607
expect(getNodesView()).toEqual(initialNodes)
609608
expect(getEdgesView()).toEqual(initialEdges)
610609
}

ui/src/singletons/store/appActions.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,10 @@ export const importFlowFromJson = (json: string) => {
198198
importFlowFromObject(flow)
199199
}
200200

201-
// TODO: Should a failed import throw an error on failure instead (for an error pop-up)?
202201
export const importFlowFromObject = (flow: SerializedFlow) => {
203202
useAppStore.setState(() => {
204203
if (!isStoreType(flow)) {
205-
console.error("Failed to import an EIP flow JSON. Malformed input")
206-
return {}
204+
throw new Error("Failed to import an EIP flow JSON. Malformed input")
207205
}
208206

209207
// Maintain backwards compatibility with older exported formats

ui/src/styles.scss

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@
4848
}
4949
}
5050

51+
$options-modal-editor-height: 30vh;
52+
53+
.options-modal__editor {
54+
background-color: themes.$layer-02;
55+
overflow-y: auto;
56+
height: $options-modal-editor-height;
57+
58+
@include type.type-style("code-02");
59+
}
60+
61+
.options-modal__editor > div {
62+
min-height: $options-modal-editor-height;
63+
}
64+
65+
.options-modal__editor:focus-within {
66+
outline: 2px solid themes.$border-interactive;
67+
}
68+
69+
.options-modal__editor-textarea:focus {
70+
outline: none;
71+
}
72+
5173
.canvas {
5274
width: 100%;
5375
height: 100%;

0 commit comments

Comments
 (0)