quit nav: File→Quit always exits to the project overview, never history.back()
Quit used to mimic the Back button (history.back() whenever a referrer
existed), but every in-app entry and every tool switch is a hard
location.assign that pushes a history entry — so after schematic ⇄ pcb
switches, one step back is another editor, not the page the user left from.
Quit now navigates to the project overview explicitly (projectPath; "/" for
lib editors), which also covers deep links uniformly.
New VITE_QUIT_ORIGIN ("" ⇒ same-origin) lets the backed editor deployment
exit to the management app's project page instead of the standalone's own;
build-editor.mjs sets it (override with --app-origin).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8aaac55908
commit
8a20f69c5e
2 changed files with 32 additions and 33 deletions
|
|
@ -2,14 +2,15 @@ import { test, expect, type Page } from '@playwright/test';
|
||||||
import { clickMenuBarItem, clickMenuItemByText } from '../e2e/utils/element-tracker';
|
import { clickMenuBarItem, clickMenuItemByText } from '../e2e/utils/element-tracker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File → Quit e2e: quitting an editor must leave it, like the browser Back
|
* File → Quit e2e: quitting an editor must leave it for the project overview,
|
||||||
* button — return to wherever the user navigated in from (the project page),
|
* however the editor was entered (project-page link or deep link).
|
||||||
* falling back to the project page on a deep link with no same-origin history.
|
|
||||||
*
|
*
|
||||||
* The wx wasm port notifies the page when the app's top window is destroyed
|
* The wx wasm port notifies the page when the app's top window is destroyed
|
||||||
* (window.wxAppTopWindowClosed, wxwidgets src/wasm/toplevel.cpp); WasmTool maps
|
* (window.wxAppTopWindowClosed, wxwidgets src/wasm/toplevel.cpp); WasmTool maps
|
||||||
* that to history.back() / location.assign(projectPath). A quit vetoed by the
|
* that to location.assign(projectPath) — deliberately NOT history.back(),
|
||||||
* unsaved-changes prompt never destroys the frame, so it never navigates.
|
* which strands the user in the previous editor after a tool switch (see
|
||||||
|
* quit-after-tool-switch.spec.ts). A quit vetoed by the unsaved-changes prompt
|
||||||
|
* never destroys the frame, so it never navigates.
|
||||||
*
|
*
|
||||||
* URL-only assertions — no screenshots.
|
* URL-only assertions — no screenshots.
|
||||||
*/
|
*/
|
||||||
|
|
@ -70,11 +71,11 @@ test.describe('web app — File → Quit leaves the editor', () => {
|
||||||
await page.waitForURL(PROJECT_URL_RE, { timeout: 30000 });
|
await page.waitForURL(PROJECT_URL_RE, { timeout: 30000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('quit on a deep-linked editor falls back to the project page', async ({ page }) => {
|
test('quit on a deep-linked editor goes to the project page', async ({ page }) => {
|
||||||
test.setTimeout(300000);
|
test.setTimeout(300000);
|
||||||
|
|
||||||
// Direct entry (typed URL): no referrer, nothing meaningful to go back
|
// Direct entry (typed URL): no project page in this tab's history —
|
||||||
// to — quit must land on the project page via the fallback URL.
|
// quit must land on the project overview all the same.
|
||||||
await page.goto(`/${SCOPE}/projects/demo/demo.kicad_sch`);
|
await page.goto(`/${SCOPE}/projects/demo/demo.kicad_sch`);
|
||||||
await waitForToolReady(page, /demo — Schematic Editor/i);
|
await waitForToolReady(page, /demo — Schematic Editor/i);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { ChevronDown, ChevronUp, EyeOff, Loader2, PanelsTopLeft } from "lucide-react";
|
import { ChevronDown, ChevronUp, EyeOff, Loader2, PanelsTopLeft } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
API_BASE_URL,
|
API_BASE_URL,
|
||||||
|
APP_URL,
|
||||||
currentScope,
|
currentScope,
|
||||||
libsSourceConfig,
|
libsSourceConfig,
|
||||||
modelsSourceConfig,
|
modelsSourceConfig,
|
||||||
|
|
@ -27,6 +28,7 @@ import {
|
||||||
type DocSource,
|
type DocSource,
|
||||||
} from "@/lib/config";
|
} from "@/lib/config";
|
||||||
import { defaultFileName, newFileTemplate, withExtension } from "@/lib/new-file";
|
import { defaultFileName, newFileTemplate, withExtension } from "@/lib/new-file";
|
||||||
|
import { redirectTargetFor } from "@/lib/redirect";
|
||||||
import { loadSessionIdentity } from "@/lib/session-identity";
|
import { loadSessionIdentity } from "@/lib/session-identity";
|
||||||
import { bootKicadTool } from "@/wasm/boot";
|
import { bootKicadTool } from "@/wasm/boot";
|
||||||
import { resolveWasmBase } from "@/wasm/wasm-assets";
|
import { resolveWasmBase } from "@/wasm/wasm-assets";
|
||||||
|
|
@ -348,8 +350,8 @@ let quitHandled = false;
|
||||||
* tool-switch hook's location.assign). The wx port's UnloadCallback runs on
|
* tool-switch hook's location.assign). The wx port's UnloadCallback runs on
|
||||||
* BEFOREUNLOAD — i.e. the instant the navigation starts, while this document
|
* BEFOREUNLOAD — i.e. the instant the navigation starts, while this document
|
||||||
* keeps running until the next one commits — and closes the top frame, which
|
* keeps running until the next one commits — and closes the top frame, which
|
||||||
* fires wxAppTopWindowClosed. Without the latch the quit hook then
|
* fires wxAppTopWindowClosed. Without the latch the quit hook then navigates
|
||||||
* history.back()s over the in-flight navigation (the pagehide latch below is
|
* to the exit URL over the in-flight navigation (the pagehide latch below is
|
||||||
* too late: pagehide only fires at commit time). One-shot per document, same
|
* too late: pagehide only fires at commit time). One-shot per document, same
|
||||||
* as the pagehide latch — this page is on its way out.
|
* as the pagehide latch — this page is on its way out.
|
||||||
*/
|
*/
|
||||||
|
|
@ -383,31 +385,23 @@ if (typeof window !== "undefined") {
|
||||||
|
|
||||||
function installQuitHook(
|
function installQuitHook(
|
||||||
win: ToolWindow,
|
win: ToolWindow,
|
||||||
opts: { fallbackUrl: string; log: (m: string) => void },
|
opts: { exitUrl: string; log: (m: string) => void },
|
||||||
): () => void {
|
): () => void {
|
||||||
const hook = () => {
|
const hook = () => {
|
||||||
// Any referrer means we entered by a real navigation — an in-app hard
|
// Quit always navigates to the exit URL (project overview / home). Never
|
||||||
// navigation (ProjectView / NewFileDialog / tool-switch all
|
// history.back(): every in-app entry AND every tool switch is a hard
|
||||||
// location.assign) OR the cross-origin management app (app.pcbjam.com →
|
// location.assign(), so after a schematic ⇄ pcb switch the previous
|
||||||
// editor.pcbjam.com deep-links; the primary entry in the closed deploy).
|
// history entry is another editor — unwinding history strands the user
|
||||||
// Quit behaves like the Back button: going back lands wherever the user
|
// there instead of leaving the editor.
|
||||||
// came from. Deep links and fresh tabs have no referrer and no usable
|
//
|
||||||
// history — go to the fallback instead.
|
|
||||||
const hasReferrer = !!win.document.referrer;
|
|
||||||
|
|
||||||
// Defer the navigation out of the wasm callback: this fires from inside the
|
// Defer the navigation out of the wasm callback: this fires from inside the
|
||||||
// frame's C++ destructor (via EM_ASM under Asyncify), and the teardown keeps
|
// frame's C++ destructor (via EM_ASM under Asyncify), and the teardown keeps
|
||||||
// running after we return. A cross-document location.assign() started here is
|
// running after we return. A cross-document location.assign() started here is
|
||||||
// aborted by that continuing teardown (only the same-document history.back()
|
// aborted by that continuing teardown — so hand it to a fresh task once the
|
||||||
// survives) — so hand it to a fresh task once the wasm stack has unwound.
|
// wasm stack has unwound.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (hasReferrer && win.history.length > 1) {
|
opts.log(`[quit] editor closed — going to ${opts.exitUrl}`);
|
||||||
opts.log("[quit] editor closed — history.back()");
|
win.location.assign(opts.exitUrl);
|
||||||
win.history.back();
|
|
||||||
} else {
|
|
||||||
opts.log(`[quit] editor closed — no in-app history, going to ${opts.fallbackUrl}`);
|
|
||||||
win.location.assign(opts.fallbackUrl);
|
|
||||||
}
|
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1042,12 +1036,16 @@ export function WasmTool({
|
||||||
log: append,
|
log: append,
|
||||||
});
|
});
|
||||||
|
|
||||||
// File→Quit leaves the editor. Lib editors (/:scope/libs/:name) have no
|
// File→Quit leaves the editor for the project overview — lib editors
|
||||||
// project overview to fall back to — go home instead.
|
// (/:scope/libs/:name) have none, so they exit home instead. Both are
|
||||||
|
// non-editor surfaces: on a backed deploy (APP_URL set) they belong to the
|
||||||
|
// management app, so quit goes straight there (one hop instead of letting
|
||||||
|
// App.tsx's 0006 redirect bounce it).
|
||||||
const segments = win.location.pathname.split("/").filter(Boolean);
|
const segments = win.location.pathname.split("/").filter(Boolean);
|
||||||
|
const exitPath =
|
||||||
|
segments[1] === "libs" ? "/" : projectPath(currentScope(), slug);
|
||||||
const removeQuitHook = installQuitHook(win, {
|
const removeQuitHook = installQuitHook(win, {
|
||||||
fallbackUrl:
|
exitUrl: redirectTargetFor(APP_URL, exitPath) ?? exitPath,
|
||||||
segments[1] === "libs" ? "/" : projectPath(currentScope(), slug),
|
|
||||||
log: append,
|
log: append,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue