feat(workspace): show the one-time older-images notice in the shell (#529)
* feat(workspace): show the one-time older-images notice in the shell After the switch everyone lands on Library, so someone who has not dismissed the notice might never be prompted. It now shows above every workspace page until dismissed, sharing pm_update_notice_dismissed with System > Sources and the admin modal. Browser specs start with it dismissed. Closes #527 * test(workspace): turn the older-images notice on where specs expect it Two System specs read the Sources notice from empty storage, and the new spec read a job list earlier specs had filled.
This commit is contained in:
@@ -4340,6 +4340,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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -685,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);
|
||||
@@ -695,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);
|
||||
@@ -739,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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -514,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()); });
|
||||
@@ -592,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`);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -407,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,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { createPromptSender } from './prompt-sender.mjs';
|
||||
import { mountSystem } from './system.mjs';
|
||||
import { createRestoreReload, onRestoreFinished, onRestoreHandedBack, showRestoreReview } from './system-backups.mjs';
|
||||
import { mountLibraryBanner } from './library-location.mjs';
|
||||
import { mountOlderImagesNotice } from './older-images-notice.mjs';
|
||||
import { detectJxlSupport } from './thumbnail-support.mjs';
|
||||
|
||||
const { createApiClient, resolveUrl } = window.ApiClient;
|
||||
@@ -85,6 +86,8 @@ function mountWorkspace() {
|
||||
activity.load();
|
||||
// D129: a node-folder library is offered a move (or "Keep it here") above every page.
|
||||
mountLibraryBanner(document.getElementById('library-banner'), { api, activity, scope });
|
||||
// D166: the one-time upgrade notice, above every page until dismissed; System > Sources shows its own.
|
||||
const olderImages = mountOlderImagesNotice(document.getElementById('older-images-notice'), { api, activity, scope, main });
|
||||
const sidebar = document.querySelector('aside.sidebar');
|
||||
// The route key a nav link carries in `data-route`: a saved view's is `saved/<id>`, a collection's `collection/<id>`.
|
||||
const routeKey = route => route.id === 'saved' ? `saved/${route.savedId}`
|
||||
@@ -259,6 +262,7 @@ function mountWorkspace() {
|
||||
|
||||
function renderNow(route) {
|
||||
leaveGuard = null;
|
||||
olderImages.showFor(route);
|
||||
// A shared page dialog never stays open over the next route.
|
||||
closeWorkspaceDialog();
|
||||
if (route.id === 'system' && system && currentKey === 'system') {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* The one-time upgrade notice "Bring older images into your prompt history" (#527, D166).
|
||||
*
|
||||
* Its dismissal is the legacy admin page's own `pm_update_notice_dismissed`, so dismissing it anywhere dismisses it
|
||||
* everywhere in this browser. The shell shows it above every page; System > Sources keeps the concept's copy in the
|
||||
* page, so the shell hides on that section and the two never show together. Storage that cannot be written still
|
||||
* dismisses it for this page.
|
||||
*/
|
||||
import { createButton } from '../shared/toolkit/core.js';
|
||||
import { openMetadataScan } from './activity-scan.mjs';
|
||||
import { showWorkspaceToast } from './toast.mjs';
|
||||
|
||||
export const NOTICE_KEY = 'pm_update_notice_dismissed';
|
||||
export const NOTICE_VERSION = '3.2.0';
|
||||
export const SOURCES_UNREADABLE = 'Could not read the image sources. Try again.';
|
||||
|
||||
const SETTINGS_URL = '/prompt_manager/settings';
|
||||
const defaultStorage = () => window.localStorage;
|
||||
const changes = new EventTarget();
|
||||
// The storages this page dismissed the notice in without being able to write it.
|
||||
const dismissedHere = new WeakSet();
|
||||
|
||||
/** Whether this browser dismissed the 3.2.0 notice; storage that cannot be read shows it. */
|
||||
export function noticeDismissed(getStorage = defaultStorage) {
|
||||
if (dismissedHere.has(getStorage)) return true;
|
||||
try {
|
||||
return getStorage().getItem(NOTICE_KEY) === NOTICE_VERSION;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Dismiss the notice everywhere on this page. Returns whether it was saved for the next visit. */
|
||||
export function dismissNotice(getStorage = defaultStorage) {
|
||||
let saved = true;
|
||||
try {
|
||||
getStorage().setItem(NOTICE_KEY, NOTICE_VERSION);
|
||||
} catch {
|
||||
dismissedHere.add(getStorage);
|
||||
saved = false;
|
||||
}
|
||||
changes.dispatchEvent(new Event('dismissed'));
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** Calls `onDismissed` when the notice is dismissed on this page or in another tab, until `scope` ends. */
|
||||
export function watchNoticeDismissal({ scope, storage = defaultStorage, onDismissed }) {
|
||||
changes.addEventListener('dismissed', onDismissed);
|
||||
scope.cleanup(() => changes.removeEventListener('dismissed', onDismissed));
|
||||
scope.listen(window, 'storage', event => {
|
||||
if (event.key === NOTICE_KEY && noticeDismissed(storage)) onDismissed();
|
||||
});
|
||||
}
|
||||
|
||||
function el(tag, text, className) {
|
||||
const node = document.createElement(tag);
|
||||
if (text !== undefined) node.textContent = text;
|
||||
if (className) node.className = className;
|
||||
return node;
|
||||
}
|
||||
|
||||
/** The concept's `.notice` with Later and Review scan. */
|
||||
export function noticeNode({ scope, onLater, onReview }) {
|
||||
const notice = el('div', undefined, 'notice');
|
||||
const text = el('div');
|
||||
text.append(el('h2', 'Bring older images into your prompt history'),
|
||||
el('p', 'The schema upgrades automatically. An optional metadata scan links existing positives and negatives without regenerating all thumbnails.'));
|
||||
const actions = el('div', undefined, 'row-actions');
|
||||
const later = createButton('Later');
|
||||
const review = createButton('Review scan', 'ca-primary');
|
||||
scope.listen(later, 'click', onLater);
|
||||
scope.listen(review, 'click', () => onReview(review));
|
||||
actions.append(later, review);
|
||||
notice.append(text, actions);
|
||||
return notice;
|
||||
}
|
||||
|
||||
/** How many output folders a scan reads, from GET /settings; null when the answer does not say. */
|
||||
export function sourceCount(settings) {
|
||||
const count = settings?.sources?.effective?.length;
|
||||
return Number.isSafeInteger(count) ? count : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount the shell's notice in `container`, hidden until the first `showFor(route)`. The pressed button leaves with the
|
||||
* notice, so focus moves to `main`. If the sources cannot be read, Review scan says so and the notice stays.
|
||||
*/
|
||||
export function mountOlderImagesNotice(container, { api, activity, scope, main, storage = defaultStorage, notify = showWorkspaceToast }) {
|
||||
let onSources = true;
|
||||
let reading = false;
|
||||
const render = () => {
|
||||
const show = !onSources && !noticeDismissed(storage);
|
||||
container.hidden = !show;
|
||||
if (!show) notice.remove();
|
||||
else if (!notice.isConnected) container.append(notice);
|
||||
};
|
||||
const notice = noticeNode({ scope,
|
||||
onLater: () => {
|
||||
dismissNotice(storage);
|
||||
main.focus({ preventScroll: true });
|
||||
},
|
||||
onReview: async button => {
|
||||
if (reading) return;
|
||||
reading = true;
|
||||
button.setAttribute('aria-disabled', 'true');
|
||||
let sources = null;
|
||||
try {
|
||||
sources = sourceCount(await api.get(SETTINGS_URL, { signal: scope.signal, cache: 'no-store' }));
|
||||
} catch {
|
||||
if (scope.signal.aborted) return;
|
||||
}
|
||||
reading = false;
|
||||
button.removeAttribute('aria-disabled');
|
||||
if (sources === null) {
|
||||
notify(SOURCES_UNREADABLE);
|
||||
return;
|
||||
}
|
||||
dismissNotice(storage);
|
||||
main.focus({ preventScroll: true });
|
||||
openMetadataScan({ scope, store: activity, sources, trigger: main });
|
||||
} });
|
||||
watchNoticeDismissal({ scope, storage, onDismissed: render });
|
||||
return {
|
||||
showFor(route) {
|
||||
onSources = route.id === 'system' && route.section === 'sources';
|
||||
render();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,8 +2,9 @@
|
||||
* System › Sources & scans (#249 System S4 Task 11): the concept's `renderSystem` "sources" body (app.js 679–681).
|
||||
*
|
||||
* - The optional `.notice` "Bring older images into your prompt history" shows until this browser dismissed the 3.2.0
|
||||
* update notice (`pm_update_notice_dismissed`, the legacy admin page's own key). Later dismisses it without scanning;
|
||||
* Review scan dismisses it, as the legacy page's Scan does, and reviews every source.
|
||||
* update notice (older-images-notice.mjs, the legacy admin page's own key). Later dismisses it without scanning;
|
||||
* Review scan dismisses it, as the legacy page's Scan does, and reviews every source. D166: dismissing it in the shell
|
||||
* or another tab removes it here too, and the shell does not show it on this section.
|
||||
* - "Image sources" lists the server's sources (GET /settings, S4 Task 9) in `#source-list`, one `.operation` each:
|
||||
* its label, "<path> · watching | watching until restart | manual scan | restart pending" and "Scan changes"
|
||||
* (watched) or "Scan folder", which opens the shared "Review metadata scan" (D55) for that one source ("1 output
|
||||
@@ -38,14 +39,15 @@ import { settingRow, operationRow } from './system-controls.mjs';
|
||||
import { HOST_UNAVAILABLE } from './system-appearance.mjs';
|
||||
import { showWorkspaceToast } from './toast.mjs';
|
||||
import { noun } from './plural.mjs';
|
||||
import { dismissNotice, noticeDismissed, noticeNode, watchNoticeDismissal } from './older-images-notice.mjs';
|
||||
|
||||
export { NOTICE_KEY, NOTICE_VERSION, dismissNotice, noticeDismissed } from './older-images-notice.mjs';
|
||||
|
||||
const SETTINGS_URL = '/prompt_manager/settings';
|
||||
const VALIDATE_URL = '/prompt_manager/settings/validate-source';
|
||||
const RESCAN_URL = '/prompt_manager/gallery/rescan';
|
||||
const ROOTS_KEY = 'gallery_root_paths';
|
||||
|
||||
export const NOTICE_KEY = 'pm_update_notice_dismissed';
|
||||
export const NOTICE_VERSION = '3.2.0';
|
||||
export const WATCH_COPY = Object.freeze({ watching: 'watching', stopping: 'watching until restart', manual: 'manual scan',
|
||||
restart_pending: 'restart pending' });
|
||||
// The states whose observer runs now: their scan only has to catch up on changes.
|
||||
@@ -87,25 +89,6 @@ const addedLabels = new Map();
|
||||
|
||||
const defaultStorage = () => window.localStorage;
|
||||
|
||||
/** Whether this browser dismissed the 3.2.0 notice; storage that cannot be read shows it. */
|
||||
export function noticeDismissed(getStorage = defaultStorage) {
|
||||
try {
|
||||
return getStorage().getItem(NOTICE_KEY) === NOTICE_VERSION;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Dismiss the notice for this browser; false when storage refused (it stays dismissed for this view). */
|
||||
export function dismissNotice(getStorage = defaultStorage) {
|
||||
try {
|
||||
getStorage().setItem(NOTICE_KEY, NOTICE_VERSION);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const sameRoots = (a, b) => Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((path, index) => path === b[index]);
|
||||
|
||||
/** The roots to draft: the saved list itself when they are equal, so the draft is clean again. */
|
||||
@@ -168,22 +151,6 @@ function el(tag, text, className) {
|
||||
return node;
|
||||
}
|
||||
|
||||
/** The concept's `.notice` with Later and Review scan. */
|
||||
function noticeNode({ scope, onLater, onReview }) {
|
||||
const notice = el('div', undefined, 'notice');
|
||||
const text = el('div');
|
||||
text.append(el('h2', 'Bring older images into your prompt history'),
|
||||
el('p', 'The schema upgrades automatically. An optional metadata scan links existing positives and negatives without regenerating all thumbnails.'));
|
||||
const actions = el('div', undefined, 'row-actions');
|
||||
const later = createButton('Later');
|
||||
const review = createButton('Review scan', 'ca-primary');
|
||||
scope.listen(later, 'click', onLater);
|
||||
scope.listen(review, 'click', onReview);
|
||||
actions.append(later, review);
|
||||
notice.append(text, actions);
|
||||
return notice;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Add image source" (the concept's `simpleInput`, D127): the server checks the folder; a refused one keeps the
|
||||
* dialog open with the server's reason under the field. Enter in the field adds, as the Add button does.
|
||||
@@ -263,6 +230,7 @@ export function mountSources(container, { api, activity, scope, drafts, notify =
|
||||
notice.remove();
|
||||
scanAll(addButton);
|
||||
} });
|
||||
watchNoticeDismissal({ scope, storage, onDismissed: () => notice.remove() });
|
||||
nodes.push(notice);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
</header>
|
||||
<!-- D129: the library-location notice, shown above every page while a node-folder library can move. -->
|
||||
<div id="library-banner" class="library-banner" hidden></div>
|
||||
<!-- D166: the one-time upgrade notice, shown above every page until dismissed. -->
|
||||
<div id="older-images-notice" class="library-banner" hidden></div>
|
||||
<main id="workspace-main" tabindex="-1"></main>
|
||||
<footer class="app-footer"><span>ComfyAssets · Local tools. Your library.</span></footer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user