Don't drop an annotationAuthor value while its author menu is loading

onConditionSelected() populates the Author value menu asynchronously,
and until it finished, the row's default textbox was still the visible
value control, so serializing the row (running or saving the search)
replaced the loaded author with an empty value. Serialize the stored
value while the menu is pending, and invalidate the population if a
newer condition selection takes over meanwhile.
This commit is contained in:
Dan Stillman 2026-07-05 17:06:51 -04:00
parent ccc7d0a5ad
commit 4188ef0ebc
2 changed files with 48 additions and 0 deletions

View file

@ -1125,6 +1125,11 @@
this.selectedCondition = conditionName;
this.selectedOperator = operatorsList.value;
// Invalidate any in-flight async value-menu population (annotationAuthor) from a
// previous selection, and clear the pending flag for synchronous conditions
let valueMenuToken = this._valueMenuToken = {};
this._valueMenuPending = false;
var condition = Zotero.SearchConditions.get(conditionName);
var operators = condition.operators;
@ -1255,11 +1260,19 @@
}
case 'annotationAuthor':
{
// The author list loads asynchronously; until the menu is populated, the
// row serializes its stored value (see getConditionData())
this._valueMenuPending = true;
let authors = await Zotero.Annotations.getAllAuthors(this.parent.search.libraryID);
// A newer selection took over while the list was loading
if (valueMenuToken !== this._valueMenuToken) {
return;
}
let collation = Zotero.getLocaleCollation();
let rows = authors.map(a => ({ name: a.name, value: a.userID }));
rows.sort((a, b) => collation.compareString(1, a.name, b.name));
this.createValueMenu(rows);
this._valueMenuPending = false;
break;
}
default:
@ -1431,6 +1444,11 @@
// The live value from whichever value control is currently shown, in the same prefixed
// form this.value is stored in (so it can be carried across a condition change)
getCurrentValue() {
// While a value menu is still being populated asynchronously, the old control is
// still the visible one, so use the stored value (see getConditionData())
if (this._valueMenuPending) {
return this.value;
}
let valueField = this.querySelector('#valuefield');
if (!valueField.hidden) {
return valueField.value;
@ -1454,6 +1472,12 @@
var operator = this.querySelector('#operatorsmenu').value;
let value;
// A value menu still being populated asynchronously (annotationAuthor) hasn't been
// swapped in yet, so serialize the stored value rather than reading the wrong control
if (this._valueMenuPending) {
return { condition, operator, value: this.value || '' };
}
// Regular text field
if (!this.querySelector('#valuefield').hidden) {
value = this.querySelector('#valuefield').value;

View file

@ -811,6 +811,30 @@ describe("Advanced Search", function () {
assert.equal(win.getComputedStyle(valuefield).display, 'none');
assert.isFalse(row.querySelector('#valuemenu').hidden);
});
it("should keep a loaded annotationAuthor value while its author menu is populating", async function () {
var deferred = Zotero.Promise.defer();
var stub = sinon.stub(Zotero.Annotations, 'getAllAuthors').returns(deferred.promise);
try {
var s = new Zotero.Search();
s.libraryID = Zotero.Libraries.userLibraryID;
s.addCondition('annotationAuthor', 'is', '12345');
pane.search = s;
// Serializing while the author list is still loading shouldn't drop the value
var row = conditions.firstChild;
assert.equal(row.getConditionData().value, '12345');
deferred.resolve([{ name: "Some User", userID: 12345 }]);
// Let onConditionSelected() finish populating the menu
await Zotero.Promise.delay(0);
assert.isFalse(row.querySelector('#valuemenu').hidden);
assert.equal(row.getConditionData().value, '12345');
}
finally {
stub.restore();
}
});
it("should place attachment and annotation conditions in their submenus", function () {
var s = new Zotero.Search();