Kaynağa Gözat

Tell a framed external link which theme Bambuddy shows

maziggy 1 gün önce
ebeveyn
işleme
bdc1fc68d0

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 0
CHANGELOG.md


+ 78 - 0
frontend/src/__tests__/pages/ExternalLinkPageTheme.test.tsx

@@ -0,0 +1,78 @@
+/**
+ * The external-link page tells the framed app which theme Bambuddy shows, so
+ * an app made for the sidebar (Bambuddy Orders) can match it.
+ */
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+import { Route, Routes } from 'react-router-dom';
+import { render } from '../utils';
+import { api } from '../../api/client';
+import { ExternalLinkPage } from '../../pages/ExternalLinkPage';
+
+const LINK = {
+  id: 5,
+  name: 'Orders',
+  url: 'http://orders.local:8090/',
+  icon: 'shopping-cart',
+  open_in_new_tab: false,
+  custom_icon: null,
+  sort_order: 0,
+  created_at: '2026-09-27T00:00:00Z',
+  updated_at: '2026-09-27T00:00:00Z',
+};
+
+function renderPage() {
+  window.history.pushState({}, '', '/external/5');
+  return render(
+    <Routes>
+      <Route path="/external/:id" element={<ExternalLinkPage />} />
+    </Routes>,
+  );
+}
+
+beforeEach(() => {
+  vi.spyOn(api, 'getExternalLink').mockResolvedValue(LINK);
+});
+
+afterEach(() => {
+  vi.restoreAllMocks();
+});
+
+// The values come from the theme context (local choice plus the server
+// settings sync); the contract is the message's shape.
+const THEME = {
+  type: 'bambuddy:theme',
+  mode: expect.stringMatching(/^(light|dark)$/),
+  style: expect.any(String),
+  background: expect.any(String),
+  accent: expect.any(String),
+};
+
+describe('ExternalLinkPage theme', () => {
+  it('sends the theme to the link origin when the page loads', async () => {
+    renderPage();
+    const frame = (await screen.findByTitle('Orders')) as HTMLIFrameElement;
+    const post = vi.spyOn(frame.contentWindow!, 'postMessage');
+    fireEvent.load(frame);
+    expect(post).toHaveBeenCalledWith(THEME, 'http://orders.local:8090');
+  });
+
+  it('answers when the framed page asks for the theme', async () => {
+    renderPage();
+    const frame = (await screen.findByTitle('Orders')) as HTMLIFrameElement;
+    const post = vi.spyOn(frame.contentWindow!, 'postMessage');
+    window.dispatchEvent(
+      new MessageEvent('message', { data: { type: 'bambuddy:theme-request' }, source: frame.contentWindow }),
+    );
+    await waitFor(() => expect(post).toHaveBeenCalledWith(THEME, 'http://orders.local:8090'));
+  });
+
+  it('ignores theme requests from other windows', async () => {
+    renderPage();
+    const frame = (await screen.findByTitle('Orders')) as HTMLIFrameElement;
+    const post = vi.spyOn(frame.contentWindow!, 'postMessage');
+    window.dispatchEvent(new MessageEvent('message', { data: { type: 'bambuddy:theme-request' }, source: window }));
+    expect(post).not.toHaveBeenCalled();
+  });
+});

+ 43 - 0
frontend/src/pages/ExternalLinkPage.tsx

@@ -1,3 +1,4 @@
+import { useCallback, useEffect, useRef, type RefObject } from 'react';
 import { useParams } from 'react-router-dom';
 import { useQuery } from '@tanstack/react-query';
 import { Loader2, AlertTriangle } from 'lucide-react';
@@ -9,6 +10,8 @@ export function ExternalLinkPage() {
   const { t } = useTranslation();
   const { id } = useParams<{ id: string }>();
   const { mode } = useTheme();
+  const iframeRef = useRef<HTMLIFrameElement>(null);
+  const sendTheme = useThemeMessenger(iframeRef);
 
   const { data: link, isLoading, error } = useQuery({
     queryKey: ['external-link', id],
@@ -35,7 +38,9 @@ export function ExternalLinkPage() {
 
   return (
     <iframe
+      ref={iframeRef}
       src={link.url}
+      onLoad={sendTheme}
       className="h-full w-full border-0"
       style={{ colorScheme: mode }}
       title={link.name}
@@ -43,3 +48,41 @@ export function ExternalLinkPage() {
     />
   );
 }
+
+/**
+ * Tell the framed page which theme Bambuddy is showing, so an app built to
+ * sit in the sidebar (Bambuddy Orders) can match it. Sent when the page
+ * loads, when the theme changes, and when the page asks for it. Only to the
+ * link's own origin, and it carries nothing but the theme names.
+ */
+function useThemeMessenger(iframeRef: RefObject<HTMLIFrameElement | null>) {
+  const { resolvedMode, darkStyle, darkBackground, darkAccent, lightStyle, lightBackground, lightAccent } = useTheme();
+  const dark = resolvedMode === 'dark';
+  const style = dark ? darkStyle : lightStyle;
+  const background = dark ? darkBackground : lightBackground;
+  const accent = dark ? darkAccent : lightAccent;
+
+  const sendTheme = useCallback(() => {
+    const frame = iframeRef.current;
+    if (!frame?.contentWindow) return;
+    let origin: string;
+    try {
+      origin = new URL(frame.src).origin;
+    } catch {
+      return;
+    }
+    frame.contentWindow.postMessage({ type: 'bambuddy:theme', mode: resolvedMode, style, background, accent }, origin);
+  }, [iframeRef, resolvedMode, style, background, accent]);
+
+  useEffect(() => sendTheme(), [sendTheme]);
+
+  useEffect(() => {
+    const onMessage = (e: MessageEvent) => {
+      if (e.source === iframeRef.current?.contentWindow && e.data?.type === 'bambuddy:theme-request') sendTheme();
+    };
+    window.addEventListener('message', onMessage);
+    return () => window.removeEventListener('message', onMessage);
+  }, [iframeRef, sendTheme]);
+
+  return sendTheme;
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
static/assets/PdfPreviewModal-D_ysTjTV.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/SpreadsheetPreviewModal-BlGo9I1s.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
static/assets/index-CwRA56yz.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/assets/pdf-DpVMGFDE.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DZQZ7yaD.js"></script>
+    <script type="module" crossorigin src="/assets/index-CwRA56yz.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/chunk-aKtaBQYM.js">
     <link rel="stylesheet" crossorigin href="/assets/index-DDT9pHR2.css">
   </head>

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor