Skip to content

Commit 18dc28a

Browse files
authored
Fix/170 (#175)
* fix Signed-off-by: Aaron Chong <aaronchongth@gmail.com> * lint and rename functions Signed-off-by: Aaron Chong <aaronchongth@gmail.com> * cleanup loops Signed-off-by: Aaron Chong <aaronchongth@gmail.com> * make easier to review Signed-off-by: Aaron Chong <aaronchongth@gmail.com> * update frontend build, only mark edges from buffers with errors Signed-off-by: Aaron Chong <aaronchongth@gmail.com> * disable export button Signed-off-by: Aaron Chong <aaronchongth@gmail.com> * cleanup, strongly type maybeValid, use error theme color Signed-off-by: Aaron Chong <aaronchongth@gmail.com> * add docs Signed-off-by: Aaron Chong <aaronchongth@gmail.com> --------- Signed-off-by: Aaron Chong <aaronchongth@gmail.com>
1 parent 767ed53 commit 18dc28a

File tree

5 files changed

+97
-33
lines changed

5 files changed

+97
-33
lines changed

diagram-editor/dist.tar.gz

253 Bytes
Binary file not shown.

diagram-editor/frontend/command-panel.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface CommandPanelProps {
1313
onNodeChanges: (changes: NodeChange<DiagramEditorNode>[]) => void;
1414
onExportClick: () => void;
1515
onLoadDiagram: (jsonStr: string, filename: string) => void;
16+
enableExport: boolean;
1617
}
1718

1819
const VisuallyHiddenInput = styled('input')({
@@ -31,6 +32,7 @@ function CommandPanel({
3132
onNodeChanges,
3233
onExportClick,
3334
onLoadDiagram,
35+
enableExport,
3436
}: CommandPanelProps) {
3537
const theme = useTheme();
3638
const [openEditTemplatesDialog, setOpenEditTemplatesDialog] =
@@ -69,8 +71,13 @@ function CommandPanel({
6971
)}
7072
<AutoLayoutButton onNodeChanges={onNodeChanges} />
7173
{editorMode.mode === EditorMode.Normal && (
72-
<Tooltip title="Export Diagram">
73-
<Button onClick={onExportClick}>
74+
<Tooltip
75+
title={
76+
enableExport
77+
? 'Export Diagram'
78+
: 'Export Diagram (disabled)'}
79+
>
80+
<Button onClick={onExportClick} disabled={!enableExport}>
7481
<MaterialSymbol symbol="download" />
7582
</Button>
7683
</Tooltip>

diagram-editor/frontend/diagram-editor.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ function getChangeParentIdAndPosition(
115115
}
116116
}
117117

118+
export type MaybeValid = { ok: true } | { ok: false, errorMessage: string };
119+
118120
interface ProvidersProps {
119121
editorModeContext: UseEditorModeContext;
120122
loadContext: LoadContext | null;
@@ -510,12 +512,11 @@ function DiagramEditor() {
510512
const showErrorToast = React.useCallback((message: string) => {
511513
setErrorToast(message);
512514
setOpenErrorToast(true);
515+
setEnableExport(false);
513516
}, []);
514517
const [loadContext, setLoadContext] = React.useState<LoadContext | null>(
515518
null,
516519
);
517-
const [diagramProperties, setDiagramProperties] =
518-
React.useState<DiagramProperties>({});
519520
const [recentlyUsedFilename, setRecentlyUsedFilename] =
520521
React.useState<string | null>(null);
521522

@@ -524,9 +525,6 @@ function DiagramEditor() {
524525
try {
525526
const [diagram, { graph, isRestored }] = await loadDiagramJson(jsonStr);
526527
setLoadContext({ diagram });
527-
setDiagramProperties({
528-
description: diagram.description,
529-
input_examples: diagram.input_examples });
530528
// do not perform auto layout if the diagram is restored from previous state.
531529
if (!isRestored) {
532530
const changes = autoLayout(graph.nodes, graph.edges, LAYOUT_OPTIONS);
@@ -611,6 +609,8 @@ function DiagramEditor() {
611609
[showErrorToast, nodeManager, edges],
612610
);
613611

612+
const [enableExport, setEnableExport] = React.useState(true);
613+
614614
return (
615615
<Providers
616616
editorModeContext={[editorMode, updateEditorModeAction]}
@@ -748,6 +748,7 @@ function DiagramEditor() {
748748
[],
749749
)}
750750
onLoadDiagram={loadDiagram}
751+
enableExport={enableExport}
751752
/>
752753
{editorMode.mode === EditorMode.Template && (
753754
<Fab
@@ -838,6 +839,12 @@ function DiagramEditor() {
838839
(filename: string) => setRecentlyUsedFilename(filename)
839840
}
840841
onClose={() => setOpenExportDiagramDialog(false)}
842+
onValidDiagram={(maybeValid: MaybeValid) => {
843+
setEnableExport(maybeValid.ok);
844+
if (!maybeValid.ok) {
845+
showErrorToast(maybeValid.errorMessage);
846+
}
847+
}}
841848
/>
842849
</Suspense>
843850
</ReactFlow>

diagram-editor/frontend/export-diagram-dialog.tsx

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import { useRegistry } from './registry-provider';
2020
import { useTemplates } from './templates-provider';
2121
import { useEdges } from './use-edges';
2222
import { exportDiagram } from './utils/export-diagram';
23+
import { MaybeValid } from './diagram-editor';
2324
import { useDiagramProperties } from './diagram-properties-provider';
2425

2526
export interface ExportDiagramDialogProps {
2627
open: boolean;
2728
suggestedFilename: string | null;
2829
onExportedFilename: (filename: string) => void;
2930
onClose: () => void;
31+
onValidDiagram: (maybeValid: MaybeValid) => void;
3032
}
3133

3234
interface DialogData {
@@ -39,6 +41,7 @@ function ExportDiagramDialogInternal({
3941
suggestedFilename,
4042
onExportedFilename,
4143
onClose,
44+
onValidDiagram,
4245
}: ExportDiagramDialogProps) {
4346
const nodeManager = useNodeManager();
4447
const edges = useEdges();
@@ -49,34 +52,42 @@ function ExportDiagramDialogInternal({
4952
const theme = useTheme();
5053

5154
const dialogDataPromise = useMemo(async () => {
52-
const diagram = exportDiagram(registry, nodeManager, edges, templates, diagramProperties ?? {});
53-
if (loadContext?.diagram.extensions) {
54-
diagram.extensions = loadContext.diagram.extensions;
55-
}
56-
await saveState(diagram, {
57-
nodes: [...nodeManager.nodes],
58-
edges: [...edges],
59-
});
60-
const diagramJsonMin = JSON.stringify(diagram);
61-
// Compress the JSON string to Uint8Array
62-
const compressedData = deflateSync(strToU8(diagramJsonMin));
63-
// Convert Uint8Array to a binary string for btoa
64-
let binaryString = '';
65-
for (let i = 0; i < compressedData.length; i++) {
66-
binaryString += String.fromCharCode(compressedData[i]);
67-
}
68-
const base64Diagram = btoa(binaryString);
55+
try {
56+
const diagram = exportDiagram(registry, nodeManager, edges, templates, diagramProperties ?? {});
57+
if (loadContext?.diagram.extensions) {
58+
diagram.extensions = loadContext.diagram.extensions;
59+
}
60+
await saveState(diagram, {
61+
nodes: [...nodeManager.nodes],
62+
edges: [...edges],
63+
});
64+
const diagramJsonMin = JSON.stringify(diagram);
65+
// Compress the JSON string to Uint8Array
66+
const compressedData = deflateSync(strToU8(diagramJsonMin));
67+
// Convert Uint8Array to a binary string for btoa
68+
let binaryString = '';
69+
for (let i = 0; i < compressedData.length; i++) {
70+
binaryString += String.fromCharCode(compressedData[i]);
71+
}
72+
const base64Diagram = btoa(binaryString);
6973

70-
const shareLink = `${window.location.origin}${window.location.pathname}?diagram=${encodeURIComponent(base64Diagram)}`;
74+
const shareLink = `${window.location.origin}${window.location.pathname}?diagram=${encodeURIComponent(base64Diagram)}`;
7175

72-
const diagramJsonPretty = JSON.stringify(diagram, null, 2);
76+
const diagramJsonPretty = JSON.stringify(diagram, null, 2);
7377

74-
const dialogData = {
75-
shareLink,
76-
diagramJson: diagramJsonPretty,
77-
} satisfies DialogData;
78+
const dialogData = {
79+
shareLink,
80+
diagramJson: diagramJsonPretty,
81+
} satisfies DialogData;
7882

79-
return dialogData;
83+
onValidDiagram({ok: true});
84+
return dialogData;
85+
} catch (e) {
86+
onValidDiagram(
87+
{ok: false, errorMessage: `failed to export diagram: ${e}`}
88+
);
89+
return null;
90+
}
8091
}, [registry, nodeManager, edges, templates, loadContext, diagramProperties]);
8192

8293
const dialogData = use(dialogDataPromise);

diagram-editor/frontend/utils/export-diagram.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
isOperationNode,
1212
isScopeNode,
1313
} from '../nodes';
14+
import { useTheme } from '@mui/material';
1415
import type {
1516
BufferSelection,
1617
Diagram,
@@ -23,6 +24,22 @@ import { exhaustiveCheck } from './exhaustive-check';
2324
import { ROOT_NAMESPACE, splitNamespaces } from './namespace';
2425
import { isArrayBufferSelection, isKeyedBufferSelection } from './operation';
2526
import type { DiagramProperties } from '../diagram-properties-provider';
27+
import { useEdges } from '../use-edges';
28+
import { getConnectedEdges } from '@xyflow/react';
29+
30+
/**
31+
* Marks a diagram editor edge visibly to signify that there is an error. This
32+
* function must be called from a react component as it uses the theme palette.
33+
* @param edge A diagram editor edge.
34+
*/
35+
function markEdgeError(edge: DiagramEditorEdge) {
36+
const theme = useTheme();
37+
edge.style = { ...edge.style, stroke: theme.palette.error.main };
38+
}
39+
40+
function markEdgeNormal(edge: DiagramEditorEdge) {
41+
edge.style = { ...edge.style, stroke: undefined };
42+
}
2643

2744
interface SubOperations {
2845
start: NextOperation;
@@ -126,16 +143,36 @@ function syncBufferSelection(
126143
// check that the buffer selection is compatible
127144
if (edge.type === 'buffer' && edge.data.input?.type === 'bufferSeq') {
128145
if (!isArrayBufferSelection(bufferSelection)) {
146+
const edges = useEdges();
147+
getConnectedEdges([targetNode], edges)
148+
.filter(
149+
(edge) => edge.type === 'buffer' && edge.target === targetNode.id
150+
)
151+
.forEach((edge) => {
152+
markEdgeError(edge);
153+
});
129154
throw new Error(
130-
'a sequential buffer edge must be assigned to an array of buffers',
155+
'A sequential buffer edge must be assigned to an array of buffers. \
156+
Ensure that other buffer edges connected to the same target node have \
157+
the same slot type.',
131158
);
132159
}
133160
bufferSelection[edge.data.input.seq] = sourceNode.data.opId;
134161
}
135162
if (edge.type === 'buffer' && edge.data.input?.type === 'bufferKey') {
136163
if (!isKeyedBufferSelection(bufferSelection)) {
164+
const edges = useEdges();
165+
getConnectedEdges([targetNode], edges)
166+
.filter(
167+
(edge) => edge.type === 'buffer' && edge.target === targetNode.id
168+
)
169+
.forEach((edge) => {
170+
markEdgeError(edge);
171+
});
137172
throw new Error(
138-
'a keyed buffer edge must be assigned to a keyed buffer selection',
173+
'A keyed buffer edge must be assigned to a keyed buffer selection. \
174+
Ensure that other buffer edges connected to the same target node have \
175+
the same slot type.',
139176
);
140177
}
141178
bufferSelection[edge.data.input.key] = sourceNode.data.opId;
@@ -154,6 +191,8 @@ function syncBufferSelection(
154191
}
155192
}
156193
}
194+
195+
markEdgeNormal(edge);
157196
}
158197
}
159198

0 commit comments

Comments
 (0)