Skip to content

Commit 9b35b49

Browse files
committed
fixes to exporting in preparation for exporting templates
Signed-off-by: Teo Koon Peng <[email protected]>
1 parent d08543a commit 9b35b49

10 files changed

+593
-827
lines changed

diagram-editor/frontend/command-panel.tsx

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,34 +47,34 @@ function CommandPanel({
4747
)}
4848
<AutoLayoutButton onNodeChanges={onNodeChanges} />
4949
{editorMode.mode === EditorMode.Normal && (
50-
<>
51-
<Tooltip title="Export Diagram">
52-
<Button onClick={onExportClick}>
53-
<MaterialSymbol symbol="download" />
54-
</Button>
55-
</Tooltip>
56-
<Tooltip title="Load Diagram">
57-
{/* biome-ignore lint/a11y/useValidAriaRole: button used as a label, should have no role */}
58-
<Button component="label" role={undefined}>
59-
<MaterialSymbol symbol="upload_file" />
60-
<VisuallyHiddenInput
61-
type="file"
62-
accept="application/json"
63-
aria-label="load diagram"
64-
onChange={async (ev) => {
65-
if (ev.target.files) {
66-
const json = await ev.target.files[0].text();
67-
onLoadDiagram(json);
68-
}
69-
}}
70-
onClick={(ev) => {
71-
// Reset the input value so that the same file can be loaded multiple times
72-
(ev.target as HTMLInputElement).value = '';
73-
}}
74-
/>
75-
</Button>
76-
</Tooltip>
77-
</>
50+
<Tooltip title="Export Diagram">
51+
<Button onClick={onExportClick}>
52+
<MaterialSymbol symbol="download" />
53+
</Button>
54+
</Tooltip>
55+
)}
56+
{editorMode.mode === EditorMode.Normal && (
57+
<Tooltip title="Load Diagram">
58+
{/* biome-ignore lint/a11y/useValidAriaRole: button used as a label, should have no role */}
59+
<Button component="label" role={undefined}>
60+
<MaterialSymbol symbol="upload_file" />
61+
<VisuallyHiddenInput
62+
type="file"
63+
accept="application/json"
64+
aria-label="load diagram"
65+
onChange={async (ev) => {
66+
if (ev.target.files) {
67+
const json = await ev.target.files[0].text();
68+
onLoadDiagram(json);
69+
}
70+
}}
71+
onClick={(ev) => {
72+
// Reset the input value so that the same file can be loaded multiple times
73+
(ev.target as HTMLInputElement).value = '';
74+
}}
75+
/>
76+
</Button>
77+
</Tooltip>
7878
)}
7979
</ButtonGroup>
8080
</Panel>

diagram-editor/frontend/diagram-editor.tsx

