Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87166539e8 | ||
|
|
23c01455fa |
@@ -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**:
|
||||
- **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)
|
||||
- **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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -324,10 +324,9 @@ The comprehensive gallery system provides complete access to all your ComfyUI ou
|
||||
|
||||
#### **📁 Accessing the Gallery**
|
||||
|
||||
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`.
|
||||
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
|
||||
|
||||
#### **🎬 Video & Image Support**
|
||||
|
||||
|
||||
+13
-26
@@ -1402,6 +1402,13 @@ seeds two seedless files (`seedless_images` on `/__test__/concept/reset`, with
|
||||
Tag) and asserts that every row reserves the tallest footer and that their own footer
|
||||
is shorter than the row it reserves.
|
||||
|
||||
### Overview counts (#547)
|
||||
|
||||
Each Overview card is the count of the page it names: the shell's Library, Images and Tags sidebar counts, one source,
|
||||
so a card and its badge cannot differ. `/prompt_manager/stats` is for the legacy page and counts raw rows. A visit re-reads
|
||||
the three unless a read is in flight. An unread or failed card shows a dash; Retry re-reads only the failed counts.
|
||||
Tests: `tests/js/workspace-state.test.js`, `tests/browser/workspace.spec.mjs`, `tests/comfyui/workspace.spec.mjs`.
|
||||
|
||||
### Images page controls and recorded deviations (#249 PR C)
|
||||
|
||||
The Images page renders the concept's DOM with real data. Its controls, top to bottom:
|
||||
@@ -2641,8 +2648,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; the legacy Library stays at
|
||||
`/prompt_manager/admin` by direct URL, linked only from System (#525, D165)
|
||||
"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`
|
||||
(`tests/browser/workspace.spec.mjs`). `library comparison detects
|
||||
mutations` injects `.card-footer { padding: 2px }` and removes `.card-meta .chip.red`.
|
||||
|
||||
@@ -3117,8 +3124,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 at its URL
|
||||
(`/prompt_manager/gallery.html`) linked only from System (#525, D165). The page is the concept's `#metadata`
|
||||
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`
|
||||
(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 +7036,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 at
|
||||
`/prompt_manager/admin#/tags` linked only from System (#525, D165); tag search and sort, the untagged
|
||||
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
|
||||
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,23 +7159,3 @@ 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 => (window.fixtureApiBase ?? "") + path, queuePrompt: async (...args) => args};'}));
|
||||
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('**/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,28 +64,6 @@ 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/workspace#/library','_blank']);
|
||||
expect(await page.evaluate(()=>window.openArgs)).toEqual(['/prompt_manager/web','_blank']);
|
||||
});
|
||||
|
||||
test(`${variant} reports blocked Web UI popups without leaving the workflow`,async({page})=>{
|
||||
|
||||
@@ -165,8 +165,6 @@ 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 = {
|
||||
@@ -259,7 +257,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', 'tree.3', 'tree.length',
|
||||
'save.button.cursor', 'save.button.opacity', 'save.status.text',
|
||||
].sort();
|
||||
|
||||
test('system comparison detects mutations', async ({ page, request }) => {
|
||||
|
||||
@@ -602,7 +602,8 @@ async function staleSourceRecovery(page, request, roots) {
|
||||
const pages = [], inspections = [], options = [], mutations = [];
|
||||
page.on('request', request => {
|
||||
const url = new URL(request.url());
|
||||
if (url.pathname.endsWith('/images/output')) pages.push(url);
|
||||
// limit=1 is a sidebar count read (Overview re-reads it on a visit, #547), not a page of images.
|
||||
if (url.pathname.endsWith('/images/output') && url.searchParams.get('limit') !== '1') pages.push(url);
|
||||
if (url.pathname.includes('/images/inspect/')) inspections.push(url);
|
||||
if (request.method() !== 'GET') mutations.push(url.pathname);
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ test('failed library reads retry while late search responses cannot overwrite ne
|
||||
// D153 (#447): a Browse visit that still names the retired "Highest rated" sort opens the shelf on Recently added.
|
||||
test('a Library visit with the retired rating sort opens on Recently added',async({page})=>{
|
||||
await page.goto(route);await expect(cards(page)).toHaveCount(3);
|
||||
const sorts=[];page.on('request',item=>{const url=new URL(item.url());if(url.pathname==='/prompt_manager/library')sorts.push(url.searchParams.get('sort'));});
|
||||
const sorts=[];page.on('request',item=>{const url=new URL(item.url());if(url.pathname==='/prompt_manager/library'&&url.searchParams.get('limit')!=='1')sorts.push(url.searchParams.get('sort'));});// limit=1 is a sidebar count read, not the shelf
|
||||
await page.goto('/prompt_manager/workspace#/overview');await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
|
||||
await page.evaluate(async()=>{const {browseLibrary}=await import('/prompt_manager/js/workspace/library.mjs');browseLibrary({sort:'rating'});});
|
||||
await expect(cards(page)).toHaveCount(3);
|
||||
|
||||
@@ -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.
|
||||
// D29: Images no longer links the legacy gallery; Overview's tool cards still do.
|
||||
['images', 'Images', null],
|
||||
// #249 E3: the shelf no longer links the legacy Library.
|
||||
// #249 E3: the shelf no longer links the legacy Library; Overview's tool card still does.
|
||||
['library', 'Prompt library', null],
|
||||
['tags', 'Tags & categories', null],
|
||||
// #249 local Inspect: the native Inspect a file page.
|
||||
// #249 local Inspect: the native Inspect a file page; Overview's tool card still links the legacy metadata page.
|
||||
['inspect', 'Inspect a file', null],
|
||||
// #249 System S2: native System; every section native since S12 (D94 retired, asserted below).
|
||||
['system', 'System', null],
|
||||
@@ -60,6 +60,10 @@ test.afterEach(async ({ page, browserName }) => {
|
||||
|
||||
test('workspace starts with real persisted statistics without mutating data', async ({ page, request }) => {
|
||||
const before = await (await request.get('/prompt_manager/stats')).json();
|
||||
const total = async path => (await (await request.get(`/prompt_manager/${path}`)).json()).total;
|
||||
const counts = [['Prompts', await total('library?limit=1')],
|
||||
['Images', await total('images/output?mode=workspace&limit=1')],
|
||||
['Tags', await total('vocabulary/tags?limit=1')]];
|
||||
const mutations = [];
|
||||
page.on('request', request => {
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method())) mutations.push(request.url());
|
||||
@@ -69,10 +73,11 @@ test('workspace starts with real persisted statistics without mutating data', as
|
||||
await expect(page.getByRole('navigation', { name: 'Workspace', exact: true })).toBeVisible();
|
||||
const main = page.locator('#workspace-main');
|
||||
await expect(main.getByRole('heading', { level: 1, name: 'Overview', exact: true })).toBeVisible();
|
||||
for (const [label, key] of [['Prompts', 'total_prompts'], ['Images', 'total_images'], ['Tags', 'total_tags']]) {
|
||||
await expect(statistic(page, label)).toHaveText(String(before.stats[key]));
|
||||
for (const [label, count] of counts) await expect(statistic(page, label)).toHaveText(String(count));
|
||||
// #547: a card and the sidebar badge of the page it names show one number.
|
||||
for (const [route, count] of [['library', counts[0][1]], ['images', counts[1][1]], ['tags', counts[2][1]]]) {
|
||||
await expect(page.locator(`aside.sidebar nav [data-route="${route}"] .nav-count`)).toHaveText(String(count));
|
||||
}
|
||||
expect(before.stats.total_prompts).toBe(2);
|
||||
expect((await (await request.get('/prompt_manager/stats')).json()).stats).toEqual(before.stats);
|
||||
expect(mutations).toEqual([]);
|
||||
await expect(page.locator('iframe')).toHaveCount(0);
|
||||
@@ -93,106 +98,20 @@ 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();
|
||||
}
|
||||
// #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();
|
||||
}
|
||||
// D29 and #249 E3: the legacy gallery and Library are reached from Overview's tool cards, not from their pages.
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
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;
|
||||
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$/);
|
||||
});
|
||||
|
||||
test('keyboard navigation, browser history and unknown routes remain usable', async ({ page }) => {
|
||||
@@ -373,25 +292,65 @@ test('unavailable host palette falls back to the system theme and tracks system
|
||||
});
|
||||
|
||||
test('overview reports loading, offers retry after failure and renders real empty statistics', async ({ page }) => {
|
||||
const sources = ['**/prompt_manager/library?limit=1', '**/prompt_manager/images/output?mode=workspace&limit=1',
|
||||
'**/prompt_manager/vocabulary/tags?limit=1'];
|
||||
let release;
|
||||
const pending = new Promise(resolve => { release = resolve; });
|
||||
await page.route('**/prompt_manager/stats', async route => {
|
||||
await pending;
|
||||
await route.fulfill({ status: 503, json: { success: false, error: 'Unavailable' } });
|
||||
});
|
||||
for (const source of sources) {
|
||||
await page.route(source, async route => {
|
||||
await pending;
|
||||
await route.fulfill({ status: 503, json: { success: false, error: 'Unavailable' } });
|
||||
});
|
||||
}
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await expect(page.locator('#workspace-main').getByRole('status')).toBeVisible();
|
||||
release();
|
||||
await expect(page.locator('#workspace-main').getByRole('alert')).toBeVisible();
|
||||
await page.unroute('**/prompt_manager/stats');
|
||||
await page.route('**/prompt_manager/stats', route => route.fulfill({
|
||||
json: { success: true, stats: { total_prompts: 0, total_images: 0, total_tags: 0 } },
|
||||
}));
|
||||
for (const source of sources) {
|
||||
await page.unroute(source);
|
||||
await page.route(source, route => route.fulfill({ json: { success: true, total: 0, items: [], images: [], tags: [] } }));
|
||||
}
|
||||
await page.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect(page.locator('#workspace-main').getByRole('alert')).toHaveCount(0);
|
||||
for (const label of ['Prompts', 'Images', 'Tags']) {
|
||||
await expect(statistic(page, label)).toHaveText('0');
|
||||
}
|
||||
await expect(page.locator('#workspace-main .empty-state')).toBeVisible();
|
||||
});
|
||||
|
||||
test('a slow or failed Images read leaves the other Overview cards and retries only itself', async ({ page }) => {
|
||||
const images = '**/prompt_manager/images/output?mode=workspace&limit=1';
|
||||
let release;
|
||||
const pending = new Promise(resolve => { release = resolve; });
|
||||
await page.route(images, async route => {
|
||||
await pending;
|
||||
await route.fulfill({ status: 503, json: { success: false, error: 'Unavailable' } });
|
||||
});
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await expect(statistic(page, 'Prompts')).toHaveText('2');
|
||||
await expect(statistic(page, 'Images')).toHaveText('–');
|
||||
await expect(page.locator('#workspace-main').getByRole('status')).toBeVisible();
|
||||
release();
|
||||
await expect(page.locator('#workspace-main').getByRole('alert')).toBeVisible();
|
||||
await expect(statistic(page, 'Prompts')).toHaveText('2');
|
||||
await page.unroute(images);
|
||||
let libraryReads = 0;
|
||||
page.on('request', request => { if (new URL(request.url()).pathname === '/prompt_manager/library') libraryReads++; });
|
||||
await page.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect(page.locator('#workspace-main').getByRole('alert')).toHaveCount(0);
|
||||
await expect(statistic(page, 'Images')).toHaveText(/^\d+$/);
|
||||
expect(libraryReads).toBe(0);
|
||||
});
|
||||
|
||||
test('opening Overview reads each count once beside the sidebar', async ({ page }) => {
|
||||
let reads = 0;
|
||||
page.on('request', request => {
|
||||
const url = new URL(request.url());
|
||||
if (url.pathname === '/prompt_manager/images/output' && url.searchParams.get('favorites') === null) reads++;
|
||||
});
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await expect(statistic(page, 'Images')).toHaveText(/^\d+$/);
|
||||
expect(reads).toBe(1);
|
||||
});
|
||||
|
||||
test('small screens keep navigation and the theme accessible without horizontal page overflow', async ({ page }) => {
|
||||
@@ -410,11 +369,9 @@ test('leaving a loading overview prevents its delayed result from replacing the
|
||||
let completed;
|
||||
const pending = new Promise(resolve => { release = resolve; });
|
||||
const finished = new Promise(resolve => { completed = resolve; });
|
||||
await page.route('**/prompt_manager/stats', async route => {
|
||||
await page.route('**/prompt_manager/library?limit=1', async route => {
|
||||
await pending;
|
||||
await route.fulfill({ json: {
|
||||
success: true, stats: { total_prompts: 999, total_images: 888, total_tags: 777 },
|
||||
} }).catch(() => {});
|
||||
await route.fulfill({ json: { success: true, total: 999, items: [] } }).catch(() => {});
|
||||
completed();
|
||||
});
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
@@ -437,7 +394,7 @@ test('leaving a loading overview prevents its delayed result from replacing the
|
||||
test('cached-page restoration remounts one working shell and refreshes its data', async ({ page }) => {
|
||||
let reads = 0;
|
||||
page.on('request', request => {
|
||||
if (new URL(request.url()).pathname === '/prompt_manager/stats') reads++;
|
||||
if (new URL(request.url()).pathname === '/prompt_manager/vocabulary/tags') reads++;
|
||||
});
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await expect(statistic(page, 'Prompts')).toHaveText('2');
|
||||
@@ -524,7 +481,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', 'p.small legacy-pages']);
|
||||
.toEqual(['div.heading', 'div.section-nav', 'div.settings-layout']);
|
||||
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 the workspace; legacy pages still load`, async ({ page }) => {
|
||||
test(`${label} Web UI button opens actual standalone pages`, 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\/workspace#\/library$/);
|
||||
await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library');
|
||||
await expect(popup).toHaveURL(/\/prompt_manager\/(web|admin)/);
|
||||
await expect(popup.locator('body')).toContainText('Prompt');
|
||||
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 the workspace; legacy pages still load`, async ({ page }) => {
|
||||
test(`${type} Web UI button opens actual standalone pages`, async ({ page }) => {
|
||||
await page.evaluate(async type => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
const node = LiteGraph.createNode(type);
|
||||
@@ -95,74 +95,14 @@ 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\/workspace#\/library$/);
|
||||
await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library');
|
||||
await expect(popup).toHaveURL(/\/prompt_manager\/(web|admin)/);
|
||||
await expect(popup.locator('body')).toContainText('Prompt');
|
||||
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');
|
||||
|
||||
@@ -8,7 +8,7 @@ test('workspace serves real library data beside either ComfyUI renderer', async
|
||||
text: `Workspace host smoke ${testInfo.project.name}`, tags: ['workspace-host'],
|
||||
} });
|
||||
expect(saved.ok()).toBe(true);
|
||||
const stats = (await (await request.get('/prompt_manager/stats')).json()).stats;
|
||||
const library = await (await request.get('/prompt_manager/library?limit=1')).json();
|
||||
const workspace = await context.newPage();
|
||||
const errors = [];
|
||||
workspace.on('pageerror', error => errors.push(error.message));
|
||||
@@ -16,7 +16,7 @@ test('workspace serves real library data beside either ComfyUI renderer', async
|
||||
await workspace.goto('/prompt_manager/workspace#/overview');
|
||||
await expect(workspace.locator('#workspace-main h1')).toHaveText('Overview');
|
||||
await expect(workspace.locator('dt').filter({ hasText: /^Prompts$/ }).locator('+ dd'))
|
||||
.toHaveText(stats.total_prompts.toLocaleString());
|
||||
.toHaveText(library.total.toLocaleString());
|
||||
await workspace.getByRole('combobox', { name: 'Theme' }).selectOption('light');
|
||||
await expect(workspace.locator('body')).toHaveAttribute('data-ca-theme', 'light');
|
||||
const imageResponse = workspace.waitForResponse(response => {
|
||||
@@ -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; #525: nor does Overview. The page stays at its URL.
|
||||
// D29: Images no longer links the legacy gallery; Overview's "Open existing image gallery" card does.
|
||||
await expect(workspace.locator('#workspace-main a[href="/prompt_manager/gallery"]')).toHaveCount(0);
|
||||
await workspace.evaluate(() => { location.hash = '/overview'; });
|
||||
const imagesCard = workspace.locator('#workspace-main').getByRole('link', { name: 'Open Images', exact: true });
|
||||
await expect(imagesCard).toHaveAttribute('href', '#/images');
|
||||
const gallery = workspace.locator('#workspace-main').getByRole('link', { name: 'Open existing image gallery', exact: true });
|
||||
await expect(gallery).toHaveAttribute('href', '/prompt_manager/gallery');
|
||||
expect((await workspace.request.get('/prompt_manager/gallery')).ok()).toBe(true);
|
||||
await workspace.evaluate(() => { location.hash = '/images'; });
|
||||
await workspace.reload();
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// #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, '#/tags', 'Overview opens the native page (#525)');
|
||||
assert.equal(tags.href, '/prompt_manager/admin#/tags', 'Overview keeps the existing tag manager card');
|
||||
for (const id of ['images', 'library', 'inspect', 'overview', 'system']) {
|
||||
const route = resolveRoute(`#/${id}`);
|
||||
assert.equal(routeTitle(route), route.label);
|
||||
@@ -48,6 +48,7 @@ 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');
|
||||
});
|
||||
|
||||
@@ -76,28 +77,9 @@ 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'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);
|
||||
// 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');
|
||||
});
|
||||
|
||||
test('a job of the Activity page is its own deep link', async () => {
|
||||
@@ -123,17 +105,29 @@ test('a job of the Activity page is its own deep link', async () => {
|
||||
dispose();
|
||||
});
|
||||
|
||||
test('statistics preserve zero and reject incomplete or invalid counts', async () => {
|
||||
const { summarizeStats } = await source;
|
||||
assert.deepEqual(summarizeStats({total_prompts:0, total_images:12, total_tags:3}), [
|
||||
['Prompts', 0], ['Images', 12], ['Tags', 3],
|
||||
]);
|
||||
for (const stats of [undefined, null, {}, {total_prompts:0, total_images:-1, total_tags:0},
|
||||
{total_prompts:'2', total_images:0, total_tags:0},
|
||||
{total_prompts:Infinity, total_images:0, total_tags:0},
|
||||
{total_prompts:0, total_images:1.5, total_tags:0}]) {
|
||||
assert.throws(() => summarizeStats(stats), /statistics/i);
|
||||
}
|
||||
test('Overview cards show the count of the page they name', async () => {
|
||||
const { summarizeOverviewCounts, OVERVIEW_COUNT_ROUTES } = await source;
|
||||
assert.deepEqual(OVERVIEW_COUNT_ROUTES, ['library', 'images', 'tags']);
|
||||
const counts = new Map([['library', 64], ['images', 122], ['tags', 306], ['favorites', 8]]);
|
||||
assert.deepEqual(summarizeOverviewCounts(counts), {
|
||||
cards: [['Prompts', 64], ['Images', 122], ['Tags', 306]], pending: false, failed: false, empty: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Overview tells loading, failed and really empty counts apart', async () => {
|
||||
const { summarizeOverviewCounts } = await source;
|
||||
const nothing = summarizeOverviewCounts(new Map());
|
||||
assert.deepEqual([nothing.pending, nothing.failed, nothing.empty], [true, false, false]);
|
||||
assert.deepEqual(nothing.cards, [['Prompts', null], ['Images', null], ['Tags', null]]);
|
||||
// A slow Images read does not hold back the cards already read.
|
||||
const partial = summarizeOverviewCounts(new Map([['library', 0], ['tags', 0]]));
|
||||
assert.deepEqual(partial.cards, [['Prompts', 0], ['Images', null], ['Tags', 0]]);
|
||||
assert.deepEqual([partial.pending, partial.failed, partial.empty], [true, false, false]);
|
||||
const failed = summarizeOverviewCounts(new Map([['library', 2], ['images', null], ['tags', 3]]));
|
||||
assert.deepEqual([failed.pending, failed.failed, failed.empty], [false, true, false]);
|
||||
assert.deepEqual(failed.cards[1], ['Images', null]);
|
||||
const empty = summarizeOverviewCounts(new Map([['library', 0], ['images', 0], ['tags', 0]]));
|
||||
assert.deepEqual([empty.pending, empty.failed, empty.empty], [false, false, true]);
|
||||
});
|
||||
|
||||
test('router normalizes unknown routes, avoids duplicate renders, and disposes its listener', async () => {
|
||||
|
||||
Vendored
+1
-2
@@ -25,8 +25,7 @@ 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)
|
||||
// + #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.match(stock, /Total: 110 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,12 +107,6 @@
|
||||
"functions": 98,
|
||||
"statements": 98
|
||||
},
|
||||
"workspace-entry.js": {
|
||||
"lines": 80,
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"statements": 80
|
||||
},
|
||||
"save-to-library.js": {
|
||||
"lines": 98,
|
||||
"branches": 98,
|
||||
|
||||
@@ -7,7 +7,6 @@ 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']);
|
||||
|
||||
// ============================================================================
|
||||
@@ -110,7 +109,7 @@ function addPMTopBarButton() {
|
||||
// Create PM button
|
||||
const pmButton = $el("button", {
|
||||
classList: "comfyui-button comfyui-menu-mobile-collapse primary",
|
||||
title: "PromptManager - Open workspace",
|
||||
title: "PromptManager - Open Admin Dashboard",
|
||||
style: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -126,7 +125,7 @@ function addPMTopBarButton() {
|
||||
transition: "background 0.2s",
|
||||
},
|
||||
onclick: () => {
|
||||
window.open(api.fileURL(WORKSPACE_ENTRY_ROUTE), "_blank");
|
||||
window.open(api.fileURL("/prompt_manager/admin"), "_blank");
|
||||
},
|
||||
onmouseenter: (e) => { e.target.style.background = "#0d9488"; },
|
||||
onmouseleave: (e) => { e.target.style.background = "#14b8a6"; },
|
||||
@@ -162,7 +161,7 @@ function addPMTopBarButtonLegacy() {
|
||||
const btn = document.createElement("button");
|
||||
btn.id = "pm-toolbar-btn";
|
||||
btn.innerText = "PM";
|
||||
btn.title = "Open PromptManager workspace";
|
||||
btn.title = "Open PromptManager Admin Dashboard";
|
||||
|
||||
// Teal button styling
|
||||
btn.style.cssText = `
|
||||
@@ -182,7 +181,7 @@ function addPMTopBarButtonLegacy() {
|
||||
btn.onmouseleave = () => btn.style.background = "#14b8a6";
|
||||
|
||||
btn.onclick = () => {
|
||||
window.open(api.fileURL(WORKSPACE_ENTRY_ROUTE), "_blank");
|
||||
window.open(api.fileURL("/prompt_manager/admin"), "_blank");
|
||||
};
|
||||
|
||||
topBar.appendChild(btn);
|
||||
@@ -1185,7 +1184,9 @@ app.registerExtension({
|
||||
// Method to open web interface
|
||||
nodeType.prototype.openWebInterface = function () {
|
||||
try {
|
||||
const webUrl = api.fileURL(WORKSPACE_ENTRY_ROUTE);
|
||||
// Open the web interface served via our API endpoint
|
||||
const currentOrigin = window.location.origin;
|
||||
const webUrl = `${currentOrigin}/prompt_manager/web`;
|
||||
|
||||
if (this.properties.webuiDisplayMode === "newtab") {
|
||||
// Open in new tab without window options
|
||||
@@ -1197,8 +1198,7 @@ app.registerExtension({
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
// Never navigate the canvas away: it may hold unsaved work.
|
||||
this.showNotification("Allow popups to open Web UI, then try again.", "error");
|
||||
window.location.href = webUrl;
|
||||
}
|
||||
} else {
|
||||
// Default popup mode with specific dimensions
|
||||
@@ -1211,8 +1211,7 @@ app.registerExtension({
|
||||
if (newWindow) {
|
||||
this.showNotification("Web interface opened in popup", "success");
|
||||
} else {
|
||||
// Never navigate the canvas away: it may hold unsaved work.
|
||||
this.showNotification("Allow popups to open Web UI, then try again.", "error");
|
||||
window.location.href = webUrl;
|
||||
}
|
||||
}
|
||||
} 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 - Opening the workspace</title>
|
||||
<title>PromptManager - Redirecting to Admin</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>Opening the workspace...</p>
|
||||
<noscript><p><a href="workspace#/library">Open the workspace</a></p></noscript>
|
||||
<p>Redirecting to Admin Dashboard...</p>
|
||||
<p><small>Loading the full-featured prompt management interface</small></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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'));
|
||||
// Redirect to admin interface immediately
|
||||
window.location.href = window.location.pathname.replace(/\/(?:web|index\.html)\/?$/, '/admin');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -254,7 +253,7 @@ export function installFocusedNode(nodeType, app, api, parseQuery, {layout = 'fo
|
||||
}
|
||||
});
|
||||
scope.listen(web,'click',() => {
|
||||
const url = api.fileURL(WORKSPACE_ENTRY_ROUTE);
|
||||
const url = api.fileURL('/prompt_manager/web');
|
||||
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.';
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// 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';
|
||||
+42
-27
@@ -1,6 +1,6 @@
|
||||
import '../api-client.js';
|
||||
import { createButton, createScope } from '../shared/toolkit/core.js';
|
||||
import { routes, mountRouter, summarizeStats, routeTitle, loadNavCounts, navigateTo, COLLECTION_ROUTE,
|
||||
import { routes, mountRouter, summarizeOverviewCounts, OVERVIEW_COUNT_ROUTES, routeTitle, loadNavCounts, navigateTo, COLLECTION_ROUTE,
|
||||
LIBRARY_WRITE_COUNTS, NAV_COUNT_ROUTES, FAVORITE_WRITE_COUNTS, FAVORITE_COUNT_DELAY_MS, REVIEW_WRITE_COUNTS,
|
||||
JOB_END_COUNTS } from './state.mjs';
|
||||
import { mountWorkspaceTheme } from './theme.mjs';
|
||||
@@ -34,9 +34,10 @@ function element(tag, text, className) {
|
||||
return node;
|
||||
}
|
||||
|
||||
function cardLink(route) {
|
||||
function legacyLink(route) {
|
||||
const link = element('a', route.action, 'workspace-link');
|
||||
link.href = route.href;
|
||||
// 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);
|
||||
return link;
|
||||
}
|
||||
|
||||
@@ -148,9 +149,13 @@ function mountWorkspace() {
|
||||
countRequests.get(id)?.abort();
|
||||
const request = new AbortController();
|
||||
countRequests.set(id, request);
|
||||
// A failed count reads as loading again while it is retried; a known one stays until replaced.
|
||||
if (knownCounts.get(id) === null) { knownCounts.delete(id); notifyCounts(); }
|
||||
// Each badge lands as its own read settles; a count that failed to refresh is removed, never left stale.
|
||||
loadNavCounts(api, request.signal, (_, count) => {
|
||||
if (!request.signal.aborted) showCount(id, count);
|
||||
if (request.signal.aborted) return;
|
||||
countRequests.delete(id);
|
||||
showCount(id, count);
|
||||
}, [id]);
|
||||
}
|
||||
}
|
||||
@@ -158,7 +163,15 @@ function mountWorkspace() {
|
||||
const link = sidebar.querySelector(`nav [data-route="${id}"]`);
|
||||
link?.querySelector('.nav-count')?.remove();
|
||||
if (link && Number.isSafeInteger(count)) link.append(element('span', count.toLocaleString(), 'nav-count'));
|
||||
knownCounts.set(id, count);
|
||||
notifyCounts();
|
||||
}
|
||||
/** #547: Overview shows these same counts, so a card and its badge cannot differ, and no read runs twice. */
|
||||
const knownCounts = new Map();
|
||||
const countListeners = new Set();
|
||||
function notifyCounts() { for (const listener of countListeners) listener(); }
|
||||
/** Re-read the given counts unless a read is already in flight. */
|
||||
function freshenCounts(ids) { refreshCounts(ids.filter(id => !countRequests.has(id))); }
|
||||
/** A count a page has just read itself: it replaces the badge, and an older read in flight no longer lands. */
|
||||
function setCount(id, count) {
|
||||
countRequests.get(id)?.abort();
|
||||
@@ -484,43 +497,47 @@ 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', 'Pick up where you left off in your images, prompts and tools.'));
|
||||
tools.append(element('h2', 'Continue creating'), element('p', 'Your existing tools remain available while the new workspaces take shape.'));
|
||||
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'), cardLink(target));
|
||||
card.append(element('h3', target.label), element('p', target.description, 'small'), legacyLink(target));
|
||||
cards.append(card);
|
||||
}
|
||||
tools.append(cards);
|
||||
main.append(tools);
|
||||
async function loadStats() {
|
||||
const status = element('p', 'Loading library statistics…');
|
||||
status.setAttribute('role', 'status');
|
||||
stats.replaceChildren(status);
|
||||
try {
|
||||
const result = await api.get('/prompt_manager/stats', { signal: view.signal });
|
||||
if (view.signal.aborted) return;
|
||||
const values = summarizeStats(result.stats);
|
||||
function showStats() {
|
||||
const { cards, pending, failed, empty } = summarizeOverviewCounts(knownCounts);
|
||||
const parts = [];
|
||||
if (cards.some(([, count]) => count !== null)) {
|
||||
const list = element('dl', undefined, 'stat-grid');
|
||||
for (const [label, count] of values) {
|
||||
for (const [label, count] of cards) {
|
||||
const stat = element('div', undefined, 'stat');
|
||||
stat.append(element('dt', label), element('dd', count.toLocaleString()));
|
||||
stat.append(element('dt', label), element('dd', count === null ? '–' : count.toLocaleString()));
|
||||
list.append(stat);
|
||||
}
|
||||
stats.replaceChildren(list);
|
||||
if (values.every(([, count]) => count === 0)) {
|
||||
stats.append(element('p', 'Your library is empty. Add a prompt in the prompt library or configure sources in System.', 'empty-state'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (view.signal.aborted) return;
|
||||
parts.push(list);
|
||||
}
|
||||
if (pending) {
|
||||
const status = element('p', 'Loading library statistics…');
|
||||
status.setAttribute('role', 'status');
|
||||
parts.push(status);
|
||||
} else if (failed) {
|
||||
const alert = element('p', 'Library statistics could not be loaded. Your saved data has not changed.');
|
||||
alert.setAttribute('role', 'alert');
|
||||
const retry = createButton('Retry');
|
||||
view.listen(retry, 'click', loadStats);
|
||||
stats.replaceChildren(alert, retry);
|
||||
view.listen(retry, 'click', () => refreshCounts(OVERVIEW_COUNT_ROUTES.filter(id => knownCounts.get(id) === null)));
|
||||
parts.push(alert, retry);
|
||||
} else if (empty) {
|
||||
parts.push(element('p', 'Your library is empty. Add a prompt in the prompt library or configure sources in System.', 'empty-state'));
|
||||
}
|
||||
stats.replaceChildren(...parts);
|
||||
}
|
||||
loadStats();
|
||||
countListeners.add(showStats);
|
||||
view.cleanup(() => countListeners.delete(showStats));
|
||||
showStats();
|
||||
// A visit reads them again, as a generation since the last read changes the sidebar's badges too.
|
||||
freshenCounts(OVERVIEW_COUNT_ROUTES);
|
||||
}
|
||||
scope.cleanup(mountRouter(window, render));
|
||||
scope.listen(window, 'pagehide', () => scope.dispose());
|
||||
@@ -528,8 +545,6 @@ 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();
|
||||
});
|
||||
|
||||
+26
-31
@@ -1,15 +1,14 @@
|
||||
export const routes = [
|
||||
// `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: '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' },
|
||||
{ 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' },
|
||||
@@ -19,20 +18,13 @@ 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; 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 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,
|
||||
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']);
|
||||
|
||||
@@ -95,15 +87,6 @@ export function resolveRoute(hash) {
|
||||
return routes.find(route => hash === `#/${route.id}`) || routes[0];
|
||||
}
|
||||
|
||||
export function summarizeStats(stats) {
|
||||
return [['Prompts', 'total_prompts'], ['Images', 'total_images'], ['Tags', 'total_tags']]
|
||||
.map(([label, key]) => {
|
||||
const count = stats?.[key];
|
||||
if (!Number.isSafeInteger(count) || count < 0) throw new Error('Invalid library statistics');
|
||||
return [label, count];
|
||||
});
|
||||
}
|
||||
|
||||
// The sidebar's real counts (D27): one bounded `limit=1` read each, whose `total` is the count. Favorites (D51) is the
|
||||
// Favorites page's own listing, so its count is what that page lists.
|
||||
const NAV_COUNT_SOURCES = Object.freeze([
|
||||
@@ -158,6 +141,18 @@ export async function loadNavCounts(api, signal, onCount = () => {}, ids = NAV_C
|
||||
return new Map([...summarizeNavCounts(pages)].filter(([id]) => ids.includes(id)));
|
||||
}
|
||||
|
||||
// #547: each Overview card is the count of the page it names, the same number as that page's sidebar badge.
|
||||
const OVERVIEW_CARDS = Object.freeze([['Prompts', 'library'], ['Images', 'images'], ['Tags', 'tags']]);
|
||||
export const OVERVIEW_COUNT_ROUTES = Object.freeze(OVERVIEW_CARDS.map(([, id]) => id));
|
||||
|
||||
/** Overview's state from the shell's counts: no entry is a read not settled yet, null a read that failed. */
|
||||
export function summarizeOverviewCounts(counts) {
|
||||
const cards = OVERVIEW_CARDS.map(([label, id]) => [label, counts.get(id) ?? null]);
|
||||
const pending = OVERVIEW_COUNT_ROUTES.some(id => !counts.has(id));
|
||||
const failed = OVERVIEW_COUNT_ROUTES.some(id => counts.get(id) === null);
|
||||
return { cards, pending, failed, empty: !pending && cards.every(([, count]) => count === 0) };
|
||||
}
|
||||
|
||||
/** Fired on the window to render the current route again, where setting the same hash fires no `hashchange`. */
|
||||
const ROUTE_REFRESH_EVENT = 'prompt-manager:route-refresh';
|
||||
|
||||
|
||||
@@ -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 { LEGACY_PAGES, SYSTEM_SECTIONS, navigateTo } from './state.mjs';
|
||||
import { 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,22 +143,6 @@ 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;
|
||||
@@ -184,7 +168,6 @@ 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();
|
||||
@@ -210,8 +193,6 @@ 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,7 +493,6 @@ 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,13 +13,6 @@
|
||||
<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">
|
||||
@@ -57,13 +50,6 @@
|
||||
</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