Browse Source

Merge pull request #11196 from growilabs/claude/happy-pasteur-Tj3lr

fix: draw.io stencil URLs for local instances
Yuki Takei 1 day ago
parent
commit
14dadcf8a8

+ 14 - 1
apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx

@@ -2,16 +2,20 @@ import { type JSX, useCallback } from 'react';
 import Script from 'next/script';
 import type { IGraphViewerGlobal } from '@growi/remark-drawio';
 
+import { patchStencilRegistryUrls } from './patch-stencil-registry-urls';
 import { generateViewerMinJsUrl } from './use-viewer-min-js-url';
 
 declare global {
   var GraphViewer: IGraphViewerGlobal;
+  var mxStencilRegistry: { libraries: Record<string, string[]> } | undefined;
 }
 
 type Props = {
   drawioUri: string;
 };
 
+const DEFAULT_DRAWIO_ORIGIN = 'https://embed.diagrams.net';
+
 export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
   const loadedHandler = useCallback(() => {
     // disable useResizeSensor and checkVisibleState
@@ -29,8 +33,17 @@ export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
     GraphViewer.prototype.lightboxZIndex = 1200;
     GraphViewer.prototype.toolbarZIndex = 1200;
 
+    try {
+      const origin = new URL(drawioUri).origin;
+      if (origin !== DEFAULT_DRAWIO_ORIGIN) {
+        patchStencilRegistryUrls(mxStencilRegistry?.libraries, origin);
+      }
+    } catch {
+      // skip patching if drawioUri cannot be parsed
+    }
+
     GraphViewer.processElements();
-  }, []);
+  }, [drawioUri]);
 
   // Return empty element if drawioUri is not provided to avoid Invalid URL error
   if (!drawioUri) {

+ 140 - 0
apps/app/src/components/Script/DrawioViewerScript/patch-stencil-registry-urls.spec.ts

@@ -0,0 +1,140 @@
+import { patchStencilRegistryUrls } from './patch-stencil-registry-urls';
+
+describe('patchStencilRegistryUrls', () => {
+  it('should replace viewer.diagrams.net origin with the local origin', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      basic: [
+        'https://viewer.diagrams.net/shapes/basic/cube.xml',
+        'https://viewer.diagrams.net/stencils/basic/sphere.xml',
+      ],
+      arrows: ['https://viewer.diagrams.net/shapes/arrows/arrow.xml'],
+    };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries).toEqual({
+      basic: [
+        'http://localhost:8080/shapes/basic/cube.xml',
+        'http://localhost:8080/stencils/basic/sphere.xml',
+      ],
+      arrows: ['http://localhost:8080/shapes/arrows/arrow.xml'],
+    });
+  });
+
+  it('should mutate libraries in place', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      basic: ['https://viewer.diagrams.net/shapes/basic/cube.xml'],
+    };
+    const ref = libraries;
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(ref).toBe(libraries);
+    expect(ref.basic[0]).toBe('http://localhost:8080/shapes/basic/cube.xml');
+  });
+
+  it('should leave URLs without the viewer.diagrams.net origin unchanged', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      mixed: [
+        'https://viewer.diagrams.net/shapes/a.xml',
+        'https://example.com/shapes/b.xml',
+        '/relative/path/c.xml',
+      ],
+    };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries.mixed).toEqual([
+      'http://localhost:8080/shapes/a.xml',
+      'https://example.com/shapes/b.xml',
+      '/relative/path/c.xml',
+    ]);
+  });
+
+  it('should be idempotent on a second invocation', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      basic: ['https://viewer.diagrams.net/shapes/basic/cube.xml'],
+    };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries.basic).toEqual([
+      'http://localhost:8080/shapes/basic/cube.xml',
+    ]);
+  });
+
+  it('should not throw when libraries is undefined', () => {
+    // Act & Assert
+    expect(() =>
+      patchStencilRegistryUrls(undefined, 'http://localhost:8080'),
+    ).not.toThrow();
+  });
+
+  it('should handle an empty libraries object', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {};
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries).toEqual({});
+  });
+
+  it('should handle an empty url array', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = { basic: [] };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries.basic).toEqual([]);
+  });
+
+  it('should skip non-string entries defensively', () => {
+    // Arrange — simulates an unexpected runtime value from the third-party script
+    const libraries = {
+      basic: ['https://viewer.diagrams.net/shapes/a.xml', null, undefined, 123],
+    } as unknown as Record<string, string[]>;
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'http://localhost:8080');
+
+    // Assert
+    expect(libraries.basic).toEqual([
+      'http://localhost:8080/shapes/a.xml',
+      null,
+      undefined,
+      123,
+    ]);
+  });
+
+  it('should support custom origins with ports and paths', () => {
+    // Arrange
+    const libraries: Record<string, string[]> = {
+      basic: ['https://viewer.diagrams.net/shapes/basic/cube.xml'],
+    };
+
+    // Act
+    patchStencilRegistryUrls(libraries, 'https://drawio.example.com:8443');
+
+    // Assert
+    expect(libraries.basic).toEqual([
+      'https://drawio.example.com:8443/shapes/basic/cube.xml',
+    ]);
+  });
+});

+ 20 - 0
apps/app/src/components/Script/DrawioViewerScript/patch-stencil-registry-urls.ts

@@ -0,0 +1,20 @@
+// viewer-static.min.js hardcodes stencil resource URLs to https://viewer.diagrams.net.
+// For local draw.io instances that origin is unreachable, so we rewrite the URLs
+// in mxStencilRegistry.libraries (fetched lazily on first diagram render) to point
+// to the configured local origin before any diagram is rendered.
+// refs: https://github.com/growilabs/growi/issues/10726
+export const VIEWER_DIAGRAMS_NET_ORIGIN = 'https://viewer.diagrams.net';
+
+export const patchStencilRegistryUrls = (
+  libraries: Record<string, string[]> | undefined,
+  localOrigin: string,
+): void => {
+  if (libraries == null) return;
+  for (const key of Object.keys(libraries)) {
+    libraries[key] = libraries[key].map((url) =>
+      typeof url === 'string'
+        ? url.replace(VIEWER_DIAGRAMS_NET_ORIGIN, localOrigin)
+        : url,
+    );
+  }
+};