* feat(images): card selection checkbox and the concept selection bar
Every Images card's overlay starts with the concept's "Select <filename>"
checkbox before the star. Checked image keys form the page's selection,
kept across Load more, infinite scroll and bounded re-mounting and cleared
whenever the collection is replaced. The #bulkbar between the filter row
and the gallery shows "N selected" with Set category, Export metadata and
Clear (the first two are wired by the next tasks); Clear empties it.
The interim D8 star rule is removed, and the concept comparison now
measures the overlay with its checkbox and the bar with one card checked.
* feat(images): export the selected images' metadata as one JSON bundle
The selection bar's Export metadata reads each checked image's inspection,
at most four at a time, and downloads prompt-manager-images-metadata.json:
format, version 1, exported_at and, in listing order, the same envelope the
inspector's "Download metadata JSON" saves (one shared builder,
metadataEnvelope). The button is aria-disabled and ignores a second press
while it runs, the selection is kept, and the toast reports the exported
count, the images that could not be read, or that none could (no download).
Also: the bar and its count are built only on the Images page, the
Library-embedded gallery is asserted to have no checkbox or bar, a sort
change is asserted to clear the selection, the checkbox label test asserts
the toggle and an unchanged selection and inspector, and TESTING.md records
that a return to Images keeps the selection while a reload starts empty.
* feat(images): set the category of the selected images' linked prompts
The selection bar's Set category opens the concept's simpleInput dialog
("Categorize selected images", one Category field, Cancel / Apply) and
posts the selection's distinct linked positive ids to
/prompt_manager/bulk/category. Images have no category of their own; the
listing's category filter matches the linked positive's, so that is what
changes. With no linked image checked nothing is posted and a toast says
so; on success the toast counts the prompts and any skipped images, then
the categories and the listing reload (clearing the selection); a failure
keeps the selection. The button ignores a second press while it runs.
The bulk category route now rejects anything but a non-empty list of at
most 1000 positive integer ids (deduplicated in order) and validates the
category with validate_category; bad JSON or a non-object body is a 400.
The response shape and the legacy admin request are unchanged.
* test(images): zoomed card keyboard order follows the concept's checkbox before the star
The card's Tab order is image, Select checkbox, star, Inspect (concept app.js:398), both ways; List hides the overlays as the concept does, so there the image is followed by Inspect.
* fix(api): answer 400 for bulk category ids beyond SQLite's INTEGER range
An id of 2**63 or more passed the parser and made sqlite3 raise OverflowError, which the route turned into a 500.
* refactor(images): move the selection bar into images-selection.mjs and harden its actions
- images.mjs is back under 800 lines; the bar, its checked set, Export metadata and Set category live in images-selection.mjs.
- Busy flags release in finally; the dead guards are gone and the category targets are taken when the modal dialog opens.
- The category write no longer carries the page signal; leaving Images mid-write drops the saved visit so the return reloads.
- The categories and the listing reload together after Set category.
- The Category field stops at the server's 100 characters; the server's updated_count is a tested pure helper.
- New browser tests: leaving mid-export and mid-write, Blob URL revoke, updated_count 0 and a 400 rejection.
- Additive coverage baseline entries for images-category, images-export and images-selection.
- TESTING.md D8 records the dialog deviations and these rulings.
41 lines
1.8 KiB
JavaScript
41 lines
1.8 KiB
JavaScript
import { metadataEnvelope } from './file-inspector.mjs';
|
|
import { countOf } from './plural.mjs';
|
|
|
|
/** The Images selection bar's "Export metadata" (#249 D8): one JSON bundle of the inspector's envelopes. */
|
|
export const BUNDLE_FILENAME = 'prompt-manager-images-metadata.json';
|
|
const BUNDLE_FORMAT = 'prompt-manager-images-metadata';
|
|
const EXPORT_CONCURRENCY = 4;
|
|
|
|
export function metadataBundle(envelopes, exportedAt) {
|
|
return {format:BUNDLE_FORMAT, version:1, exported_at:exportedAt, images:[...envelopes]};
|
|
}
|
|
|
|
/**
|
|
* Read every image's inspection record, at most four at a time, and keep the envelopes in the images' order.
|
|
* A read that fails, or a record without full metadata, counts as one image that could not be read.
|
|
*/
|
|
export async function collectImagesMetadata(images, read) {
|
|
const envelopes = new Array(images.length).fill(null);
|
|
let next = 0;
|
|
async function worker() {
|
|
while (next < images.length) {
|
|
const index = next++;
|
|
try {
|
|
envelopes[index] = metadataEnvelope(await read(images[index]));
|
|
} catch {
|
|
// Counted below with the records that carry no full metadata.
|
|
}
|
|
}
|
|
}
|
|
await Promise.all(Array.from({length:Math.min(EXPORT_CONCURRENCY, images.length)}, worker));
|
|
const found = envelopes.filter(envelope => envelope !== null);
|
|
return {envelopes:found, failed:images.length - found.length};
|
|
}
|
|
|
|
/** The export's toast; with nothing exported there is no download. */
|
|
export function exportOutcome(exported, total) {
|
|
if (!exported) return 'Metadata could not be read. Try again.';
|
|
if (exported === total) return `Exported metadata for ${countOf(exported, 'image')}.`;
|
|
return `Exported metadata for ${exported} of ${countOf(total, 'image')}. ${total - exported} could not be read.`;
|
|
}
|