fix(library): act on a loaded prompt only, keep focus on a gone page, close the menu on focus loss (#512)

Review fixes: Edit metadata and the More menu are disabled while the
prompt loads or failed to load; on a gone prompt's page a focused control
hands focus to Open the Prompt library; the menu closes when focus leaves
it. A unit pins that a paired prompt with a missing digest is not called
another prompt's negative.
This commit is contained in:
Vito Sansevero
2026-10-29 16:24:49 -07:00
parent 1b12ad0262
commit 68d501b161
4 changed files with 13 additions and 3 deletions
@@ -318,6 +318,9 @@ test('a negative used by another prompt is named as such when it is archived', a
"that prompt's variant stays. To bring them back, press Show archived prompts, select them and press Restore."]);
const mixed = reviewOf(preview({archive: [2, 5], deletable: [2], review_digests: {2: 'c'.repeat(64)}}));
assert.deepEqual(reviewGrid(mixed)[0], ['Archive', "2 prompts in a saved pair (1 used as another prompt's negative): hidden, restorable"]);
// A paired prompt whose review digest is missing is not called another prompt's negative (review M1).
const noDigest = reviewOf(preview({archive: [2], deletable: [2], review_digests: {}}));
assert.deepEqual(reviewGrid(noDigest), [['Archive', '1 prompt in a saved pair: hidden, restorable']]);
const sentences = reviewSentences(mixed);
assert.match(sentences[0], /^Prompts in a saved pair, as its positive or its negative, are archived instead of deleted/);
assert.match(sentences[1], /^Prompts used as another prompt's negative are archived instead of deleted, so that prompt's variant stays\.$/);
+1
View File
@@ -67,6 +67,7 @@ function pairedOf(preview) {
variants: count(counts.variants), keptNegatives: count(counts.kept_negatives) },
pairedImageLinks: count(preview?.paired_image_links), choice: 'archive',
// #512: archive ids the server says cannot be deleted (another prompt's negative); none from an older answer.
// The server's own list, not the digest-checked one: a deletable prompt with a bad digest is still paired.
archiveOnly: Array.isArray(preview?.deletable)
? [...archive].filter(id => !ids(preview.deletable).includes(id)) : [],
};
+7 -3
View File
@@ -581,12 +581,16 @@ export function mountLibrary(main,api,parentScope,{positiveId,savedState=null,on
contentScope?.dispose();contentScope=createScope();detail=null;updateBranches=null;updateParent=null;current=null;send.disabled=true;
blocks?.dispose();blocks=null;galleryScope?.dispose();galleryScope=null;body.replaceChildren();
// #512: a prompt that no longer exists (deleted, or a stale link) says so, with the way back; no Retry.
// Edit metadata and ⋯ act on a loaded prompt only (review M2).
const hadFocus=[edit,moreMenu.button].includes(document.activeElement);
const loaded=!state.loading && !state.error;edit.disabled=!loaded;moreMenu.setDisabled(!loaded);
if(state.error?.status===404) {
edit.disabled=true;moreMenu.setDisabled(true);
const message=el('p',PROMPT_GONE,'library-status');message.setAttribute('role','status');body.append(message);
const open=createButton('Open the Prompt library');contentScope.listen(open,'click',()=>{window.location.hash='#/library';});body.append(open);return;
const open=createButton('Open the Prompt library');contentScope.listen(open,'click',()=>{window.location.hash='#/library';});body.append(open);
// A focused control that was just disabled hands focus to the way back (review M3).
if(hadFocus) open.focus({preventScroll:true});
return;
}
edit.disabled=false;moreMenu.setDisabled(false);
if(state.loading || state.error) {
const message=el('p',state.loading?'Loading prompt history…':'Prompt history could not be loaded.','library-status');message.setAttribute('role',state.error?'alert':'status');body.append(message);
if(state.error) {const retry=createButton('Retry prompt history');contentScope.listen(retry,'click',()=>loader.load(positiveId));body.append(retry);}return;
+2
View File
@@ -71,6 +71,8 @@ export function createMoreMenu({ scope, label, items }) {
event.preventDefault();
keys[event.key]();
});
// Focus leaving the menu (a screen reader's cursor, a script) closes it too.
scope.listen(element, 'focusout', event => { if (event.relatedTarget && !element.contains(event.relatedTarget)) close(false); });
// An outside press closes it; capture, so a press that a page control stops still counts.
const outside = event => { if (isOpen() && !element.contains(event.target)) close(false); };
document.addEventListener('pointerdown', outside, true);