Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
196abf0502 | ||
|
|
e0fdde68b6 | ||
|
|
10f987fcdc | ||
|
|
901b7be156 | ||
|
|
ec06120a00 |
@@ -251,9 +251,9 @@ match Classic and Focused.
|
||||
- **PromptManagerText**: Look for "Prompt Manager Text" in the PromptManager/Text category - outputs STRING for any text node
|
||||
|
||||
5. **Access the web interface**:
|
||||
- **Admin Dashboard**: `http://localhost:8188/prompt_manager/admin` - Full management interface
|
||||
- **Metadata Viewer**: `http://localhost:8188/prompt_manager/metadata.html` - Standalone PNG analysis tool
|
||||
- **Simple Interface**: `http://localhost:8188/prompt_manager/` - Basic prompt browser
|
||||
- **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 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
|
||||
|
||||
@@ -324,9 +324,10 @@ The comprehensive gallery system provides complete access to all your ComfyUI ou
|
||||
|
||||
#### **📁 Accessing the Gallery**
|
||||
|
||||
1. **Open the admin interface** at `http://localhost:8188/prompt_manager/admin`
|
||||
2. **Click the "🖼️ Gallery" button** to open the output-wide gallery
|
||||
3. **Browse all content** from your ComfyUI output directory
|
||||
1. **Open the workspace's Images page** at `http://localhost:8188/prompt_manager/workspace#/images`
|
||||
2. **Browse all content** from your ComfyUI output directory
|
||||
|
||||
The legacy gallery stays at `http://localhost:8188/prompt_manager/gallery`.
|
||||
|
||||
#### **🎬 Video & Image Support**
|
||||
|
||||
|
||||
+26
-6
@@ -2641,8 +2641,8 @@ applied), D35 (singular captions), D36 (sorted categories), D40 (the continuatio
|
||||
the grid) and D43 (sorted chips, checked against the concept with its chips sorted).
|
||||
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; Overview's legacy card ("Open
|
||||
existing prompt library") and the sidebar still link `/prompt_manager/admin`
|
||||
"Current prompt library" link is removed from the shelf; the legacy Library stays at
|
||||
`/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`.
|
||||
|
||||
@@ -3117,8 +3117,8 @@ npm run --prefix tools/vendor test:workflows -- history-concept.spec.mjs \
|
||||
### Inspect a file page and recorded deviations (#249 local Inspect, PR L2)
|
||||
|
||||
`#/inspect` ("Inspect a file") is a native workspace route; it no longer hands off to
|
||||
the legacy metadata page, which stays reachable from Overview's "Open existing metadata
|
||||
inspector" card (`/prompt_manager/gallery.html`). The page is the concept's `#metadata`
|
||||
the legacy metadata page, which stays at its URL
|
||||
(`/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
|
||||
@@ -7029,8 +7029,8 @@ Deploying this backend needs a ComfyUI restart (Python and route changes).
|
||||
## Native vocabulary UI (#236)
|
||||
|
||||
`#/tags` ("Tags & categories", page title "Tags") is a native workspace route; it no
|
||||
longer hands off to the legacy tag manager. That page stays reachable from Overview's
|
||||
"Open existing tag manager" card for its own page; tag search and sort, the untagged
|
||||
longer hands off to the legacy tag manager. That page stays at
|
||||
`/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
|
||||
@@ -7152,3 +7152,23 @@ until acceptance. Not compared: hover states and the dialog focus ring (the page
|
||||
keyboard ring and link ring offsets are asserted). No coverage floor was lowered;
|
||||
`vocabulary-state.mjs`, `vocabulary.mjs` and `vocabulary-dialog.mjs` have additive
|
||||
floors in `frontend-coverage-baseline.json`.
|
||||
|
||||
## Cutover entry points (#525)
|
||||
|
||||
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, 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`).
|
||||
|
||||
@@ -7,7 +7,7 @@ test.beforeEach(async ({page, browserName}) => {
|
||||
page.classicErrors = [];
|
||||
page.on('pageerror', error => page.classicErrors.push(error.message));
|
||||
await page.route('**/scripts/app.js', route => route.fulfill({contentType:'text/javascript', body:'export const app = window.fixtureApp;'}));
|
||||
await page.route('**/scripts/api.js', route => route.fulfill({contentType:'text/javascript', body:'export const api = {fileURL: path => path, queuePrompt: async (...args) => args};'}));
|
||||
await page.route('**/scripts/api.js', route => route.fulfill({contentType:'text/javascript', body:'export const api = {fileURL: path => (window.fixtureApiBase ?? "") + path, queuePrompt: async (...args) => args};'}));
|
||||
await page.route('**/extensions/promptmanager/prompt_manager.js', route => route.fulfill({contentType:'text/javascript', body:readFileSync(new URL('../../web/comfy/prompt_manager.js', import.meta.url),'utf8')}));
|
||||
await page.goto('/prompt_manager/test');
|
||||
await page.evaluate(() => {
|
||||
@@ -64,6 +64,28 @@ test('Classic keeps immediate selection, search query and metadata serialization
|
||||
await expect(page.getByRole('button',{name:'🌐 Web UI',exact:true})).toBeVisible();
|
||||
});
|
||||
|
||||
test('Classic Web UI opens the workspace Library in the saved display mode, through the mount prefix (#525)',async({page})=>{
|
||||
await page.evaluate(()=>{window.fixtureApiBase='/mounted';window.open=(...args)=>{window.openArgs=args;return {};};});
|
||||
const webUi=page.getByRole('button',{name:'🌐 Web UI',exact:true});
|
||||
for (const [mode,args] of [
|
||||
['newtab',['/mounted/prompt_manager/workspace#/library','_blank']],
|
||||
['popup',['/mounted/prompt_manager/workspace#/library','_blank','width=1200,height=800,scrollbars=yes,resizable=yes']],
|
||||
]) {
|
||||
await page.evaluate(value=>{node.properties.webuiDisplayMode=value;},mode);
|
||||
await webUi.click();
|
||||
expect(await page.evaluate(()=>window.openArgs)).toEqual(args);
|
||||
}
|
||||
});
|
||||
|
||||
test('Classic Web UI reports a blocked popup and keeps the canvas open (#525)',async({page})=>{
|
||||
const before=page.url();
|
||||
await page.evaluate(()=>{window.open=()=>null;});
|
||||
await page.getByRole('button',{name:'🌐 Web UI',exact:true}).click();
|
||||
await expect(page.getByText('Allow popups to open Web UI, then try again.')).toBeVisible();
|
||||
expect(page.url()).toBe(before);
|
||||
await expect(page.locator('.pm-classic')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Classic detaches native text synchronization on removal',async({page})=>{
|
||||
// The page installs the text sync (serializeValue and its listeners) in a 50 ms timer; wait for it, not for time.
|
||||
await expect.poll(()=>page.evaluate(()=>typeof node.widgets.find(item=>item.name==='text').serializeValue)).toBe('function');
|
||||
|
||||
@@ -485,7 +485,7 @@ test(`${variant} respects the existing Web UI display preference`,async({page})=
|
||||
});
|
||||
await page.evaluate(()=>node._focusedSettingsReady);
|
||||
await page.getByRole('button',{name:'Web UI ↗',exact:true}).click();
|
||||
expect(await page.evaluate(()=>window.openArgs)).toEqual(['/prompt_manager/web','_blank']);
|
||||
expect(await page.evaluate(()=>window.openArgs)).toEqual(['/prompt_manager/workspace#/library','_blank']);
|
||||
});
|
||||
|
||||
test(`${variant} reports blocked Web UI popups without leaving the workflow`,async({page})=>{
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -11,12 +11,12 @@ const sidebarLink = (page, label) => page.locator('aside.sidebar nav').getByRole
|
||||
const sidebarLinks = page => page.locator('aside.sidebar nav a');
|
||||
const routes = [
|
||||
['overview', 'Overview', null],
|
||||
// D29: Images no longer links the legacy gallery; Overview's tool cards still do.
|
||||
// D29: Images no longer links the legacy gallery.
|
||||
['images', 'Images', null],
|
||||
// #249 E3: the shelf no longer links the legacy Library; Overview's tool card still does.
|
||||
// #249 E3: the shelf no longer links the legacy Library.
|
||||
['library', 'Prompt library', null],
|
||||
['tags', 'Tags & categories', null],
|
||||
// #249 local Inspect: the native Inspect a file page; Overview's tool card still links the legacy metadata page.
|
||||
// #249 local Inspect: the native Inspect a file page.
|
||||
['inspect', 'Inspect a file', null],
|
||||
// #249 System S2: native System; every section native since S12 (D94 retired, asserted below).
|
||||
['system', 'System', null],
|
||||
@@ -93,20 +93,106 @@ test('all shell destinations deep-link, reload and expose working existing appli
|
||||
await page.reload();
|
||||
await expect(main.getByRole('heading', { level: 1, name: title, exact: true })).toBeVisible();
|
||||
}
|
||||
// D29 and #249 E3: the legacy gallery and Library are reached from Overview's tool cards, not from their pages.
|
||||
// #525: Overview's cards open the native pages, and no workspace page links a legacy one.
|
||||
for (const [action, hash, title] of [
|
||||
['Open Images', '#/images', 'Images'],
|
||||
['Open Prompt library', '#/library', 'Prompt library'],
|
||||
['Open Tags & categories', '#/tags', 'Tags & categories'],
|
||||
['Open Inspect a file', '#/inspect', 'Inspect a file'],
|
||||
['Open System', '#/system/overview', 'System'],
|
||||
]) {
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
const main = page.locator('#workspace-main');
|
||||
const card = main.getByRole('link', { name: action, exact: true });
|
||||
await expect(card).toHaveAttribute('href', hash);
|
||||
await card.click();
|
||||
await expect(page).toHaveURL(new RegExp(`/prompt_manager/workspace${hash}$`));
|
||||
await expect(main.getByRole('heading', { level: 1, name: title, exact: true })).toBeVisible();
|
||||
}
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
const library = page.locator('#workspace-main').getByRole('link', { name: 'Open existing prompt library', exact: true });
|
||||
await expect(library).toHaveAttribute('href', '/prompt_manager/admin');
|
||||
await expect(await request.get('/prompt_manager/admin')).toBeOK();
|
||||
// The legacy metadata page stays reachable from Overview beside the native Inspect page.
|
||||
const metadata = page.locator('#workspace-main').getByRole('link', { name: 'Open existing metadata inspector', exact: true });
|
||||
await expect(metadata).toHaveAttribute('href', '/prompt_manager/gallery.html');
|
||||
await expect(await request.get('/prompt_manager/gallery.html')).toBeOK();
|
||||
const gallery = page.locator('#workspace-main').getByRole('link', { name: 'Open existing image gallery', exact: true });
|
||||
await expect(gallery).toHaveAttribute('href', '/prompt_manager/gallery');
|
||||
await expect(await request.get('/prompt_manager/gallery')).toBeOK();
|
||||
await gallery.click();
|
||||
await expect(page).toHaveURL(/\/prompt_manager\/gallery$/);
|
||||
await expect(page.locator('#workspace-main').getByRole('link', { name: 'Open Images', exact: true })).toBeVisible();
|
||||
await expect(page.locator('a[href*="/prompt_manager/admin"], a[href*="/prompt_manager/gallery"]')).toHaveCount(0);
|
||||
await expect(page.locator('#workspace-main').getByText('Your existing tools remain available')).toHaveCount(0);
|
||||
// The legacy pages themselves stay at their URLs.
|
||||
for (const legacy of ['/prompt_manager/admin', '/prompt_manager/gallery', '/prompt_manager/gallery.html']) {
|
||||
await expect(await request.get(legacy)).toBeOK();
|
||||
}
|
||||
});
|
||||
|
||||
test('/prompt_manager/web lands on the workspace Library and Back does not bounce through it (#525)', async ({ page }) => {
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await page.evaluate(() => { location.href = '/prompt_manager/web'; });
|
||||
await expect(page).toHaveURL(/\/prompt_manager\/workspace#\/library$/);
|
||||
const main = page.locator('#workspace-main');
|
||||
await expect(main.getByRole('heading', { level: 1, name: 'Prompt library', exact: true })).toBeVisible();
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(/\/prompt_manager\/workspace#\/overview$/);
|
||||
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 }) => {
|
||||
@@ -438,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');
|
||||
|
||||
@@ -210,14 +210,14 @@ test(`${label} follows actual ComfyUI light and dark palettes`, async ({ page },
|
||||
}
|
||||
});
|
||||
|
||||
test(`${label} Web UI button opens actual standalone pages`, async ({ page }) => {
|
||||
test(`${label} Web UI button opens the workspace; legacy pages still load`, async ({ page }) => {
|
||||
await addEditor(page, type);
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
await page.locator('.pm-focused').getByRole('button', { name: 'Web UI ↗', exact: true }).click();
|
||||
const popup = await popupPromise;
|
||||
await popup.waitForLoadState('domcontentloaded');
|
||||
await expect(popup).toHaveURL(/\/prompt_manager\/(web|admin)/);
|
||||
await expect(popup.locator('body')).toContainText('Prompt');
|
||||
await expect(popup).toHaveURL(/\/prompt_manager\/workspace#\/library$/);
|
||||
await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library');
|
||||
await expect(page.locator('.pm-focused').getByRole('status').filter({ hasText: 'Web UI opened.' })).toHaveText('Web UI opened.');
|
||||
await popup.goto(new URL('/prompt_manager/gallery', page.url()).href);
|
||||
await expect(popup.locator('#addPromptBtn')).toBeVisible();
|
||||
|
||||
@@ -84,7 +84,7 @@ test('all node schemas and extension entrypoints load from the real host', async
|
||||
});
|
||||
|
||||
for (const type of ['PromptManager', 'PromptManagerText']) {
|
||||
test(`${type} Web UI button opens actual standalone pages`, async ({ page }) => {
|
||||
test(`${type} Web UI button opens the workspace; legacy pages still load`, async ({ page }) => {
|
||||
await page.evaluate(async type => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
const node = LiteGraph.createNode(type);
|
||||
@@ -95,14 +95,74 @@ for (const type of ['PromptManager', 'PromptManagerText']) {
|
||||
await page.getByRole('button', { name: '🌐 Web UI', exact: true }).click();
|
||||
const popup = await popupPromise;
|
||||
await popup.waitForLoadState('domcontentloaded');
|
||||
await expect(popup).toHaveURL(/\/prompt_manager\/(web|admin)/);
|
||||
await expect(popup.locator('body')).toContainText('Prompt');
|
||||
await expect(popup).toHaveURL(/\/prompt_manager\/workspace#\/library$/);
|
||||
await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library');
|
||||
await popup.goto(new URL('/prompt_manager/gallery', page.url()).href);
|
||||
await expect(popup.locator('#addPromptBtn')).toBeVisible();
|
||||
await popup.close();
|
||||
});
|
||||
}
|
||||
|
||||
test('top-bar PM button opens the workspace in either renderer', async ({ page }) => {
|
||||
const button = page.locator('.promptmanager-top-button-group button, #pm-toolbar-btn').first();
|
||||
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();
|
||||
});
|
||||
|
||||
for (const [type, buttonName] of [['PromptManager', '🌐 Web UI'], ['PromptManagerFocused', 'Web UI ↗'], ['PromptManagerPair', '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');
|
||||
|
||||
@@ -37,11 +37,11 @@ test('workspace serves real library data beside either ComfyUI renderer', async
|
||||
await expect(workspace.getByText('No matching images · 0 images', { exact: true })).toBeVisible();
|
||||
await expect(workspace.locator('.gallery-body > .empty')).toHaveText('No matching images. Try another search or category.');
|
||||
}
|
||||
// D29: Images no longer links the legacy gallery; Overview's "Open existing image gallery" card does.
|
||||
// D29: Images no longer links the legacy gallery; #525: nor does Overview. The page stays at its URL.
|
||||
await expect(workspace.locator('#workspace-main a[href="/prompt_manager/gallery"]')).toHaveCount(0);
|
||||
await workspace.evaluate(() => { location.hash = '/overview'; });
|
||||
const gallery = workspace.locator('#workspace-main').getByRole('link', { name: 'Open existing image gallery', exact: true });
|
||||
await expect(gallery).toHaveAttribute('href', '/prompt_manager/gallery');
|
||||
const imagesCard = workspace.locator('#workspace-main').getByRole('link', { name: 'Open Images', exact: true });
|
||||
await expect(imagesCard).toHaveAttribute('href', '#/images');
|
||||
expect((await workspace.request.get('/prompt_manager/gallery')).ok()).toBe(true);
|
||||
await workspace.evaluate(() => { location.hash = '/images'; });
|
||||
await workspace.reload();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// #525: where the canvas buttons open PromptManager (web/js/workspace-entry.js).
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const source = import('../../web/js/workspace-entry.js');
|
||||
const state = import('../../web/js/workspace/state.mjs');
|
||||
|
||||
test('the canvas entry route is the workspace Library, a native route', async () => {
|
||||
const { WORKSPACE_ENTRY_ROUTE } = await source;
|
||||
const { resolveRoute } = await state;
|
||||
assert.equal(WORKSPACE_ENTRY_ROUTE, '/prompt_manager/workspace#/library');
|
||||
const [pathname, hash] = WORKSPACE_ENTRY_ROUTE.split('#');
|
||||
assert.equal(pathname, '/prompt_manager/workspace');
|
||||
assert.equal(resolveRoute(`#${hash}`).id, 'library');
|
||||
});
|
||||
|
||||
test('/prompt_manager/web redirects to the same route, under any mount prefix', async () => {
|
||||
const { WORKSPACE_ENTRY_ROUTE } = await source;
|
||||
const html = fs.readFileSync(path.join(__dirname, '../../web/index.html'), 'utf8');
|
||||
const [, pattern, flags, replacement] = html.match(/location\.replace\(\s*window\.location\.pathname\.replace\(\/(.+)\/([a-z]*), '([^']+)'\)/);
|
||||
const redirect = pathname => pathname.replace(new RegExp(pattern, flags), replacement);
|
||||
assert.equal(redirect('/prompt_manager/web'), WORKSPACE_ENTRY_ROUTE);
|
||||
assert.equal(redirect('/prompt_manager/web/'), WORKSPACE_ENTRY_ROUTE);
|
||||
assert.equal(redirect('/prompt_manager/index.html'), WORKSPACE_ENTRY_ROUTE);
|
||||
assert.equal(redirect('/sub/prompt_manager/web'), `/sub${WORKSPACE_ENTRY_ROUTE}`);
|
||||
});
|
||||
|
||||
test('no canvas entry point still names a legacy page', () => {
|
||||
for (const file of ['web/comfy/prompt_manager.js', 'web/js/focused-node.js', 'web/js/classic-node.js']) {
|
||||
const code = fs.readFileSync(path.join(__dirname, '../..', file), 'utf8');
|
||||
assert.doesNotMatch(code, /\/prompt_manager\/(?:admin|gallery|web)\b/, file);
|
||||
}
|
||||
});
|
||||
@@ -17,7 +17,7 @@ test('tags is a native route titled Tags; only unbuilt routes hand off to legacy
|
||||
const tags = resolveRoute('#/tags');
|
||||
assert.equal(tags.label, 'Tags & categories');
|
||||
assert.equal(routeTitle(tags), 'Tags');
|
||||
assert.equal(tags.href, '/prompt_manager/admin#/tags', 'Overview keeps the existing tag manager card');
|
||||
assert.equal(tags.href, '#/tags', 'Overview opens the native page (#525)');
|
||||
for (const id of ['images', 'library', 'inspect', 'overview', 'system']) {
|
||||
const route = resolveRoute(`#/${id}`);
|
||||
assert.equal(routeTitle(route), route.label);
|
||||
@@ -48,7 +48,6 @@ test('system resolves its sections; #/overview stays the production hub', async
|
||||
// Q12: Overview's System card opens the native System Overview, with no legacy href or action.
|
||||
const card = routes.find(route => route.id === 'system');
|
||||
assert.equal(card.href, '#/system/overview');
|
||||
assert.equal(card.native, true);
|
||||
assert.equal(card.action, 'Open System');
|
||||
});
|
||||
|
||||
@@ -77,9 +76,28 @@ test('inspect is the native Inspect a file page with the concept description', a
|
||||
// The concept's heading description (app.js renderMetadata 743).
|
||||
assert.equal(inspect.description, 'A standalone file uses the same image viewer and prompt actions.');
|
||||
assert.equal(isHandoff(inspect), false);
|
||||
// Overview keeps its card for the legacy metadata page, which stays working.
|
||||
assert.equal(inspect.href, '/prompt_manager/gallery.html');
|
||||
assert.equal(inspect.action, 'Open existing metadata inspector');
|
||||
// Overview's card opens the native page (#525); the legacy metadata page stays at its URL.
|
||||
assert.equal(inspect.href, '#/inspect');
|
||||
assert.equal(inspect.action, 'Open Inspect a file');
|
||||
});
|
||||
|
||||
test('every Overview card opens its own native page, never a legacy one (#525)', async () => {
|
||||
const { resolveRoute, routes } = await source;
|
||||
const cards = routes.filter(route => route.href);
|
||||
assert.deepEqual(cards.map(card => card.id), ['images', 'library', 'tags', 'inspect', 'system']);
|
||||
for (const card of cards) {
|
||||
assert.match(card.href, /^#\//, card.id);
|
||||
assert.equal(resolveRoute(card.href).id, card.id);
|
||||
assert.match(card.action, /^Open /, card.id);
|
||||
assert.doesNotMatch(card.action, /existing/, card.id);
|
||||
}
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
||||
Vendored
+2
-1
@@ -25,7 +25,8 @@ test('execution script discovers only the new matrix and preserves all 78 origin
|
||||
// + local Inspect host case (inspect-sender.spec.mjs, 2 projects), + the S10 renderer case (prompt-bridge.spec.mjs, 2 projects)
|
||||
// + #343 linked text: five node types, PromptSearchList, the pre-#343 workflows, legacy Primitive, moved link,
|
||||
// paste/clone/undo, wildcard Primitive, subgraph Primitive (text-link.spec.mjs, 2 projects)
|
||||
assert.match(stock, /Total: 110 tests in 9 files/);
|
||||
// + #525 the PM button and three nodes' Web UI after renderer switches and reload (prompt-manager.spec.mjs, 2 projects)
|
||||
assert.match(stock, /Total: 118 tests in 9 files/);
|
||||
assert.doesNotMatch(stock, /execution-history/);
|
||||
assert.doesNotMatch(stock, /execution-outputs/);
|
||||
assert.doesNotMatch(stock, /library-location/); // it starts its own fresh host (#249 S6b)
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"functions": 98,
|
||||
"statements": 98
|
||||
},
|
||||
"workspace-entry.js": {
|
||||
"lines": 80,
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"statements": 80
|
||||
},
|
||||
"save-to-library.js": {
|
||||
"lines": 98,
|
||||
"branches": 98,
|
||||
|
||||
@@ -7,6 +7,7 @@ const { installFocusedNode, installSplitNode, installPairNode } = await import(a
|
||||
const { installPromptBridge } = await import(api.fileURL('/prompt_manager/js/prompt-bridge-host.js'));
|
||||
const { collapseLinkedWidget, isLinked, linkNoteText, primitiveTextValue, storedWidgetValue } = await import(api.fileURL('/prompt_manager/js/text-links.js'));
|
||||
const { normalizeSaveWidget } = await import(api.fileURL('/prompt_manager/js/save-to-library.js'));
|
||||
const { WORKSPACE_ENTRY_ROUTE } = await import(api.fileURL('/prompt_manager/js/workspace-entry.js'));
|
||||
const promptManagerTypes = new Set(['PromptManager', 'PromptManagerText', 'PromptManagerFocused', 'PromptManagerSplit', 'PromptManagerPair']);
|
||||
|
||||
// ============================================================================
|
||||
@@ -109,7 +110,7 @@ function addPMTopBarButton() {
|
||||
// Create PM button
|
||||
const pmButton = $el("button", {
|
||||
classList: "comfyui-button comfyui-menu-mobile-collapse primary",
|
||||
title: "PromptManager - Open Admin Dashboard",
|
||||
title: "PromptManager - Open workspace",
|
||||
style: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -125,7 +126,7 @@ function addPMTopBarButton() {
|
||||
transition: "background 0.2s",
|
||||
},
|
||||
onclick: () => {
|
||||
window.open(api.fileURL("/prompt_manager/admin"), "_blank");
|
||||
window.open(api.fileURL(WORKSPACE_ENTRY_ROUTE), "_blank");
|
||||
},
|
||||
onmouseenter: (e) => { e.target.style.background = "#0d9488"; },
|
||||
onmouseleave: (e) => { e.target.style.background = "#14b8a6"; },
|
||||
@@ -161,7 +162,7 @@ function addPMTopBarButtonLegacy() {
|
||||
const btn = document.createElement("button");
|
||||
btn.id = "pm-toolbar-btn";
|
||||
btn.innerText = "PM";
|
||||
btn.title = "Open PromptManager Admin Dashboard";
|
||||
btn.title = "Open PromptManager workspace";
|
||||
|
||||
// Teal button styling
|
||||
btn.style.cssText = `
|
||||
@@ -181,7 +182,7 @@ function addPMTopBarButtonLegacy() {
|
||||
btn.onmouseleave = () => btn.style.background = "#14b8a6";
|
||||
|
||||
btn.onclick = () => {
|
||||
window.open(api.fileURL("/prompt_manager/admin"), "_blank");
|
||||
window.open(api.fileURL(WORKSPACE_ENTRY_ROUTE), "_blank");
|
||||
};
|
||||
|
||||
topBar.appendChild(btn);
|
||||
@@ -1184,9 +1185,7 @@ app.registerExtension({
|
||||
// Method to open web interface
|
||||
nodeType.prototype.openWebInterface = function () {
|
||||
try {
|
||||
// Open the web interface served via our API endpoint
|
||||
const currentOrigin = window.location.origin;
|
||||
const webUrl = `${currentOrigin}/prompt_manager/web`;
|
||||
const webUrl = api.fileURL(WORKSPACE_ENTRY_ROUTE);
|
||||
|
||||
if (this.properties.webuiDisplayMode === "newtab") {
|
||||
// Open in new tab without window options
|
||||
@@ -1198,7 +1197,8 @@ app.registerExtension({
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
window.location.href = webUrl;
|
||||
// Never navigate the canvas away: it may hold unsaved work.
|
||||
this.showNotification("Allow popups to open Web UI, then try again.", "error");
|
||||
}
|
||||
} else {
|
||||
// Default popup mode with specific dimensions
|
||||
@@ -1211,7 +1211,8 @@ app.registerExtension({
|
||||
if (newWindow) {
|
||||
this.showNotification("Web interface opened in popup", "success");
|
||||
} else {
|
||||
window.location.href = webUrl;
|
||||
// Never navigate the canvas away: it may hold unsaved work.
|
||||
this.showNotification("Allow popups to open Web UI, then try again.", "error");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PromptManager - Redirecting to Admin</title>
|
||||
<title>PromptManager - Opening the workspace</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
@@ -49,13 +49,13 @@
|
||||
<div class="container">
|
||||
<div class="spinner"></div>
|
||||
<h1>🔍 PromptManager</h1>
|
||||
<p>Redirecting to Admin Dashboard...</p>
|
||||
<p><small>Loading the full-featured prompt management interface</small></p>
|
||||
<p>Opening the workspace...</p>
|
||||
<noscript><p><a href="workspace#/library">Open the workspace</a></p></noscript>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Redirect to admin interface immediately
|
||||
window.location.href = window.location.pathname.replace(/\/(?:web|index\.html)\/?$/, '/admin');
|
||||
// Replace, so Back doesn't return here and bounce. Same route as web/js/workspace-entry.js (#525).
|
||||
window.location.replace(window.location.pathname.replace(/\/(?:web|index\.html)\/?$/, '/workspace#/library'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {createScope, createButton, createField, mountTheme} from './shared/toolkit/core.js';
|
||||
import {createPromptLibrary} from './prompt-library.js';
|
||||
import {WORKSPACE_ENTRY_ROUTE} from './workspace-entry.js';
|
||||
import {attachMetadataAutocomplete} from './metadata-autocomplete.js';
|
||||
import {exposeTextSockets, linkNoteText, preferTextInputs, textInputAtPoint} from './text-links.js';
|
||||
import {SAVE_WIDGET, normalizeSaveWidget, placeSaveWidgetLast, saveWidget, savesToLibrary, setSaveToLibrary} from './save-to-library.js';
|
||||
@@ -253,7 +254,7 @@ export function installFocusedNode(nodeType, app, api, parseQuery, {layout = 'fo
|
||||
}
|
||||
});
|
||||
scope.listen(web,'click',() => {
|
||||
const url = api.fileURL('/prompt_manager/web');
|
||||
const url = api.fileURL(WORKSPACE_ENTRY_ROUTE);
|
||||
const opened = webDisplayMode === 'newtab' ? window.open(url,'_blank') : window.open(url,'_blank','width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||
footer.setAttribute('role',opened ? 'status' : 'alert');
|
||||
footer.textContent = opened ? 'Web UI opened.' : 'Allow popups to open Web UI, then try again.';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// Where the canvas's PM and Web UI buttons open PromptManager (#525). web/index.html redirects to the same route.
|
||||
export const WORKSPACE_ENTRY_ROUTE = '/prompt_manager/workspace#/library';
|
||||
@@ -34,10 +34,9 @@ function element(tag, text, className) {
|
||||
return node;
|
||||
}
|
||||
|
||||
function legacyLink(route) {
|
||||
function cardLink(route) {
|
||||
const link = element('a', route.action, 'workspace-link');
|
||||
// A native card (System, Q12) links its workspace hash; a legacy tool's page goes through the mount prefix.
|
||||
link.href = route.native ? route.href : resolveUrl(route.href);
|
||||
link.href = route.href;
|
||||
return link;
|
||||
}
|
||||
|
||||
@@ -485,11 +484,11 @@ function mountWorkspace() {
|
||||
stats.setAttribute('aria-label', 'Library statistics');
|
||||
main.append(stats);
|
||||
const tools = element('section', undefined, 'surface');
|
||||
tools.append(element('h2', 'Continue creating'), element('p', 'Your existing tools remain available while the new workspaces take shape.'));
|
||||
tools.append(element('h2', 'Continue creating'), element('p', 'Pick up where you left off in your images, prompts and tools.'));
|
||||
const cards = element('div', undefined, 'tool-grid');
|
||||
for (const target of routes.filter(item => item.href)) {
|
||||
const card = element('article', undefined, 'tool-card');
|
||||
card.append(element('h3', target.label), element('p', target.description, 'small'), legacyLink(target));
|
||||
card.append(element('h3', target.label), element('p', target.description, 'small'), cardLink(target));
|
||||
cards.append(card);
|
||||
}
|
||||
tools.append(cards);
|
||||
@@ -529,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();
|
||||
});
|
||||
|
||||
+22
-14
@@ -1,14 +1,15 @@
|
||||
export const routes = [
|
||||
{ id: 'images', label: 'Images', href: '/prompt_manager/gallery',
|
||||
description: 'Browse your images, inspect their metadata and review generated tags.', action: 'Open existing image gallery' },
|
||||
{ id: 'library', label: 'Prompt library', href: '/prompt_manager/admin',
|
||||
description: 'Find, edit and organize the prompts already in your library.', action: 'Open existing prompt library' },
|
||||
// Native screen; `title` is the concept's breadcrumb/title, the href stays for Overview's card.
|
||||
{ id: 'tags', label: 'Tags & categories', title: 'Tags', href: '/prompt_manager/admin#/tags',
|
||||
description: 'Explore your vocabulary and find the prompts associated with each tag.', action: 'Open existing tag manager' },
|
||||
// Native screen (#249 local Inspect); the href stays for Overview's card to the legacy metadata page.
|
||||
{ id: 'inspect', label: 'Inspect a file', href: '/prompt_manager/gallery.html',
|
||||
description: 'A standalone file uses the same image viewer and prompt actions.', action: 'Open existing metadata inspector' },
|
||||
// `href` and `action` are Overview's card (#525): every card opens its native page, never a legacy one.
|
||||
{ id: 'images', label: 'Images', href: '#/images',
|
||||
description: 'Browse your images, inspect their metadata and review generated tags.', action: 'Open Images' },
|
||||
{ id: 'library', label: 'Prompt library', href: '#/library',
|
||||
description: 'Find, edit and organize the prompts already in your library.', action: 'Open Prompt library' },
|
||||
// `title` is the concept's breadcrumb/title.
|
||||
{ id: 'tags', label: 'Tags & categories', title: 'Tags', href: '#/tags',
|
||||
description: 'Explore your vocabulary and find the prompts associated with each tag.', action: 'Open Tags & categories' },
|
||||
// #249 local Inspect.
|
||||
{ id: 'inspect', label: 'Inspect a file', href: '#/inspect',
|
||||
description: 'A standalone file uses the same image viewer and prompt actions.', action: 'Open Inspect a file' },
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
// SAVED VIEWS' first row (#249 D51): Images filtered to favorites. No href, so Overview lists no card for it.
|
||||
{ id: 'favorites', label: 'Favorites' },
|
||||
@@ -18,13 +19,20 @@ export const routes = [
|
||||
{ id: 'review', label: 'Needs review', title: 'Review' },
|
||||
// The concept's Activity (app.js renderActivity 732–738): this server session's jobs. No href: no Overview card.
|
||||
{ id: 'activity', label: 'Activity' },
|
||||
// Native System (#249 System S2): `#/system/<section>`. Q12: Overview's card opens its Overview section (`native`:
|
||||
// a workspace hash, not a legacy page); every section is native (S12 retired D94's hand-off). The description is
|
||||
// the concept's System heading (app.js renderSystem 700).
|
||||
{ id: 'system', label: 'System', href: '#/system/overview', native: true,
|
||||
// Native System (#249 System S2): `#/system/<section>`. Q12: Overview's card opens its Overview section; every
|
||||
// section is native (S12 retired D94's hand-off). The description is the concept's System heading (app.js renderSystem 700).
|
||||
{ id: 'system', label: 'System', href: '#/system/overview',
|
||||
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,6 +493,7 @@ 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 */ }
|
||||
|
||||
/* D164 (#524): the prompt page has four heading actions and titles of any length, which the concept never shows. From
|
||||
the concept's sidebar breakpoint up the actions keep one row and the title takes what is left, clamped to two lines
|
||||
|
||||
@@ -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