feat(library): Delete prompt… on a prompt's page (#481)
The recipe editor's actions read Create text revision, Archive variant… (the selected negative variant's archive, renamed from Archive… so the two danger buttons cannot be confused) and Delete prompt…, which closes the editor and runs the Library's Delete… review for that prompt: the same preview, saved-pair choice (Archive stays the default), backup and write. A delete opens the Prompt library; an archive reads the page again. Recorded as D161 in TESTING.md and in the editor concept comparison.
This commit is contained in:
@@ -328,3 +328,31 @@ export function previewOutcome(failure) {
|
||||
if (failure?.code === 'busy') return failureOutcome(failure, {});
|
||||
return { toast: COPY.reviewFailed, committed: false, reload: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* #481: review and run a delete of `ids` outside the selection bar (a prompt's own page): the same preview, review
|
||||
* (#482's choice included), backup and write. `onSettled(outcome, result, failure)` runs once the write has answered
|
||||
* (or the preview failed: `result` null); a dismissed review calls `onDismiss()`. The toast is the caller's.
|
||||
*/
|
||||
export async function reviewPromptDelete({ api, scope, trigger, ids: promptIds, epoch, titleOf, onSettled, onDismiss = () => {} }) {
|
||||
const body = { prompt_ids: promptIds, expected_epoch: epoch };
|
||||
let preview = null, failure = null;
|
||||
try {
|
||||
preview = await api.post(PREVIEW_URL, body, { signal: scope.signal });
|
||||
} catch (error) {
|
||||
failure = error;
|
||||
}
|
||||
if (scope.signal.aborted) return;
|
||||
if (failure !== null || !preview?.success) { onSettled(previewOutcome(failure), null, failure); return; }
|
||||
const review = { ...reviewOf(preview), epoch };
|
||||
openDeleteReview({ scope, trigger, review, titleOf, onDismiss,
|
||||
onStart: async chosen => {
|
||||
let result = null, error = null;
|
||||
try {
|
||||
result = await api.post(DELETE_URL, deleteBody(chosen));
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
onSettled(deleteOutcome(result, error, chosen), result?.success ? result : null, error);
|
||||
} });
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ function datalist(id, values) {
|
||||
* exit when prompt storage was restored; the dialog closes first.
|
||||
*/
|
||||
export function createLibraryEditor({api, parentScope, onCommitted = () => {}, onReconciled = () => {},
|
||||
onRefreshRequired = () => window.location.reload()}) {
|
||||
onRefreshRequired = () => window.location.reload(), onDeletePrompt = null}) {
|
||||
let session = null, disposed = false;
|
||||
const dialog = el('dialog', undefined, 'library-editor');
|
||||
dialog.setAttribute('aria-labelledby', 'library-editor-title');
|
||||
@@ -222,7 +222,10 @@ export function createLibraryEditor({api, parentScope, onCommitted = () => {}, o
|
||||
} else form.append(el('p', NEGATIVE_HELP, 'small'));
|
||||
const line = statusLine(), recovery = new Map();
|
||||
const actions = el('div', undefined, 'dialog-actions');
|
||||
const revise = createButton('Create text revision'), archive = createButton('Archive…', 'danger');
|
||||
// D161 (#481): the variant's archive is named for it, beside the prompt's own Delete prompt… (the concept's one
|
||||
// "Archive…" in this row archives the prompt, app.js 976).
|
||||
const revise = createButton('Create text revision'), archive = createButton('Archive variant…', 'danger');
|
||||
const removePrompt = createButton('Delete prompt…', 'danger');
|
||||
const cancel = createButton('Cancel'), save = createButton('Save', 'ca-primary');
|
||||
save.type = 'submit';
|
||||
form.append(line.note, actions);
|
||||
@@ -237,6 +240,12 @@ export function createLibraryEditor({api, parentScope, onCommitted = () => {}, o
|
||||
cancel.addEventListener('click', () => finish(), {signal: views.signal});
|
||||
revise.addEventListener('click', () => open({mode: 'revision', positiveId, pairId, trigger: session.trigger}), {signal: views.signal});
|
||||
archive.addEventListener('click', () => archiveView(), {signal: views.signal});
|
||||
// #481: the prompt's own delete review (#443/#482) replaces this dialog; focus returns to its trigger.
|
||||
removePrompt.addEventListener('click', () => {
|
||||
const {trigger} = session;
|
||||
finish();
|
||||
onDeletePrompt?.({positiveId, trigger});
|
||||
}, {signal: views.signal});
|
||||
if (!negativeOnly) loadVocabulary(tags, views.signal);
|
||||
|
||||
let categoryKey = null, refocus = null;
|
||||
@@ -256,6 +265,7 @@ export function createLibraryEditor({api, parentScope, onCommitted = () => {}, o
|
||||
cancel.disabled = close.disabled = saving;
|
||||
// After a restore the numeric IDs may name another prompt; only the Library reload exit stays.
|
||||
revise.disabled = saving || restoredStorage(state);
|
||||
removePrompt.disabled = saving || restoredStorage(state);
|
||||
const pairProblem = readOnly && !state.pair && pairId === null ? NO_PAIR : state.pair?.archived ? ALREADY_ARCHIVED : null;
|
||||
archive.disabled = !SUBMITTABLE.has(state.phase) || !state.pair || state.pair.archived;
|
||||
const status = statusOf(state, false, mode);
|
||||
@@ -263,7 +273,8 @@ export function createLibraryEditor({api, parentScope, onCommitted = () => {}, o
|
||||
if (!status && pairProblem) archive.setAttribute('aria-describedby', line.note.id);
|
||||
else archive.removeAttribute('aria-describedby');
|
||||
const retry = recoveryButton(recovery, status?.action, views.signal);
|
||||
setChildren(actions, [...(retry ? [retry] : []), ...(readOnly ? [revise, archive] : []), cancel, save]);
|
||||
const prompt = readOnly && onDeletePrompt ? [removePrompt] : [];
|
||||
setChildren(actions, [...(retry ? [retry] : []), ...(readOnly ? [revise, archive, ...prompt] : []), cancel, save]);
|
||||
refocus = restoreFocus(refocus, state, [retry, save]);
|
||||
if (state.phase === 'complete') finish();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {libraryFilters,libraryViewState} from './saved-views.mjs';
|
||||
import {openRefineDialog,openContentDialog} from './workspace-dialogs.mjs';
|
||||
import {mountLibrarySelection} from './library-selection.mjs';
|
||||
import {fallBackToOriginal,replaceOnError,showsThumbnail,thumbnailSource} from './thumbnail-support.mjs';
|
||||
import {COPY as DELETE_COPY,RESTORE_URL,archivedNoticeText,restoreOutcome} from './library-delete.mjs';
|
||||
import {COPY as DELETE_COPY,RESTORE_URL,archivedNoticeText,restoreOutcome,reviewPromptDelete} from './library-delete.mjs';
|
||||
|
||||
let shelfVisit=null;
|
||||
// Every confirmed receipt advances this; a shelf mounted before it never restores its cached stream.
|
||||
@@ -496,7 +496,28 @@ export function mountLibrary(main,api,parentScope,{positiveId,savedState=null,on
|
||||
let pendingSelection=takeSavedSelection(positiveId);
|
||||
if(selectedPairs.get(positiveId)?.id!==pendingSelection) selectedPairs.delete(positiveId);
|
||||
const loader=createLibraryDetail({api,onChange:render});scope.cleanup(()=>loader.dispose());
|
||||
const editor=createLibraryEditor({api,parentScope:scope,onCommitted:invalidateLibraryAfterWrite,onReconciled:reconciled,onRefreshRequired:reloadView});
|
||||
const editor=createLibraryEditor({api,parentScope:scope,onCommitted:invalidateLibraryAfterWrite,onReconciled:reconciled,onRefreshRequired:reloadView,
|
||||
onDeletePrompt:()=>deletePrompt()});
|
||||
/** #481: this prompt through the Library's Delete… review; a delete returns to the shelf, an archive shows its notice. */
|
||||
let deleting=false;
|
||||
function deletePrompt() {
|
||||
const shown=detail;if(deleting || !shown?.positive) return;
|
||||
// One review at a time, as the selection bar holds itself (a second press during the preview starts nothing).
|
||||
deleting=true;
|
||||
reviewPromptDelete({api,scope,trigger:edit,ids:[shown.positive.id],epoch:shown.library_epoch,
|
||||
titleOf:()=>displayTitle(shown.positive),onDismiss:()=>{deleting=false;if(edit.isConnected) edit.focus({preventScroll:true});},
|
||||
onSettled:(outcome,result,failure)=>{
|
||||
deleting=false;
|
||||
if(outcome.committed) invalidateLibraryAfterWrite({kind:'bulk-delete',receipt:result ?? {phase:'unconfirmed'}});
|
||||
// A left page still invalidates what it cached, but says nothing (as the bar's write).
|
||||
if(scope.signal.aborted) return;
|
||||
showWorkspaceToast(outcome.toast);
|
||||
// Restored storage: the whole Library view reads again, as the editor's own restore path.
|
||||
if(failure?.code==='storage-changed') {reloadView();return;}
|
||||
if(result && Number.isSafeInteger(result.deleted) && result.deleted>0) {focusHeadingOnMount=true;window.location.hash='#/library';return;}
|
||||
if(outcome.reload) {detail=null;loader.load(positiveId);focusHeading(main);}
|
||||
}});
|
||||
}
|
||||
scope.listen(back,'click',()=>{window.location.hash='#/library';});
|
||||
scope.listen(edit,'click',()=>{
|
||||
const draft=metadataDrafts.get(positiveId);metadataDrafts.delete(positiveId);
|
||||
|
||||
Reference in New Issue
Block a user