mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
fix: Small race conditions in diagrams.net integration (#11458)
This commit is contained in:
@@ -12,6 +12,16 @@ import {
|
||||
} from "../lib/DiagramsNetClient";
|
||||
import { sanitizeUrl } from "../../utils/urls";
|
||||
|
||||
/**
|
||||
* Tracks the mutable state for a single diagram editing session. Callbacks
|
||||
* close over a session object so that concurrent or overlapping sessions
|
||||
* do not interfere with each other.
|
||||
*/
|
||||
interface DiagramSession {
|
||||
/** The current src used to locate the node in the document. Updated after each successful export. */
|
||||
nodeSrc: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An editor extension that adds commands to insert and edit diagrams using diagrams.net.
|
||||
*
|
||||
@@ -27,6 +37,10 @@ export default class Diagrams extends Extension {
|
||||
commands(): Record<string, CommandFactory> {
|
||||
return {
|
||||
editDiagram: (): Command => (state, dispatch) => {
|
||||
if (!dispatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selectedNode = this.getSelectedImageNode(state);
|
||||
|
||||
if (!selectedNode) {
|
||||
@@ -61,7 +75,10 @@ export default class Diagrams extends Extension {
|
||||
* @param state - the editor state.
|
||||
* @param dispatch - the dispatch function.
|
||||
*/
|
||||
private insertEmptyDiagram(state: EditorState, dispatch?: any) {
|
||||
private insertEmptyDiagram(
|
||||
state: EditorState,
|
||||
dispatch: (tr: ReturnType<EditorState["tr"]["insert"]>) => void
|
||||
) {
|
||||
const type = this.editor.schema.nodes.image;
|
||||
const { tr } = state;
|
||||
const transaction = tr.insert(
|
||||
@@ -71,7 +88,7 @@ export default class Diagrams extends Extension {
|
||||
source: ImageSource.DiagramsNet,
|
||||
})
|
||||
);
|
||||
dispatch?.(transaction);
|
||||
dispatch(transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,18 +97,21 @@ export default class Diagrams extends Extension {
|
||||
* @param node - the selected image node, if any.
|
||||
*/
|
||||
private openDiagramEditor(node?: Node) {
|
||||
this.currentNodeSrc = node?.attrs.src ?? "";
|
||||
const sourceUrl = this.currentNodeSrc || EMPTY_DIAGRAM_IMAGE;
|
||||
const nodeSrc = node?.attrs.src ?? "";
|
||||
const sourceUrl = nodeSrc || EMPTY_DIAGRAM_IMAGE;
|
||||
|
||||
// Create a per-session object. Async callbacks close over this object so
|
||||
// that a second editing session does not clobber the first session's state.
|
||||
const session: DiagramSession = { nodeSrc };
|
||||
|
||||
// Clean up any existing client
|
||||
if (this.client) {
|
||||
this.client.close();
|
||||
}
|
||||
|
||||
// Create new client with callbacks
|
||||
this.client = new DiagramsNetClient(
|
||||
() => this.onDiagramReady(sourceUrl),
|
||||
(base64Data) => this.onDiagramExported(base64Data)
|
||||
(client) => this.onDiagramReady(client, sourceUrl),
|
||||
(base64Data) => this.onDiagramExported(base64Data, session)
|
||||
);
|
||||
|
||||
this.client.open(this.getDiagramsNetUrl());
|
||||
@@ -100,9 +120,10 @@ export default class Diagrams extends Extension {
|
||||
/**
|
||||
* Called when the diagram editor is ready to receive commands.
|
||||
*
|
||||
* @param client - the DiagramsNetClient that fired the ready event.
|
||||
* @param sourceUrl - the URL of the diagram to load, or the empty diagram constant.
|
||||
*/
|
||||
private async onDiagramReady(sourceUrl: string) {
|
||||
private async onDiagramReady(client: DiagramsNetClient, sourceUrl: string) {
|
||||
let data: string;
|
||||
|
||||
if (sourceUrl === EMPTY_DIAGRAM_IMAGE) {
|
||||
@@ -113,25 +134,38 @@ export default class Diagrams extends Extension {
|
||||
data = await FileHelper.urlToBase64(sourceUrl);
|
||||
}
|
||||
|
||||
this.client.loadDiagram(data);
|
||||
client.loadDiagram(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a diagram has been exported from the editor.
|
||||
*
|
||||
* @param base64Data - the exported diagram as base64 encoded SVG.
|
||||
* @param session - the editing session that produced this export.
|
||||
*/
|
||||
private async onDiagramExported(base64Data: string) {
|
||||
const file = FileHelper.base64ToFile(
|
||||
base64Data,
|
||||
"diagram.svg",
|
||||
"image/svg+xml"
|
||||
);
|
||||
const dimensions = await FileHelper.getImageDimensions(file);
|
||||
const uploadedUrl = await this.uploadDiagramFile(file);
|
||||
private async onDiagramExported(base64Data: string, session: DiagramSession) {
|
||||
try {
|
||||
const file = FileHelper.base64ToFile(
|
||||
base64Data,
|
||||
"diagram.svg",
|
||||
"image/svg+xml"
|
||||
);
|
||||
|
||||
this.updateDiagramInDocument(uploadedUrl, dimensions || {});
|
||||
this.currentNodeSrc = uploadedUrl;
|
||||
const dimensions = await FileHelper.getImageDimensions(file);
|
||||
const uploadedUrl = await this.uploadDiagramFile(file);
|
||||
|
||||
// Capture the src we need to search for *before* updating the session,
|
||||
// then update the document and the session atomically.
|
||||
const srcToFind = session.nodeSrc;
|
||||
this.updateDiagramInDocument(uploadedUrl, dimensions || {}, srcToFind);
|
||||
|
||||
// Update session so that subsequent saves within the same editing session
|
||||
// can locate the node by its new uploaded URL.
|
||||
session.nodeSrc = uploadedUrl;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to export diagram:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,21 +184,26 @@ export default class Diagrams extends Extension {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates or inserts the diagram image in the document.
|
||||
* Updates or inserts the diagram image in the document. Always reads fresh
|
||||
* editor state at call-time so that positions are accurate even after async
|
||||
* gaps.
|
||||
*
|
||||
* @param uploadedUrl - the URL of the uploaded diagram.
|
||||
* @param dimensions - the image dimensions.
|
||||
* @param srcToFind - the src attribute value to search for in the document.
|
||||
*/
|
||||
private updateDiagramInDocument(
|
||||
uploadedUrl: string,
|
||||
dimensions: { width?: number; height?: number }
|
||||
dimensions: { width?: number; height?: number },
|
||||
srcToFind: string
|
||||
) {
|
||||
// Read fresh state at the moment of dispatch to avoid stale positions.
|
||||
const { state } = this.editor.view;
|
||||
const { dispatch } = this.editor.view;
|
||||
const imageType = this.editor.schema.nodes.image;
|
||||
|
||||
// Try to find and update existing node
|
||||
const existingNode = this.findImageNodeBySrc(state, this.currentNodeSrc);
|
||||
// Try to find and update the existing node by its current src attribute.
|
||||
const existingNode = this.findImageNodeBySrc(state, srcToFind);
|
||||
|
||||
const attrs = {
|
||||
...dimensions,
|
||||
@@ -226,5 +265,4 @@ export default class Diagrams extends Extension {
|
||||
}
|
||||
|
||||
private client: DiagramsNetClient;
|
||||
private currentNodeSrc: string = "";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user