Lines changed: 147 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import {
22
Alert,
33
alpha,
44
darken,
5+
Fab,
56
Popover,
67
type PopoverPosition,
78
type PopoverProps,
89
Snackbar,
10+
Typography,
911
useTheme,
1012
} from '@mui/material';
1113
import {
@@ -17,6 +19,7 @@ import {
1719
type EdgeRemoveChange,
1820
type NodeChange,
1921
type NodeRemoveChange,
22+
Panel,
2023
ReactFlow,
2124
type ReactFlowInstance,
2225
reconnectEdge,
@@ -35,6 +38,7 @@ import EditScopeForm from './forms/edit-scope-form';
3538
import {
3639
type DiagramEditorNode,
3740
isOperationNode,
41+
MaterialSymbol,
3842
NODE_TYPES,
3943
type OperationNode,
4044
} from './nodes';
@@ -94,7 +98,7 @@ const DiagramEditor = () => {
9498
DiagramEditorEdge
9599
> | null>(null);
96100

97-
const [editorMode] = useEditorMode();
101+
const [editorMode, setEditorMode] = useEditorMode();
98102

99103
const [nodes, setNodes] = React.useState<DiagramEditorNode[]>(
100104
() => loadEmpty().nodes,
@@ -454,146 +458,159 @@ const DiagramEditor = () => {
454458
}, []);
455459

456460
return (
457-
<>
458-
<ReactFlow
459-
nodes={renderedNodes}
460-
edges={renderedEdges}
461-
fitView
462-
fitViewOptions={{ padding: 0.2 }}
463-
nodeTypes={NODE_TYPES}
464-
edgeTypes={EDGE_TYPES}
465-
onInit={(instance) => {
466-
reactFlowInstance.current = instance;
467-
468-
const queryParams = new URLSearchParams(window.location.search);
469-
const diagramParam = queryParams.get('diagram');
470-
471-
if (!diagramParam) {
472-
return;
473-
}
461+
<ReactFlow
462+
nodes={renderedNodes}
463+
edges={renderedEdges}
464+
fitView
465+
fitViewOptions={{ padding: 0.2 }}
466+
nodeTypes={NODE_TYPES}
467+
edgeTypes={EDGE_TYPES}
468+
onInit={(instance) => {
469+
reactFlowInstance.current = instance;
470+
471+
const queryParams = new URLSearchParams(window.location.search);
472+
const diagramParam = queryParams.get('diagram');
473+
474+
if (!diagramParam) {
475+
return;
476+
}
474477

475-
try {
476-
const binaryString = atob(diagramParam);
477-
const byteArray = new Uint8Array(binaryString.length);
478-
for (let i = 0; i < binaryString.length; i++) {
479-
byteArray[i] = binaryString.charCodeAt(i);
480-
}
481-
const diagramJson = strFromU8(inflateSync(byteArray));
482-
loadDiagram(diagramJson);
483-
} catch (e) {
484-
if (e instanceof Error) {
485-
showErrorToast(`failed to load diagram: ${e.message}`);
486-
} else {
487-
throw e;
488-
}
478+
try {
479+
const binaryString = atob(diagramParam);
480+
const byteArray = new Uint8Array(binaryString.length);
481+
for (let i = 0; i < binaryString.length; i++) {
482+
byteArray[i] = binaryString.charCodeAt(i);
489483
}
490-
}}
491-
onNodesChange={handleNodeChanges}
492-
onNodesDelete={() => {
493-
closeAllPopovers();
494-
}}
495-
onEdgesChange={handleEdgeChanges}
496-
onEdgesDelete={() => {
497-
closeAllPopovers();
498-
}}
499-
onConnect={(conn) => {
500-
const sourceNode = nodes.find((n) => n.id === conn.source);
501-
const targetNode = nodes.find((n) => n.id === conn.target);
502-
if (!sourceNode || !targetNode) {
503-
throw new Error('cannot find source or target node');
504-
}
505-
506-
const allowedEdges = getAllowEdges(sourceNode, targetNode);
507-
if (allowedEdges.length === 0) {
508-
showErrorToast(
509-
`cannot connect "${sourceNode.type}" to "${targetNode.type}"`,
510-
);
511-
return;
484+
const diagramJson = strFromU8(inflateSync(byteArray));
485+
loadDiagram(diagramJson);
486+
} catch (e) {
487+
if (e instanceof Error) {
488+
showErrorToast(`failed to load diagram: ${e.message}`);
489+
} else {
490+
throw e;
512491
}
492+
}
493+
}}
494+
onNodesChange={handleNodeChanges}
495+
onNodesDelete={() => {
496+
closeAllPopovers();
497+
}}
498+
onEdgesChange={handleEdgeChanges}
499+
onEdgesDelete={() => {
500+
closeAllPopovers();
501+
}}
502+
onConnect={(conn) => {
503+
const sourceNode = nodes.find((n) => n.id === conn.source);
504+
const targetNode = nodes.find((n) => n.id === conn.target);
505+
if (!sourceNode || !targetNode) {
506+
throw new Error('cannot find source or target node');
507+
}
513508

514-
setEdges((prev) =>
515-
addEdge(
516-
{
517-
...conn,
518-
type: allowedEdges[0],
519-
data: defaultEdgeData(allowedEdges[0]),
520-
},
521-
prev,
522-
),
509+
const allowedEdges = getAllowEdges(sourceNode, targetNode);
510+
if (allowedEdges.length === 0) {
511+
showErrorToast(
512+
`cannot connect "${sourceNode.type}" to "${targetNode.type}"`,
523513
);
524-
}}
525-
onReconnect={(oldEdge, newConnection) =>
526-
setEdges((prev) => reconnectEdge(oldEdge, newConnection, prev))
514+
return;
527515
}
528-
onNodeClick={(ev, node) => {
529-
ev.stopPropagation();
530-
closeAllPopovers();
531516

532-
if (!isOperationNode(node)) {
533-
return;
534-
}
535-
setEditingNodeId(node.id);
517+
setEdges((prev) =>
518+
addEdge(
519+
{
520+
...conn,
521+
type: allowedEdges[0],
522+
data: defaultEdgeData(allowedEdges[0]),
523+
},
524+
prev,
525+
),
526+
);
527+
}}
528+
onReconnect={(oldEdge, newConnection) =>
529+
setEdges((prev) => reconnectEdge(oldEdge, newConnection, prev))
530+
}
531+
onNodeClick={(ev, node) => {
532+
ev.stopPropagation();
533+
closeAllPopovers();
536534

537-
setEditOpFormPopoverProps({
538-
open: true,
539-
anchorReference: 'anchorPosition',
540-
anchorPosition: { left: ev.clientX, top: ev.clientY },
541-
});
542-
}}
543-
onEdgeClick={(ev, edge) => {
544-
ev.stopPropagation();
545-
closeAllPopovers();
535+
if (!isOperationNode(node)) {
536+
return;
537+
}
538+
setEditingNodeId(node.id);
539+
540+
setEditOpFormPopoverProps({
541+
open: true,
542+
anchorReference: 'anchorPosition',
543+
anchorPosition: { left: ev.clientX, top: ev.clientY },
544+
});
545+
}}
546+
onEdgeClick={(ev, edge) => {
547+
ev.stopPropagation();
548+
closeAllPopovers();
546549

547-
const sourceNode = nodes.find((n) => n.id === edge.source);
548-
const targetNode = nodes.find((n) => n.id === edge.target);
549-
if (!sourceNode || !targetNode) {
550-
throw new Error('unable to find source or target node');
551-
}
550+
const sourceNode = nodes.find((n) => n.id === edge.source);
551+
const targetNode = nodes.find((n) => n.id === edge.target);
552+
if (!sourceNode || !targetNode) {
553+
throw new Error('unable to find source or target node');
554+
}
552555

553-
setEditingEdgeId(edge.id);
556+
setEditingEdgeId(edge.id);
554557

555-
setEditOpFormPopoverProps({
556-
open: true,
557-
anchorReference: 'anchorPosition',
558-
anchorPosition: { left: ev.clientX, top: ev.clientY },
559-
});
560-
}}
561-
onPaneClick={(ev) => {
562-
if (addOperationPopover.open || editOpFormPopoverProps.open) {
563-
closeAllPopovers();
564-
return;
565-
}
558+
setEditOpFormPopoverProps({
559+
open: true,
560+
anchorReference: 'anchorPosition',
561+
anchorPosition: { left: ev.clientX, top: ev.clientY },
562+
});
563+
}}
564+
onPaneClick={(ev) => {
565+
if (addOperationPopover.open || editOpFormPopoverProps.open) {
566+
closeAllPopovers();
567+
return;
568+
}
566569

567-
// filter out erroneous click after connecting an edge
568-
const now = Date.now();
569-
if (now - mouseDownTime.current > 200) {
570-
return;
571-
}
572-
setAddOperationPopover({
573-
open: true,
574-
popOverPosition: { left: ev.clientX, top: ev.clientY },
575-
parentId: null,
576-
});
577-
}}
578-
onMouseDownCapture={handleMouseDown}
579-
onTouchStartCapture={handleMouseDown}
580-
colorMode="dark"
581-
deleteKeyCode={'Delete'}
582-
>
583-
<Background
584-
bgColor={backgroundColor}
585-
color={alpha(theme.palette.text.primary, 0.3)}
586-
gap={30}
587-
/>
588-
<CommandPanel
589-
onNodeChanges={handleNodeChanges}
590-
onExportClick={React.useCallback(
591-
() => setOpenExportDiagramDialog(true),
592-
[],
593-
)}
594-
onLoadDiagram={loadDiagram}
595-
/>
596-
</ReactFlow>
570+
// filter out erroneous click after connecting an edge
571+
const now = Date.now();
572+
if (now - mouseDownTime.current > 200) {
573+
return;
574+
}
575+
setAddOperationPopover({
576+
open: true,
577+
popOverPosition: { left: ev.clientX, top: ev.clientY },
578+
parentId: null,
579+
});
580+
}}
581+
onMouseDownCapture={handleMouseDown}
582+
onTouchStartCapture={handleMouseDown}
583+
colorMode="dark"
584+
deleteKeyCode={'Delete'}
585+
>
586+
<Background
587+
bgColor={backgroundColor}
588+
color={alpha(theme.palette.text.primary, 0.3)}
589+
gap={30}
590+
/>
591+
{editorMode.mode === EditorMode.Template && (
592+
<Panel position="top-left">
593+
<Typography variant="h4">{editorMode.templateId}</Typography>
594+
</Panel>
595+
)}
596+
<CommandPanel
597+
onNodeChanges={handleNodeChanges}
598+
onExportClick={React.useCallback(
599+
() => setOpenExportDiagramDialog(true),
600+
[],
601+
)}
602+
onLoadDiagram={loadDiagram}
603+
/>
604+
{editorMode.mode === EditorMode.Template && (
605+
<Fab
606+
color="primary"
607+
aria-label="Save"
608+
sx={{ position: 'absolute', right: 64, bottom: 64 }}
609+
onClick={() => setEditorMode({ mode: EditorMode.Normal })}
610+
>
611+
<MaterialSymbol symbol="check" />
612+
</Fab>
613+
)}
597614
<Popover
598615
open={addOperationPopover.open}
599616
onClose={() =>
@@ -656,7 +673,7 @@ const DiagramEditor = () => {
656673
nodes={nodes}
657674
edges={edges}
658675
/>
659-
</>
676+
</ReactFlow>
660677
);
661678
};
662679

0 commit comments

Comments
 (0)