feat(cutover): link the legacy pages from System and when the workspace fails to load
System's Overview section ends with one small Legacy pages line (D165). If a workspace module fails to load or throws while starting, the page shows a static message with the same links instead of an empty shell. Part of #525
This commit is contained in:
@@ -252,7 +252,7 @@ match Classic and Focused.
|
||||
|
||||
5. **Access the web interface**:
|
||||
- **Workspace**: click **PM** in ComfyUI's top bar or **Web UI** on a node, or open `http://localhost:8188/prompt_manager/workspace`
|
||||
- **Legacy pages** still work at their URLs, and the workspace doesn't link to them:
|
||||
- **Legacy pages** still work at their URLs, and System's Overview section links to them:
|
||||
`/prompt_manager/admin` (dashboard), `/prompt_manager/admin#/tags` (tags), `/prompt_manager/gallery` (gallery), `/prompt_manager/gallery.html` (metadata viewer)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
+15
-5
@@ -2642,7 +2642,7 @@ the grid) and D43 (sorted chips, checked against the concept with its chips sort
|
||||
D44 needs no mapping: the comparison runs with no Refine filter active, so the button
|
||||
reads "Filters" as the concept's does. Nothing else follows the grid: the legacy
|
||||
"Current prompt library" link is removed from the shelf; the legacy Library stays at
|
||||
`/prompt_manager/admin` by direct URL, with no workspace link (#525)
|
||||
`/prompt_manager/admin` by direct URL, linked only from System (#525, D165)
|
||||
(`tests/browser/workspace.spec.mjs`). `library comparison detects
|
||||
mutations` injects `.card-footer { padding: 2px }` and removes `.card-meta .chip.red`.
|
||||
|
||||
@@ -3110,7 +3110,7 @@ npm run --prefix tools/vendor test:workflows -- history-concept.spec.mjs \
|
||||
|
||||
`#/inspect` ("Inspect a file") is a native workspace route; it no longer hands off to
|
||||
the legacy metadata page, which stays at its URL
|
||||
(`/prompt_manager/gallery.html`) with no workspace link (#525). The page is the concept's `#metadata`
|
||||
(`/prompt_manager/gallery.html`) linked only from System (#525, D165). The page is the concept's `#metadata`
|
||||
(app.js `renderMetadata` a740–748, selection a1715–1729; style.css 709–722, 1180–1194,
|
||||
1543–1545, 1559–1564) in the concept's own classes (`.surface`, `.file-drop`,
|
||||
`.spaced-20`, `.local-file-preview`, all from the synced `concept.css`), with no
|
||||
@@ -7022,7 +7022,7 @@ Deploying this backend needs a ComfyUI restart (Python and route changes).
|
||||
|
||||
`#/tags` ("Tags & categories", page title "Tags") is a native workspace route; it no
|
||||
longer hands off to the legacy tag manager. That page stays at
|
||||
`/prompt_manager/admin#/tags` with no workspace link (#525); tag search and sort, the untagged
|
||||
`/prompt_manager/admin#/tags` linked only from System (#525, D165); tag search and sort, the untagged
|
||||
entry (D149) and multi-tag OR (the Library's Tag match, D150) moved to the workspace
|
||||
(#444); bulk prompt delete stays on the legacy admin page (bulk tags: the Prompt
|
||||
library's selection bar, D151). A prompt's category is changed in the Prompt library
|
||||
@@ -7151,6 +7151,16 @@ The top-bar PM button, both Web UI buttons and `/prompt_manager/web` open
|
||||
`/prompt_manager/workspace#/library` (`web/js/workspace-entry.js`; `web/index.html` keeps its
|
||||
own copy, pinned by `tests/js/workspace-entry.test.js`). Overview's cards open native hashes.
|
||||
The legacy pages (`/prompt_manager/admin`, `admin#/tags`, `gallery`, `gallery.html`) stay at
|
||||
their URLs. Only the workspace's `<noscript>` links one, and admin's Metadata and Gallery buttons
|
||||
still open their legacy pages (pinned by `legacy-system-links.spec.mjs`). The PM button has no
|
||||
their URLs, and admin's Metadata and Gallery buttons still open their legacy pages (maintainer
|
||||
ruling; pinned by `legacy-system-links.spec.mjs`).
|
||||
|
||||
- **D165** (beyond the concept, maintainer ruling 2026-10-30): System's Overview section ends
|
||||
with one small line, "Legacy pages: Dashboard · Tags · Gallery · Metadata viewer"
|
||||
(`.legacy-pages`, new tabs), outside `.settings-layout`. The concept has no such line;
|
||||
`mapOverviewConcept` adds it to the page tree.
|
||||
- **Load failure** (maintainer ruling 2026-10-30): `web/workspace.html` holds static
|
||||
`#workspace-boot-failure` with the same four links, styled inline so it needs no stylesheet.
|
||||
An inline guard shows it at `load` unless `index.mjs` set `data-workspace-booted`, so a failed
|
||||
fetch of any module and a throw while starting are both covered; `workspace.spec.mjs` tests
|
||||
both. Editing `workspace.html` needs a ComfyUI restart, because the server caches it. The PM button has no
|
||||
page-fixture test; it is proven on the managed host (`tests/comfyui/prompt-manager.spec.mjs`).
|
||||
|
||||
@@ -165,6 +165,8 @@ const number = value => value.toLocaleString('en-US');
|
||||
|
||||
/** The concept's shell and Overview with the recorded deviations applied, given the server's real overview. */
|
||||
function mapOverviewConcept(reference, overview) {
|
||||
// D165: the discreet legacy-pages line follows the settings layout.
|
||||
reference = { ...reference, tree: [...reference.tree, 'p.legacy-pages.small'] };
|
||||
const relabel = { 'Database integrity': 'Not checked', 'Positive prompts': number(overview.library.positive_prompts),
|
||||
'Negative variants': number(overview.library.negative_variants), Images: number(overview.images.indexed) };
|
||||
const rows = {
|
||||
@@ -257,7 +259,7 @@ const UNMAPPED_PATHS = [
|
||||
'columns.surfaces.0.rows.length', 'columns.surfaces.0.tree.4', 'columns.surfaces.0.tree.length',
|
||||
'columns.surfaces.1.rows.0.description', 'columns.surfaces.1.rows.1.title', 'columns.surfaces.1.rows.2.description',
|
||||
'heading.actions.0', 'heading.actions.length', 'metrics.items.3.value', 'nav.label',
|
||||
'save.button.cursor', 'save.button.opacity', 'save.status.text',
|
||||
'save.button.cursor', 'save.button.opacity', 'save.status.text', 'tree.3', 'tree.length',
|
||||
].sort();
|
||||
|
||||
test('system comparison detects mutations', async ({ page, request }) => {
|
||||
|
||||
@@ -130,6 +130,71 @@ test('/prompt_manager/web lands on the workspace Library and Back does not bounc
|
||||
await expect(main.getByRole('heading', { level: 1, name: 'Overview', exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
// D165 (#525, maintainer ruling): a discreet line under System is the workspace's only visible way to the legacy pages.
|
||||
test('System Overview links the legacy pages discreetly, and each one loads (#525)', async ({ page, context }) => {
|
||||
await page.goto(`${workspace}#/system/overview`);
|
||||
const line = page.locator('#workspace-main .legacy-pages');
|
||||
await expect(line).toBeVisible();
|
||||
await expect(line).toContainText('Legacy pages');
|
||||
const links = line.getByRole('link');
|
||||
await expect(links).toHaveCount(4);
|
||||
for (const [name, url, ready] of [
|
||||
['Dashboard', '/prompt_manager/admin', '#addPromptBtn'],
|
||||
['Tags', '/prompt_manager/admin#/tags', '#addPromptBtn'],
|
||||
['Gallery', '/prompt_manager/gallery', 'body'],
|
||||
['Metadata viewer', '/prompt_manager/gallery.html', 'body'],
|
||||
]) {
|
||||
const opened = context.waitForEvent('page');
|
||||
await line.getByRole('link', { name, exact: true }).click();
|
||||
const tab = await opened;
|
||||
await tab.waitForLoadState('domcontentloaded');
|
||||
const target = new URL(tab.url());
|
||||
expect(target.pathname + target.hash).toBe(url);
|
||||
await expect(tab.locator(ready).first()).toBeAttached();
|
||||
await tab.close();
|
||||
}
|
||||
// Only on System's Overview section, and gone with the page.
|
||||
await page.locator('#workspace-main .section-nav').getByRole('button', { name: 'Appearance', exact: true }).click();
|
||||
await expect(line).toBeHidden();
|
||||
await page.goto(`${workspace}#/library`);
|
||||
await expect(page.locator('#workspace-main .legacy-pages')).toHaveCount(0);
|
||||
});
|
||||
|
||||
const bootFailure = page => page.locator('#workspace-boot-failure');
|
||||
|
||||
test('a healthy workspace never shows the load failure message (#525)', async ({ page }) => {
|
||||
await page.goto(`${workspace}#/library`);
|
||||
await expect(page.locator('#workspace-main h1')).toHaveText('Prompt library');
|
||||
await page.waitForFunction(() => document.readyState === 'complete');
|
||||
await expect(bootFailure(page)).toBeHidden();
|
||||
});
|
||||
|
||||
for (const [label, pattern] of [['the entry module', '**/js/workspace/index.mjs*'], ['a dependency', '**/js/workspace/state.mjs*']]) {
|
||||
test(`when ${label} fails to load the page still offers the legacy pages (#525)`, async ({ page }) => {
|
||||
const pageErrors = [];
|
||||
page.on('pageerror', error => pageErrors.push(error.message));
|
||||
await page.route(pattern, route => route.abort());
|
||||
await page.goto(`${workspace}#/library`);
|
||||
const failure = bootFailure(page);
|
||||
await expect(failure).toBeVisible();
|
||||
await expect(failure).toHaveAttribute('role', 'alert');
|
||||
const dashboard = failure.getByRole('link', { name: 'Dashboard', exact: true });
|
||||
await expect(dashboard).toBeVisible();
|
||||
await expect(failure.getByRole('link')).toHaveCount(4);
|
||||
await dashboard.click();
|
||||
await expect(page).toHaveURL(/\/prompt_manager\/admin$/);
|
||||
expect(pageErrors).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test('when a module throws while starting the page still offers the legacy pages (#525)', async ({ page }) => {
|
||||
await page.route('**/js/workspace/state.mjs*', route => route.fulfill({ contentType: 'text/javascript', body: "throw new Error('boot failure');" }));
|
||||
await page.goto(`${workspace}#/library`);
|
||||
await expect(bootFailure(page).getByRole('link', { name: 'Dashboard', exact: true })).toBeVisible();
|
||||
// The broken module's own error is this test's premise, not a failure of the page.
|
||||
page.workspaceErrors.length = 0;
|
||||
});
|
||||
|
||||
test('keyboard navigation, browser history and unknown routes remain usable', async ({ page }) => {
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
const navigation = page.getByRole('navigation', { name: 'Workspace', exact: true });
|
||||
@@ -459,7 +524,7 @@ test('System is native: the concept shell, section navigation, sections and the
|
||||
// D92: no "Sample configuration" chip.
|
||||
await expect(main.locator('.heading .chip')).toHaveCount(0);
|
||||
expect(await main.evaluate(node => [...node.children].map(child => `${child.localName}.${child.className}`)))
|
||||
.toEqual(['div.heading', 'div.section-nav', 'div.settings-layout']);
|
||||
.toEqual(['div.heading', 'div.section-nav', 'div.settings-layout', 'p.small legacy-pages']);
|
||||
const nav = main.getByRole('group', { name: 'System sections', exact: true });
|
||||
await expect(nav.getByRole('button')).toHaveText(SYSTEM_SECTION_LABELS);
|
||||
await expect(nav.getByRole('button', { name: 'Overview', exact: true })).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
@@ -114,6 +114,55 @@ test('top-bar PM button opens the workspace in either renderer', async ({ page }
|
||||
await popup.close();
|
||||
});
|
||||
|
||||
for (const [type, buttonName] of [['PromptManager', '🌐 Web UI'], ['PromptManagerFocused', 'Web UI ↗']]) {
|
||||
test(`${type} and the PM button open the workspace after switching renderers both ways, a workflow reload and node removal`, async ({ page }, testInfo) => {
|
||||
const id = await page.evaluate(async type => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
const node = LiteGraph.createNode(type);
|
||||
app.graph.add(node);
|
||||
node.setSize([900, 800]);
|
||||
app.canvas.centerOnNode(node);
|
||||
return node.id;
|
||||
}, type);
|
||||
const webUi = page.getByRole('button', { name: buttonName, exact: true });
|
||||
const pm = page.locator('.promptmanager-top-button-group button, #pm-toolbar-btn').first();
|
||||
const opensWorkspace = async button => {
|
||||
await expect(button).toHaveCount(1);
|
||||
await expect(button).toBeVisible();
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
await button.click();
|
||||
const popup = await popupPromise;
|
||||
await expect(popup).toHaveURL(/\/prompt_manager\/workspace#\/library$/);
|
||||
await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library');
|
||||
await popup.close();
|
||||
};
|
||||
await opensWorkspace(webUi);
|
||||
for (const renderer of [!testInfo.project.metadata.initialRenderer, testInfo.project.metadata.initialRenderer]) {
|
||||
await page.evaluate(async renderer => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
await app.ui.settings.setSettingValue('Comfy.VueNodes.Enabled', renderer);
|
||||
app.canvas.ds.scale = .8;
|
||||
app.canvas.centerOnNode(app.graph._nodes[0]);
|
||||
}, renderer);
|
||||
await expect.poll(() => page.evaluate(() => LiteGraph.vueNodesMode)).toBe(renderer);
|
||||
await opensWorkspace(webUi);
|
||||
await opensWorkspace(pm);
|
||||
}
|
||||
await page.evaluate(async () => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
await app.loadGraphData(app.graph.serialize());
|
||||
});
|
||||
await opensWorkspace(webUi);
|
||||
await opensWorkspace(pm);
|
||||
await page.evaluate(async id => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
app.graph.remove(app.graph.getNodeById(id));
|
||||
}, id);
|
||||
await expect(webUi).toHaveCount(0);
|
||||
await opensWorkspace(pm);
|
||||
});
|
||||
}
|
||||
|
||||
test('Classic controls follow actual ComfyUI light and dark palettes', async ({ page }, testInfo) => {
|
||||
await page.evaluate(async () => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
|
||||
@@ -93,6 +93,13 @@ test('every Overview card opens its own native page, never a legacy one (#525)',
|
||||
}
|
||||
});
|
||||
|
||||
test('the legacy pages System links are the four that stay at their URLs (#525 D165)', async () => {
|
||||
const { LEGACY_PAGES } = await source;
|
||||
assert.deepEqual(LEGACY_PAGES.map(page => page.href), ['/prompt_manager/admin', '/prompt_manager/admin#/tags',
|
||||
'/prompt_manager/gallery', '/prompt_manager/gallery.html']);
|
||||
for (const page of LEGACY_PAGES) assert.ok(page.label);
|
||||
});
|
||||
|
||||
test('a job of the Activity page is its own deep link', async () => {
|
||||
const { resolveRoute, routeTitle, isHandoff } = await source;
|
||||
const id = '0123456789abcdef'.repeat(2);
|
||||
|
||||
@@ -528,6 +528,8 @@ function mountWorkspace() {
|
||||
}
|
||||
|
||||
mountWorkspace();
|
||||
// #525: web/workspace.html shows its legacy-page fallback at `load` unless the shell got this far.
|
||||
document.documentElement.dataset.workspaceBooted = 'true';
|
||||
window.addEventListener('pageshow', event => {
|
||||
if (event.persisted) mountWorkspace();
|
||||
});
|
||||
|
||||
@@ -25,6 +25,14 @@ export const routes = [
|
||||
description: 'Configuration, maintenance and recovery—with browsing kept in the workspace.', action: 'Open System' },
|
||||
];
|
||||
|
||||
/** D165 (#525): the legacy pages, which stay at their URLs; System's Overview section links them discreetly. */
|
||||
export const LEGACY_PAGES = Object.freeze([
|
||||
{ label: 'Dashboard', href: '/prompt_manager/admin' },
|
||||
{ label: 'Tags', href: '/prompt_manager/admin#/tags' },
|
||||
{ label: 'Gallery', href: '/prompt_manager/gallery' },
|
||||
{ label: 'Metadata viewer', href: '/prompt_manager/gallery.html' },
|
||||
].map(page => Object.freeze(page)));
|
||||
|
||||
const NATIVE_ROUTES = new Set(['images', 'library', 'tags', 'inspect', 'overview', 'saved', 'favorites', 'activity', 'review', 'system',
|
||||
'collection']);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* listeners and timers never outlive it. The drafts are module-level (Q10): they live for the page session.
|
||||
*/
|
||||
import { createButton, createScope } from '../shared/toolkit/core.js';
|
||||
import { SYSTEM_SECTIONS, navigateTo } from './state.mjs';
|
||||
import { LEGACY_PAGES, SYSTEM_SECTIONS, navigateTo } from './state.mjs';
|
||||
import { createSystemDrafts, DRAFT_DOMAINS, INVALID_SETTING_CODE } from './system-state.mjs';
|
||||
import { sectionNav } from './system-controls.mjs';
|
||||
import { mountOverview } from './system-overview.mjs';
|
||||
@@ -143,6 +143,22 @@ export const DELIVERED_SECTIONS = Object.freeze({
|
||||
* under one of this tab's drafts flags that draft (D92's conflict status), including a change made while System was
|
||||
* not mounted (the drafts outlive it, Q10).
|
||||
*/
|
||||
/** D165 (#525): one small line under System's Overview, the workspace's only visible way to the legacy pages. */
|
||||
function legacyPagesLine() {
|
||||
const line = el('p', 'small legacy-pages');
|
||||
line.append('Legacy pages: ');
|
||||
LEGACY_PAGES.forEach((page, index) => {
|
||||
const link = document.createElement('a');
|
||||
link.textContent = page.label;
|
||||
link.href = window.ApiClient.resolveUrl(page.href);
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener';
|
||||
if (index) line.append(' · ');
|
||||
line.append(link);
|
||||
});
|
||||
return line;
|
||||
}
|
||||
|
||||
export function mountSystem(main, api, scope, { section, activity, sections = DELIVERED_SECTIONS, navigate = navigateTo }) {
|
||||
const store = systemDrafts(api);
|
||||
jobs = activity;
|
||||
@@ -168,6 +184,7 @@ export function mountSystem(main, api, scope, { section, activity, sections = DE
|
||||
saveRow.append(status, save);
|
||||
layout.append(saveRow);
|
||||
main.append(nav, layout);
|
||||
const legacyPages = legacyPagesLine();
|
||||
|
||||
const paintStatus = () => {
|
||||
status.textContent = store.statusText();
|
||||
@@ -193,6 +210,8 @@ export function mountSystem(main, api, scope, { section, activity, sections = DE
|
||||
// Every section is delivered (S12), and the router only names known sections (state.mjs).
|
||||
const nodes = sections[next](layout, { api, activity, scope: sectionScope, drafts: store });
|
||||
layout.prepend(...nodes);
|
||||
if (next === 'overview') main.append(legacyPages);
|
||||
else legacyPages.remove();
|
||||
paintStatus();
|
||||
}
|
||||
show(section);
|
||||
|
||||
@@ -493,3 +493,4 @@ pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; margin-bloc
|
||||
}
|
||||
.more-menu-list[hidden] { display: none; }
|
||||
.more-menu-list > button { justify-content: flex-start; text-align: left; width: 100%; white-space: nowrap; }
|
||||
.legacy-pages { margin: 16px 0 0; color: var(--muted); /* D165 */ }
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
<script src="js/comfy-metadata.js" defer></script>
|
||||
<script src="js/gallery-settings.js" defer></script>
|
||||
<script src="js/png-metadata.js" defer></script>
|
||||
<script>
|
||||
// #525: no module of the workspace can show this. A failed fetch or a throw while starting leaves the mark
|
||||
// unset, and modules run before `load`.
|
||||
window.addEventListener('load', () => {
|
||||
if (!document.documentElement.dataset.workspaceBooted) document.getElementById('workspace-boot-failure').hidden = false;
|
||||
});
|
||||
</script>
|
||||
<script type="module" src="js/workspace/index.mjs"></script>
|
||||
</head>
|
||||
<body class="ca-tools">
|
||||
@@ -50,6 +57,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="workspace-toast" class="toast" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||
<div id="workspace-boot-failure" role="alert" hidden style="position: fixed; inset: 16px 16px auto; z-index: 1000; padding: 16px; border: 1px solid #888; border-radius: 8px; background: #1b1b1f; color: #eee; font: 14px/1.5 system-ui, sans-serif;">
|
||||
<p style="margin: 0;">The workspace couldn't load. Reload the page, or use a legacy page:
|
||||
<a style="color: inherit;" href="admin">Dashboard</a> ·
|
||||
<a style="color: inherit;" href="admin#/tags">Tags</a> ·
|
||||
<a style="color: inherit;" href="gallery">Gallery</a> ·
|
||||
<a style="color: inherit;" href="gallery.html">Metadata viewer</a></p>
|
||||
</div>
|
||||
<noscript><p>This workspace needs JavaScript. <a href="admin">Open the existing dashboard</a>.</p></noscript>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user