fix(images): focus after a changed source; Browse is a fresh Images visit (#332)
Review fixes: a 409 source-changed reload moves focus to the heading as a success does; a selection the listing no longer holds says none could be found instead of sending nothing; a Tags & categories Browse opens a plain Images visit filtered by the category (remembered and with the Appearance defaults) rather than a saved-view visit.
This commit is contained in:
+6
-4
@@ -1266,7 +1266,8 @@ Recorded deviations on this page (spec §8):
|
||||
it finish without a toast, and the return loads the collection afresh instead of the
|
||||
saved visit. Its success reloads the categories and the listing together (clearing
|
||||
the selection, focus to the heading, and, as any replaced collection does, the
|
||||
inspector moves to the first listed image); failure keeps the selection. `images selection bar matches
|
||||
inspector moves to the first listed image); a failure keeps the selection, except a
|
||||
changed source (409), which reloads the listing as a success does. `images selection bar matches
|
||||
the concept` compares the bar and the checked card with one card checked; the omitted
|
||||
actions flow again from the concept's own boxes, checked against the concept page with
|
||||
them removed at runtime.
|
||||
@@ -1475,8 +1476,8 @@ Recorded deviations on this page (spec §8):
|
||||
- **D146** (#332, amends D21 and D39): the category chip names its origin in visible
|
||||
text. It is `span.chip` > `span.ca-visually-hidden` (the D21 prefix) + the category +
|
||||
`span.chip-source` (" · this image", " · prompt" or " · recipe"; muted
|
||||
`--ca-muted`, `white-space: nowrap`, so at 390px the chip may wrap only before the
|
||||
origin). It shows when the inspection names a non-empty category and either the
|
||||
`--ca-muted`, `white-space: nowrap`, so at 390px the origin never splits; the chip may wrap
|
||||
before it or inside a long category). It shows when the inspection names a non-empty category and either the
|
||||
scope is `image` (any link status, so an unlinked or ambiguous file with an own
|
||||
category shows it) or the file is linked. The concept's chip shows the bare name
|
||||
(app.js a402). The inspector comparison (inspector-concept.spec) measures each chip's
|
||||
@@ -5670,7 +5671,8 @@ fixture-only tags after the concept's sample tags.
|
||||
(`image_count`, the stored rows, missing files included): "N positive prompts · M
|
||||
images" beside prompts, "M images" for an image-only value, which follows every
|
||||
prompt-backed row (by value) before Manage category. Browse on an image-only row opens
|
||||
Images with exactly that Category filter (its Library would be empty); every other
|
||||
Images with exactly that Category filter (its Library would be empty), as a fresh
|
||||
Images visit that becomes the Images return state, the way Browse seeds the Library's; every other
|
||||
row opens the Library as before. Manage category's select lists image-only values
|
||||
last, so past 1,000 categories they are the first left out (the dialog says the list is
|
||||
capped).
|
||||
|
||||
@@ -2252,6 +2252,8 @@ test('a changed source says so and reloads the listing (#332)', async ({ page })
|
||||
await expect(toast(page)).toHaveText('The sources changed. Refresh the images and try again.');
|
||||
await reloaded;
|
||||
await expect(cards(page)).toHaveCount(2);
|
||||
// The reload hides the bar with Set category in it: focus moves to the page heading, as after a success.
|
||||
await expect(page.locator('#workspace-main h1')).toBeFocused();
|
||||
});
|
||||
|
||||
test('a recipe-linked image is listed under its recipe\'s category (#332 D142)', async ({ page, request }) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { imageKey, toggleSelection, selectionLabel } from './images-state.mjs';
|
||||
import { inspectPath, toInspectionRecord } from './inspection-state.mjs';
|
||||
import { BUNDLE_FILENAME, metadataBundle, collectImagesMetadata, exportOutcome } from './images-export.mjs';
|
||||
import { CATEGORY_DIALOG, CATEGORY_MAX_LENGTH, CATEGORY_URL, MAX_CATEGORY_IMAGES, REMOVE_DIALOG, USE_PROMPT_CATEGORY,
|
||||
clearOutcome, failureMessage, removeQuestion, setOutcome } from './images-category.mjs';
|
||||
NONE_FOUND, clearOutcome, failureMessage, removeQuestion, setOutcome } from './images-category.mjs';
|
||||
import { closeWorkspaceDialog, openContentDialog, openInputDialog } from './workspace-dialogs.mjs';
|
||||
import { TOO_MANY, bulkTargets, openAddTags, openAutoTag } from './images-tagging-bulk.mjs';
|
||||
import { showWorkspaceToast } from './toast.mjs';
|
||||
@@ -270,7 +270,9 @@ export function mountSelectionBar({ scope, api, grid, getItems, focusFallback, o
|
||||
if (categorizing) return;
|
||||
const count = allMatching ? getListing().total : checked.size;
|
||||
const targets = allMatching ? {filter: getFilter()} : {items: bulkTargets(checkedImages())};
|
||||
if (count > MAX_CATEGORY_IMAGES || (!allMatching && !targets.items)) { showWorkspaceToast(TOO_MANY); return; }
|
||||
if (count > MAX_CATEGORY_IMAGES) { showWorkspaceToast(TOO_MANY); return; }
|
||||
// Checked files the listing no longer holds (a refresh dropped them): nothing to send.
|
||||
if (!allMatching && !targets.items?.length) { showWorkspaceToast(NONE_FOUND); return; }
|
||||
openInputDialog({ scope, trigger: button, title: CATEGORY_DIALOG.title, label: CATEGORY_DIALOG.label,
|
||||
action: CATEGORY_DIALOG.action, maxLength: CATEGORY_MAX_LENGTH, suggestions: getCategories(),
|
||||
secondary: { label: USE_PROMPT_CATEGORY, onClick: () => confirmRemove(button, targets, count) },
|
||||
@@ -293,8 +295,8 @@ export function mountSelectionBar({ scope, api, grid, getItems, focusFallback, o
|
||||
}
|
||||
/**
|
||||
* The write carries no page signal: once sent it finishes even if the page is left (`writing` then tells the page
|
||||
* not to save a visit that predates it). A failure keeps the selection (a changed source reloads the listing); a
|
||||
* success reloads through `onCategorySet`.
|
||||
* not to save a visit that predates it). A failure keeps the selection, except a changed source, which reloads the
|
||||
* listing as a success does (`onCategorySet`).
|
||||
*/
|
||||
async function writeCategory(button, send, outcome) {
|
||||
categorizing = true;
|
||||
@@ -309,13 +311,16 @@ export function mountSelectionBar({ scope, api, grid, getItems, focusFallback, o
|
||||
release();
|
||||
}
|
||||
if (scope.signal.aborted) return;
|
||||
const focused = document.activeElement === button;
|
||||
if (failure !== null || !result) {
|
||||
showWorkspaceToast(failureMessage(failure));
|
||||
if (failure?.code === 'source-changed') onCategorySet();
|
||||
// A changed source reloads the listing (clearing the selection); any other failure keeps it.
|
||||
if (failure?.code !== 'source-changed') return;
|
||||
onCategorySet();
|
||||
if (focused) focusFallback.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
showWorkspaceToast(outcome(result));
|
||||
const focused = document.activeElement === button;
|
||||
onCategorySet();
|
||||
// The reload hides the bar with the focused action in it, as Clear does: focus moves to the page heading.
|
||||
if (focused) focusFallback.focus({ preventScroll: true });
|
||||
|
||||
@@ -125,11 +125,12 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
scope.cleanup(inspection.subscribe(next => detailInspector?.setState(next)));
|
||||
scope.cleanup(() => inspection.dispose());
|
||||
scope.cleanup(() => detailInspector?.dispose());
|
||||
// A Tags & categories Browse (browseImages) is handed to this mount once, as a saved view's state is.
|
||||
// A Tags & categories Browse (browseImages) is handed to this mount once: a fresh Images visit filtered by its
|
||||
// category, which becomes the Images return state like any plain visit (as Browse does for the Library).
|
||||
const handed = !embedded && !favorites && !savedState ? browseState : null;
|
||||
if (!embedded && !favorites) browseState = null;
|
||||
const saved = embedded || !(savedState || handed) ? null : imagesFilters(savedState || handed);
|
||||
const prior = embedded || saved || favorites ? null : lastVisit;
|
||||
const saved = embedded || !savedState ? null : imagesFilters(savedState);
|
||||
const prior = embedded || saved || favorites || handed ? null : lastVisit;
|
||||
// A stale visit keeps its filters, layout and size, and lists again instead of restoring (M6).
|
||||
const visit = prior?.stale ? null : prior;
|
||||
// The layout and size defaults are the Images page's: an embedded gallery (a prompt history's, its dialogs) has no
|
||||
@@ -148,6 +149,7 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
const rendered = new Map();
|
||||
const itemIndexes = new Map();
|
||||
const filters = { root: 'all', q: '', category: '', tags: [], subfolder: '', media: 'all', sort: 'newest', ...prior?.filters, ...saved,
|
||||
...(handed ? {category: handed.category} : {}),
|
||||
favorites: favorites || saved?.favorites === true };
|
||||
filters.tags = [...filters.tags];
|
||||
if (!IMAGE_SORT_OPTIONS.some(([value]) => value === filters.sort)) filters.sort = 'newest';
|
||||
|
||||
Reference in New Issue
Block a user