Don't re-prompt when canceling out of a saved-search edit

When another row was Cmd-clicked while editing a saved search, canceling the
save-changes prompt tried to revert via selectByID(), which no-ops when the row
is already part of the (new multi-)selection -- so the change stuck and the
prompt immediately reopened. Reset to the edited search with selection.select().
This commit is contained in:
Dan Stillman 2026-06-16 12:42:26 -04:00
parent 206ab311db
commit ea68385428
2 changed files with 46 additions and 2 deletions

View file

@ -1811,15 +1811,24 @@ var ZoteroPane = new function () {
case 0:
await advancedSearchDeck.pane.save();
return;
case 1:
case 1: {
// Revert to just the search being edited. Use selection.select()
// directly rather than selectByID(): the latter awaits
// waitForSelect() (which would deadlock here) and no-ops when the
// row is already selected -- which it is, as part of the new
// multi-selection -- leaving the change in place and re-prompting.
this.collectionsView.selection.selectEventsSuppressed = true;
try {
await this.collectionsView.selectByID(this.itemsView.collectionTreeRow.id);
let index = this.collectionsView.getRowIndexByID(this.itemsView.collectionTreeRow.id);
if (index !== false) {
this.collectionsView.selection.select(index);
}
}
finally {
this.collectionsView.selection.selectEventsSuppressed = false;
}
return;
}
case 2:
await advancedSearchDeck.pane.cancel();
break;

View file

@ -318,6 +318,41 @@ describe("Advanced Search", function () {
await selectLibrary(win);
});
it("should revert to the edited search without re-prompting when canceling", async function () {
var search1 = await createDataObject('search', { name: "CancelEditing1" });
var search2 = await createDataObject('search', { name: "CancelEditing2" });
let cv = zp.collectionsView;
await select(win, search1);
await zp.setSavedSearchEditorState('open');
assert.equal(deck.selectedSearchType, 'saved');
// zoteroPane.js uses the pane window's Services, so stub there. Set it before
// touching the selection so no prompt can reach the real (modal) service.
let stub = sinon.stub().returns(1); // Cancel
let promptService = win.Services.prompt;
win.Services.prompt = { confirmEx: stub };
try {
// Form a [search1, search2] multi-selection without firing a selection change
cv.selection.selectEventsSuppressed = true;
cv.selection.select(cv.getRowIndexByID("S" + search1.id));
cv.selection.toggleSelect(cv.getRowIndexByID("S" + search2.id));
cv.selection.selectEventsSuppressed = false;
await zp.onCollectionSelected();
// Prompted once (no loop) and reverted to just the edited search
assert.equal(stub.callCount, 1);
assert.deepEqual(cv.getSelectedRows().map(r => r.id), ["S" + search1.id]);
}
finally {
win.Services.prompt = promptService;
}
await zp.setSavedSearchEditorState('closed');
await Zotero.Searches.erase([search1.id, search2.id]);
await selectLibrary(win);
});
it("should prompt for a name when saving a new search", async function () {
await selectLibrary(win);
await zp.toggleAdvancedSearchState('open');