diff --git a/README.md b/README.md index 946967d1..66d96f8b 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/TESTING.md b/TESTING.md index df3ca4fe..4339df66 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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`. @@ -3140,8 +3140,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 @@ -4363,6 +4363,15 @@ the per-section hand-off panel, was retired by S12 (#329): every section is nati `system shell and overview match the concept` (the mapping inserts the row) and `library notice matches the concept notice` with its mutation check (system-concept.spec), and the D129 tests of workspace-system.spec. +- **D166** (one-time upgrade notice, #527): the concept's README puts the notice in the + new shell, while its `app.js` draws it in System β€Ί Sources. Production does both: the + Sources `.notice` is kept and compared as drawn, and `#older-images-notice`, styled as + `#library-banner`, shows the same notice above every other page until dismissed. The + shell hides on Sources, so they never show together. Dismissal is the legacy key + `pm_update_notice_dismissed`, shared with the admin modal and across tabs. Browser + contexts start dismissed (`tests/browser/fixtures.mjs`, `storageState`); a spec about + the notice sets `test.use({ olderImagesNotice: true })`. Pinned by + workspace-older-images-notice.spec. - **D130** (Appearance, #317): the concept keeps "Thumbnail quality, size limit" in Appearance (README) but draws no rows for them. Appearance gains a second `section.surface`, "Thumbnails", after "Browsing & display", built from the concept's @@ -7052,8 +7061,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 @@ -7175,3 +7184,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`). diff --git a/tests/browser/classic-node.spec.mjs b/tests/browser/classic-node.spec.mjs index 0f6cdead..bd8d0b57 100644 --- a/tests/browser/classic-node.spec.mjs +++ b/tests/browser/classic-node.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'); diff --git a/tests/browser/fixtures.mjs b/tests/browser/fixtures.mjs index 357f8c19..c89996e8 100644 --- a/tests/browser/fixtures.mjs +++ b/tests/browser/fixtures.mjs @@ -8,6 +8,9 @@ // browser that exits abnormally prints one `pm-browser-exit|` line, and a test // that fails on a closed or crashed target gets that log attached as // `browser-stderr`. +// The one-time older-images notice (#527) shows above every workspace page and as +// a modal on the admin page until dismissed, so every context starts with it +// dismissed. A spec about the notice sets `test.use({ olderImagesNotice: true })`. import { test as base, expect } from '../../tools/vendor/node_modules/@playwright/test/index.mjs'; import { attachServerStderr, createServerSupervisor, startBrowserServer } from '../../tools/vendor/start-browser-server.mjs'; import { attachBrowserLogs, createBrowserLogRecorder } from '../../tools/vendor/browser-logs.mjs'; @@ -44,6 +47,11 @@ export const test = base.extend({ if (crash) throw crash; }, { auto: true, timeout: SERVER_FIXTURE_TIMEOUT_MS }], baseURL: async ({ workerServer }, use) => use(workerServer.url), + olderImagesNotice: [false, { option: true }], + storageState: async ({ baseURL, olderImagesNotice }, use) => use(olderImagesNotice ? undefined : { + cookies: [], + origins: [{ origin: new URL(baseURL).origin, localStorage: [{ name: 'pm_update_notice_dismissed', value: '3.2.0' }] }], + }), }); export { expect }; diff --git a/tests/browser/focused-node.spec.mjs b/tests/browser/focused-node.spec.mjs index 6ab2818c..00c4667c 100644 --- a/tests/browser/focused-node.spec.mjs +++ b/tests/browser/focused-node.spec.mjs @@ -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})=>{ diff --git a/tests/browser/system-concept.spec.mjs b/tests/browser/system-concept.spec.mjs index 3e37eaad..37b66f78 100644 --- a/tests/browser/system-concept.spec.mjs +++ b/tests/browser/system-concept.spec.mjs @@ -165,6 +165,8 @@ const number = value => value.toLocaleString('en-US'); /** The concept's shell and Overview with the recorded deviations applied, given the server's real overview. */ function mapOverviewConcept(reference, overview) { + // D165: the discreet legacy-pages line follows the settings layout. + reference = { ...reference, tree: [...reference.tree, 'p.legacy-pages.small'] }; const relabel = { 'Database integrity': 'Not checked', 'Positive prompts': number(overview.library.positive_prompts), 'Negative variants': number(overview.library.negative_variants), Images: number(overview.images.indexed) }; const rows = { @@ -257,7 +259,7 @@ const UNMAPPED_PATHS = [ 'columns.surfaces.0.rows.length', 'columns.surfaces.0.tree.4', 'columns.surfaces.0.tree.length', 'columns.surfaces.1.rows.0.description', 'columns.surfaces.1.rows.1.title', 'columns.surfaces.1.rows.2.description', 'heading.actions.0', 'heading.actions.length', 'metrics.items.3.value', 'nav.label', - 'save.button.cursor', 'save.button.opacity', 'save.status.text', + 'save.button.cursor', 'save.button.opacity', 'save.status.text', 'tree.3', 'tree.length', ].sort(); test('system comparison detects mutations', async ({ page, request }) => { @@ -683,6 +685,8 @@ async function watchedSources(request) { for (const theme of THEMES) for (const [width, height] of SIZES) for (const notice of [true, false]) { const variant = notice ? 'with' : 'without'; + test.describe(() => { + test.use({ olderImagesNotice: notice }); test(`sources matches the concept ${variant} the notice ${theme} ${width}x${height}`, async ({ page, request }, testInfo) => { await page.setViewportSize({ width, height }); const server = await watchedSources(request); @@ -693,8 +697,11 @@ for (const theme of THEMES) for (const [width, height] of SIZES) for (const noti expect(diffInventory(mapSourcesConcept(concept, server), production)).toEqual([]); expect(production.pageScroll).toBe(false); }); + }); } +test.describe(() => { +test.use({ olderImagesNotice: true }); test('sources comparison detects mutations', async ({ page, request }) => { await page.setViewportSize({ width: 1600, height: 1100 }); const server = await watchedSources(request); @@ -737,6 +744,7 @@ test('sources comparison detects mutations', async ({ page, request }) => { await undoToken(); expect(diffInventory(expected, await measure())).toEqual([]); }); +}); /** * Runs in the page: the open "Add image source" dialog (the concept's `simpleInput`, app.js 1138 and 1497): its title, diff --git a/tests/browser/workspace-older-images-notice.spec.mjs b/tests/browser/workspace-older-images-notice.spec.mjs new file mode 100644 index 00000000..14568230 --- /dev/null +++ b/tests/browser/workspace-older-images-notice.spec.mjs @@ -0,0 +1,153 @@ +// The one-time older-images notice in the workspace shell (#527, D166). Its dismissal is the legacy admin page's key, +// so dismissing it in any place dismisses it everywhere. +import { test, expect } from './fixtures.mjs'; + +const WORKSPACE = '/prompt_manager/workspace'; +const ADMIN = '/prompt_manager/admin'; +const NOTICE_KEY = 'pm_update_notice_dismissed'; + +const shellNotice = page => page.locator('#older-images-notice > .notice'); +const sourcesNotice = page => page.locator('#workspace-main .settings-layout > .notice'); +const dialog = page => page.locator('dialog#modal[open]'); +const adminModal = page => page.locator('#updateNoticeModal'); +const stored = page => page.evaluate(key => localStorage.getItem(key), NOTICE_KEY); + +async function openRoute(page, route) { + await page.goto(`${WORKSPACE}#/${route}`); + await expect(page.locator('#workspace-main h1').first()).toBeVisible(); +} + +// Every spec on this worker's server shares its job list. +test.beforeEach(async ({ request }) => { + await expect(await request.post('/__test__/jobs/restart')).toBeOK(); +}); + +test.describe('with the notice not dismissed', () => { + test.use({ olderImagesNotice: true }); + + for (const route of ['library', 'images', 'tags', 'activity', 'system/overview']) { + test(`shows above the ${route} page with Later and Review scan`, async ({ page }) => { + await openRoute(page, route); + await expect(shellNotice(page).locator('h2')).toHaveText('Bring older images into your prompt history'); + await expect(shellNotice(page).locator('.row-actions > button')).toHaveText(['Later', 'Review scan']); + await expect(page.locator('#older-images-notice')).toBeVisible(); + }); + } + + test('Later dismisses it for good without a write or a scan', async ({ page, request }) => { + const writes = []; + page.on('request', item => { if (!['GET', 'HEAD', 'OPTIONS'].includes(item.method())) writes.push(item.url()); }); + await openRoute(page, 'library'); + await shellNotice(page).getByRole('button', { name: 'Later', exact: true }).click(); + await expect(shellNotice(page)).toHaveCount(0); + await expect(page.locator('#older-images-notice')).toBeHidden(); + await expect(page.locator('#workspace-main')).toBeFocused(); + expect(await stored(page)).toBe('3.2.0'); + await expect(dialog(page)).toHaveCount(0); + await page.reload(); + await expect(page.locator('#workspace-main h1').first()).toBeVisible(); + await expect(shellNotice(page)).toHaveCount(0); + expect(writes).toEqual([]); + expect((await (await request.get('/prompt_manager/jobs')).json()).jobs).toEqual([]); + }); + + test('Review scan dismisses it and opens the review of every source', async ({ page }) => { + await openRoute(page, 'library'); + await shellNotice(page).getByRole('button', { name: 'Review scan', exact: true }).click(); + await expect(dialog(page).locator('#modal-title')).toHaveText('Review metadata scan'); + await expect(dialog(page).locator('dl.meta-grid > div > dd')).toHaveText(['2 output folders', 'New and unresolved files']); + await expect(shellNotice(page)).toHaveCount(0); + expect(await stored(page)).toBe('3.2.0'); + await dialog(page).getByRole('button', { name: 'Cancel', exact: true }).click(); + await expect(page.locator('#workspace-main')).toBeFocused(); + }); + + test('Review scan keeps the notice and says so when the sources cannot be read', async ({ page }) => { + await openRoute(page, 'library'); + await page.route('**/prompt_manager/settings', route => route.fulfill({ status: 500, json: { success: false } })); + await shellNotice(page).getByRole('button', { name: 'Review scan', exact: true }).click(); + await expect(page.locator('#workspace-toast')).toHaveText('Could not read the image sources. Try again.'); + await expect(shellNotice(page)).toHaveCount(1); + await expect(shellNotice(page).getByRole('button', { name: 'Review scan', exact: true })).not.toHaveAttribute('aria-disabled', 'true'); + await expect(dialog(page)).toHaveCount(0); + expect(await stored(page)).toBeNull(); + }); + + test('System > Sources shows one notice, its own, and dismissing it there clears the shell', async ({ page }) => { + await openRoute(page, 'system/overview'); + await expect(shellNotice(page)).toHaveCount(1); + await page.goto(`${WORKSPACE}#/system/sources`); + await expect(page.locator('#workspace-main .settings-layout > .notice:visible')).toHaveCount(1); + await expect(shellNotice(page)).toHaveCount(0); + await sourcesNotice(page).getByRole('button', { name: 'Later', exact: true }).click(); + await expect(sourcesNotice(page)).toHaveCount(0); + await page.goto(`${WORKSPACE}#/library`); + await expect(page.locator('#workspace-main h1').first()).toBeVisible(); + await expect(shellNotice(page)).toHaveCount(0); + }); + + test('dismissing in the shell clears the copy System has already mounted', async ({ page }) => { + await openRoute(page, 'system/sources'); + await expect(sourcesNotice(page)).toHaveCount(1); + await page.goto(`${WORKSPACE}#/system/overview`); + await shellNotice(page).getByRole('button', { name: 'Later', exact: true }).click(); + await page.goto(`${WORKSPACE}#/system/sources`); + await expect(page.locator('#workspace-main #source-list')).toBeVisible(); + await expect(sourcesNotice(page)).toHaveCount(0); + }); + + test('dismissing in another tab clears it here', async ({ page, context }) => { + await openRoute(page, 'library'); + await expect(shellNotice(page)).toHaveCount(1); + const other = await context.newPage(); + await openRoute(other, 'images'); + await shellNotice(other).getByRole('button', { name: 'Later', exact: true }).click(); + await expect(shellNotice(page)).toHaveCount(0); + await other.close(); + }); + + test('the admin modal still works: Later without the checkbox keeps the notice everywhere', async ({ page }) => { + await page.goto(ADMIN); + await expect(adminModal(page)).toBeVisible(); + await page.locator('#updateNoticeDismiss').click(); + await expect(adminModal(page)).toBeHidden(); + expect(await stored(page)).toBeNull(); + await openRoute(page, 'library'); + await expect(shellNotice(page)).toHaveCount(1); + }); + + test('dismissing on the admin page dismisses it in the workspace', async ({ page }) => { + await page.goto(ADMIN); + await page.locator('#dismissUpdateNotice').check(); + await page.locator('#updateNoticeDismiss').click(); + await expect(adminModal(page)).toBeHidden(); + await openRoute(page, 'library'); + await expect(shellNotice(page)).toHaveCount(0); + }); + + test('dismissing in the workspace dismisses it on the admin page', async ({ page }) => { + await openRoute(page, 'library'); + await shellNotice(page).getByRole('button', { name: 'Later', exact: true }).click(); + await page.goto(ADMIN); + await page.waitForLoadState('load'); + await expect(adminModal(page)).toBeHidden(); + }); + + test('the admin Scan still dismisses it and opens the scan modal', async ({ page }) => { + await page.goto(ADMIN); + await page.locator('#updateNoticeScan').click(); + await expect(adminModal(page)).toBeHidden(); + expect(await stored(page)).toBe('3.2.0'); + }); +}); + +test('specs start with the notice dismissed, on every page', async ({ page, browser, baseURL }) => { + await openRoute(page, 'library'); + await expect(shellNotice(page)).toHaveCount(0); + const context = await browser.newContext({ baseURL }); + const other = await context.newPage(); + await other.goto(ADMIN); + await other.waitForLoadState('load'); + await expect(adminModal(other)).toBeHidden(); + await context.close(); +}); diff --git a/tests/browser/workspace-system-sources.spec.mjs b/tests/browser/workspace-system-sources.spec.mjs index 74097aa3..cd1ac570 100644 --- a/tests/browser/workspace-system-sources.spec.mjs +++ b/tests/browser/workspace-system-sources.spec.mjs @@ -41,6 +41,9 @@ async function serverJson(request, url) { return response.json(); } +// `openSources` dismisses the notice itself unless a test asks for it. +test.use({ olderImagesNotice: true }); + /** Open Sources with the update notice dismissed (or shown, `notice: true`) once the source list has loaded. */ async function openSources(page, { notice = false } = {}) { if (!notice) await page.addInitScript(key => { if (location.protocol.startsWith('http')) localStorage.setItem(key, '3.2.0'); }, NOTICE_KEY); diff --git a/tests/browser/workspace-system.spec.mjs b/tests/browser/workspace-system.spec.mjs index 6581fe85..98ca5e6a 100644 --- a/tests/browser/workspace-system.spec.mjs +++ b/tests/browser/workspace-system.spec.mjs @@ -897,6 +897,8 @@ test('reset restores defaults without touching other keys', async ({ page }) => // System section, found by role and accessible name (a `term` by its text, a disclosure by its summary). Collapsed // `details` are opened first, so a control inside one counts once it is shown. tests/vendor/legacy-coverage.test.mjs // checks the rest of each row. +test.describe(() => { +test.use({ olderImagesNotice: true }); test('every covered legacy System capability has its native control', async ({ page, request }) => { // Download is offered for a model that is not on disk ("Download again" otherwise): the selected default's file is // missing here. @@ -907,6 +909,7 @@ test('every covered legacy System capability has its native control', async ({ p await request.post('/__test__/tagging/reset', { data: {} }); } }); +}); async function findControls(page) { const covered = readCoverageTable().filter(row => row.Status === 'covered') diff --git a/tests/browser/workspace.spec.mjs b/tests/browser/workspace.spec.mjs index dff63d85..9fbab6d4 100644 --- a/tests/browser/workspace.spec.mjs +++ b/tests/browser/workspace.spec.mjs @@ -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 }) => { @@ -428,6 +514,8 @@ const SYSTEM_SECTION_LABELS = ['Overview', 'Appearance', 'Sources & scans', 'Int 'Backup & export', 'Diagnostics & logs']; const SYSTEM_HASHES = ['overview', 'appearance', 'sources', 'integrations', 'tagging', 'maintenance', 'backups', 'diagnostics']; +test.describe(() => { +test.use({ olderImagesNotice: true }); test('System is native: the concept shell, section navigation, sections and the Save row', async ({ page }) => { const mutations = []; page.on('request', item => { if (!['GET', 'HEAD', 'OPTIONS'].includes(item.method())) mutations.push(item.url()); }); @@ -438,7 +526,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'); @@ -506,6 +594,7 @@ test('System is native: the concept shell, section navigation, sections and the await expect(main.locator('.settings-layout')).toHaveCount(1); expect(mutations).toEqual([]); }); +}); test('Overview opens native System from its System card (Q12)', async ({ page }) => { await page.goto(`${workspace}#/overview`); diff --git a/tests/comfyui/focused-node.spec.mjs b/tests/comfyui/focused-node.spec.mjs index 1f261934..ff7bafdd 100644 --- a/tests/comfyui/focused-node.spec.mjs +++ b/tests/comfyui/focused-node.spec.mjs @@ -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(); diff --git a/tests/comfyui/prompt-manager.spec.mjs b/tests/comfyui/prompt-manager.spec.mjs index 07ae1ce6..29e4d005 100644 --- a/tests/comfyui/prompt-manager.spec.mjs +++ b/tests/comfyui/prompt-manager.spec.mjs @@ -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'); diff --git a/tests/comfyui/workspace.spec.mjs b/tests/comfyui/workspace.spec.mjs index c4c1b458..00d2c8d4 100644 --- a/tests/comfyui/workspace.spec.mjs +++ b/tests/comfyui/workspace.spec.mjs @@ -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(); diff --git a/tests/js/workspace-entry.test.js b/tests/js/workspace-entry.test.js new file mode 100644 index 00000000..aec1b674 --- /dev/null +++ b/tests/js/workspace-entry.test.js @@ -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); + } +}); diff --git a/tests/js/workspace-state.test.js b/tests/js/workspace-state.test.js index 526cbe70..6b818430 100644 --- a/tests/js/workspace-state.test.js +++ b/tests/js/workspace-state.test.js @@ -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 () => { diff --git a/tests/js/workspace-system-sources.test.js b/tests/js/workspace-system-sources.test.js index 81719d44..31f4ce83 100644 --- a/tests/js/workspace-system-sources.test.js +++ b/tests/js/workspace-system-sources.test.js @@ -174,3 +174,19 @@ test('D158: "Save prompts from other workflows" drafts save_foreign_runs, disabl assert.deepEqual(foreignRunsState({ value: true, monitoring: false, stopping: true }), { disabled: false, checked: true, note: FOREIGN_RUNS.off }); }); + +test('a failed notice write still dismisses for this page and tells watchers (#527)', async () => { + const { noticeDismissed, dismissNotice } = await sourcesModule; + const denied = () => { throw new Error('SecurityError'); }; + assert.equal(noticeDismissed(denied), false); + assert.equal(dismissNotice(denied), false); + assert.equal(noticeDismissed(denied), true); +}); + +test('sourceCount reads the effective sources, null when the answer does not say (#527)', async () => { + const { sourceCount } = await import('../../web/js/workspace/older-images-notice.mjs'); + assert.equal(sourceCount({ sources: { effective: [{}, {}] } }), 2); + assert.equal(sourceCount({ sources: { effective: [] } }), 0); + assert.equal(sourceCount({ sources: {} }), null); + assert.equal(sourceCount(null), null); +}); diff --git a/tests/vendor/comfyui-config.test.mjs b/tests/vendor/comfyui-config.test.mjs index b77e7d8b..8b81a4ce 100644 --- a/tests/vendor/comfyui-config.test.mjs +++ b/tests/vendor/comfyui-config.test.mjs @@ -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) diff --git a/tools/vendor/frontend-coverage-baseline.json b/tools/vendor/frontend-coverage-baseline.json index 33a431e5..07788e50 100644 --- a/tools/vendor/frontend-coverage-baseline.json +++ b/tools/vendor/frontend-coverage-baseline.json @@ -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, @@ -401,6 +407,12 @@ "functions": 98, "statements": 98 }, + "workspace/older-images-notice.mjs": { + "lines": 80, + "branches": 70, + "functions": 80, + "statements": 80 + }, "workspace/library-location.mjs": { "lines": 94, "branches": 91, diff --git a/web/comfy/prompt_manager.js b/web/comfy/prompt_manager.js index 706c9d78..ddd36e5e 100644 --- a/web/comfy/prompt_manager.js +++ b/web/comfy/prompt_manager.js @@ -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) { diff --git a/web/index.html b/web/index.html index 57806727..4e7f7f88 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - PromptManager - Redirecting to Admin + PromptManager - Opening the workspace