From 3793a6df52d4d07330812eb4de5f74a03b50e012 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 19 Jun 2026 13:34:13 +0100 Subject: [PATCH] Fix bad frontend architecture (#6730) # Description of Changes #6727 introduced frontend code which goes against the architecture, so this PR re-implements it in the architecture properly, along with another bad Tauri check that I found in the source. I also updated the `AGENTS.md` file to use Claude's "read this file" syntax to try and force AI to actually read the file instead of just suggesting that it does it. --- AGENTS.md | 2 +- .../core/components/shared/UpdateModal.tsx | 25 ++----------------- .../components/shared/UpdateStartupPopup.tsx | 24 +----------------- .../src/core/platform/externalLinkClick.ts | 15 +++++++++++ .../components/shared/UpdateStartupPopup.tsx | 13 ++++++++++ .../src/desktop/platform/externalLinkClick.ts | 18 +++++++++++++ 6 files changed, 50 insertions(+), 47 deletions(-) create mode 100644 frontend/editor/src/core/platform/externalLinkClick.ts create mode 100644 frontend/editor/src/desktop/components/shared/UpdateStartupPopup.tsx create mode 100644 frontend/editor/src/desktop/platform/externalLinkClick.ts diff --git a/AGENTS.md b/AGENTS.md index ae8eb60316..44647b0063 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,7 +152,7 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie #### Import Paths - CRITICAL **ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation. -For a broader explanation of the frontend layering and override architecture, see [frontend/editor/DeveloperGuide.md](frontend/editor/DeveloperGuide.md). +For a broader explanation of the frontend layering and override architecture, read @frontend/editor/DeveloperGuide.md ```typescript // ✅ CORRECT - Use @app/* for all imports diff --git a/frontend/editor/src/core/components/shared/UpdateModal.tsx b/frontend/editor/src/core/components/shared/UpdateModal.tsx index ca052ae523..1399325b03 100644 --- a/frontend/editor/src/core/components/shared/UpdateModal.tsx +++ b/frontend/editor/src/core/components/shared/UpdateModal.tsx @@ -24,7 +24,7 @@ import { MachineInfo, } from "@app/services/updateService"; import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex"; -import { openExternal } from "@app/platform/openExternal"; +import { handleExternalLinkClick } from "@app/platform/externalLinkClick"; import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import DownloadIcon from "@mui/icons-material/Download"; @@ -37,19 +37,6 @@ import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; -/** - * Best-effort Tauri detection without importing `@tauri-apps/api` into the - * core bundle (which must stay runnable on plain web). Tauri v2 injects - * `__TAURI_INTERNALS__` before any user code runs. Mirrors UpdateStartupPopup. - */ -function isRunningInTauri(): boolean { - if (typeof window === "undefined") return false; - return ( - typeof (window as unknown as { __TAURI_INTERNALS__?: unknown }) - .__TAURI_INTERNALS__ !== "undefined" - ); -} - export type DesktopInstallState = | "idle" | "downloading" @@ -211,18 +198,10 @@ const UpdateModal: React.FC = ({ onClose(); }; - // External links (release notes, migration guides, download fallback) use - // real anchors so they open a new tab on web. Inside Tauri the webview traps - // target="_blank", so on desktop we intercept and hand the URL to the OS - // browser via the platform seam. stopPropagation keeps links nested in the - // clickable version-history rows from toggling the row. const handleExternalLink = (url: string) => (e: React.MouseEvent) => { e.stopPropagation(); - if (isRunningInTauri()) { - e.preventDefault(); - void openExternal(url); - } + handleExternalLinkClick(url, e); }; // Sort versions newest first, skip the latest (already shown in header) diff --git a/frontend/editor/src/core/components/shared/UpdateStartupPopup.tsx b/frontend/editor/src/core/components/shared/UpdateStartupPopup.tsx index f3e2a446b9..62f6dbb999 100644 --- a/frontend/editor/src/core/components/shared/UpdateStartupPopup.tsx +++ b/frontend/editor/src/core/components/shared/UpdateStartupPopup.tsx @@ -20,29 +20,9 @@ const STARTUP_DELAY_MS = 15_000; const SNOOZE_KEY = "stirling-pdf-updater:snoozedUntil"; const SNOOZE_DURATION_MS = 24 * 60 * 60 * 1000; -/** - * Best-effort Tauri detection without importing `@tauri-apps/api` into the - * core bundle (which must remain runnable on plain web). Tauri v2 injects - * `__TAURI_INTERNALS__` before any user code runs. - */ -function isRunningInTauri(): boolean { - if (typeof window === "undefined") return false; - return ( - typeof (window as unknown as { __TAURI_INTERNALS__?: unknown }) - .__TAURI_INTERNALS__ !== "undefined" - ); -} - /** * Web/server-side auto-popup that shows the UpdateModal on startup when a - * newer Stirling-PDF version is available. Previously this check only ran - * from the Settings → General "Check for Updates" button, so non-desktop - * users could sit on stale versions indefinitely without any prompt. - * - * On desktop (Tauri) this component is a no-op — `useDesktopUpdatePopup` - * drives the desktop flow because it also has to honour the headless - * `updateMode` provisioning flag and wire up the silent/auto installer. - * Running both would double-popup. + * newer Stirling-PDF version is available. */ export function UpdateStartupPopup() { const { config } = useAppConfig(); @@ -60,8 +40,6 @@ export function UpdateStartupPopup() { const hasChecked = useRef(false); useEffect(() => { - // Skip on desktop — the Tauri popup owns that flow end-to-end. - if (isRunningInTauri()) return; if (hasChecked.current) return; if (!currentVersion) return; // Don't even schedule the timer until we have a version to compare. diff --git a/frontend/editor/src/core/platform/externalLinkClick.ts b/frontend/editor/src/core/platform/externalLinkClick.ts new file mode 100644 index 0000000000..9be70d062f --- /dev/null +++ b/frontend/editor/src/core/platform/externalLinkClick.ts @@ -0,0 +1,15 @@ +import type { MouseEvent } from "react"; + +/** + * Click handler for external `` links rendered by shared + * components (e.g. UpdateModal release notes / migration guides). + * + * In a normal browser a `target="_blank"` anchor already opens the URL in a new + * tab, so this default does nothing and lets the native navigation proceed. + * Builds whose webview traps `target="_blank"` inside the app window shadow this + * module to intercept the click and route the URL to the OS browser instead. + */ +export function handleExternalLinkClick( + _url: string, + _event: MouseEvent, +): void {} diff --git a/frontend/editor/src/desktop/components/shared/UpdateStartupPopup.tsx b/frontend/editor/src/desktop/components/shared/UpdateStartupPopup.tsx new file mode 100644 index 0000000000..07ca5ad2b6 --- /dev/null +++ b/frontend/editor/src/desktop/components/shared/UpdateStartupPopup.tsx @@ -0,0 +1,13 @@ +/** + * Desktop (Tauri) override of @app/components/shared/UpdateStartupPopup. + * + * On desktop the update flow is owned end-to-end by `useDesktopUpdatePopup`, + * which also honours the headless `updateMode` provisioning flag and wires up + * the silent/auto installer. The web startup popup must therefore be a no-op + * here, otherwise both would run and double-popup. + */ +export function UpdateStartupPopup() { + return null; +} + +export default UpdateStartupPopup; diff --git a/frontend/editor/src/desktop/platform/externalLinkClick.ts b/frontend/editor/src/desktop/platform/externalLinkClick.ts new file mode 100644 index 0000000000..23c5722e2b --- /dev/null +++ b/frontend/editor/src/desktop/platform/externalLinkClick.ts @@ -0,0 +1,18 @@ +import type { MouseEvent } from "react"; +import { openExternal } from "@app/platform/openExternal"; + +/** + * Desktop (Tauri) override of the @app/platform/externalLinkClick seam. + * + * The app runs inside a Tauri webview, which traps a `target="_blank"` anchor + * inside our own window. Intercept the click and hand the URL to the OS browser + * via the openExternal seam (Tauri shell open) so the link lands in the user's + * real browser. + */ +export function handleExternalLinkClick( + url: string, + event: MouseEvent, +): void { + event.preventDefault(); + void openExternal(url); +}