Fix various saved search bugs, and add tests

Search condition ids are now indexed from 0, and always saved
contiguously (no more 'fixGaps' option), since they're just in an array
in the API. (They're still returned as an object from
Zotero.Search.prototype.getConditions() because it's easier for the
advanced search window to not have to deal with shifting ids between
saves.)
This commit is contained in:
Dan Stillman 2015-04-17 19:27:37 -04:00
parent d9c32a8e90
commit 4b040c78a7
6 changed files with 149 additions and 89 deletions

View File

@ -48,6 +48,12 @@ var ZoteroAdvancedSearch = new function() {
io.dataIn.search.loadPrimaryData() io.dataIn.search.loadPrimaryData()
.then(function () { .then(function () {
return Zotero.Groups.getAll();
})
.then(function (groups) {
// Since the search box can be used as a modal dialog, which can't use promises,
// it expects groups to be passed in.
_searchBox.groups = groups;
_searchBox.search = io.dataIn.search; _searchBox.search = io.dataIn.search;
}); });
} }

View File

@ -36,6 +36,8 @@
</resources> </resources>
<implementation> <implementation>
<property name="groups"/>
<field name="searchRef"/> <field name="searchRef"/>
<property name="search" onget="return this.searchRef;"> <property name="search" onget="return this.searchRef;">
<setter> <setter>
@ -49,28 +51,25 @@
conditionsBox.removeChild(conditionsBox.firstChild); conditionsBox.removeChild(conditionsBox.firstChild);
var conditions = this.search.getConditions(); var conditions = this.search.getConditions();
for (let id in conditions) {
for(var id in conditions) let condition = conditions[id];
{
// Checkboxes // Checkboxes
switch (conditions[id]['condition']) { switch (condition.condition) {
case 'recursive': case 'recursive':
case 'noChildren': case 'noChildren':
case 'includeParentsAndChildren': case 'includeParentsAndChildren':
var checkbox = conditions[id]['condition'] + 'Checkbox'; let checkbox = condition.condition + 'Checkbox';
this.id(checkbox).setAttribute('condition',id); this.id(checkbox).setAttribute('condition', id);
this.id(checkbox).checked = (conditions[id]['operator']=='true'); this.id(checkbox).checked = condition.operator == 'true';
continue; continue;
} }
if(conditions[id]['condition'] == 'joinMode') if(condition.condition == 'joinMode') {
{ this.id('joinModeMenu').setAttribute('condition', id);
this.id('joinModeMenu').setAttribute('condition',id); this.id('joinModeMenu').value = condition.operator;
this.id('joinModeMenu').value = conditions[id]['operator'];
} }
else else {
{ this.addCondition(condition);
this.addCondition(conditions[id]);
} }
} }
]]> ]]>
@ -96,9 +95,8 @@
menupopup.appendChild(menuitem); menupopup.appendChild(menuitem);
// Add groups // Add groups
var groups = Zotero.Groups.getAll(); for (let i = 0; i < this.groups.length; i++) {
for (let i=0; i<groups.length; i++) { let group = this.groups[i];
let group = groups[i];
let menuitem = document.createElement('menuitem'); let menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', group.name); menuitem.setAttribute('label', group.name);
menuitem.setAttribute('libraryID', group.libraryID); menuitem.setAttribute('libraryID', group.libraryID);
@ -232,7 +230,7 @@
<body> <body>
<![CDATA[ <![CDATA[
this.updateSearch(); this.updateSearch();
return this.search.save({fixGaps: true}); return this.search.save();
]]> ]]>
</body> </body>
</method> </method>

View File

@ -35,7 +35,9 @@ function doLoad()
io = window.arguments[0]; io = window.arguments[0];
document.getElementById('search-box').search = io.dataIn.search; var searchBox = document.getElementById('search-box');
searchBox.groups = io.dataIn.groups;
searchBox.search = io.dataIn.search;
document.getElementById('search-name').value = io.dataIn.name; document.getElementById('search-name').value = io.dataIn.name;
} }

View File

@ -32,8 +32,8 @@ Zotero.Search = function() {
this._scopeIncludeChildren = null; this._scopeIncludeChildren = null;
this._sql = null; this._sql = null;
this._sqlParams = false; this._sqlParams = false;
this._maxSearchConditionID = 0; this._maxSearchConditionID = -1;
this._conditions = []; this._conditions = {};
this._hasPrimaryConditions = false; this._hasPrimaryConditions = false;
} }
@ -179,7 +179,6 @@ Zotero.Search.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
}); });
Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) { Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
var fixGaps = env.options.fixGaps;
var isNew = env.isNew; var isNew = env.isNew;
var searchID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('savedSearches'); var searchID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('savedSearches');
@ -217,39 +216,28 @@ Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.DB.queryAsync(sql, this.id); yield Zotero.DB.queryAsync(sql, this.id);
} }
// Close gaps in savedSearchIDs var i = 0;
var saveConditions = {}; var sql = "INSERT INTO savedSearchConditions "
var i = 1; + "(savedSearchID, searchConditionID, condition, operator, value, required) "
for (var id in this._conditions) { + "VALUES (?,?,?,?,?,?)";
if (!fixGaps && id != i) { for (let id in this._conditions) {
Zotero.DB.rollbackTransaction(); let condition = this._conditions[id];
throw ('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' + this._id);
}
saveConditions[i] = this._conditions[id];
i++;
}
this._conditions = saveConditions;
for (var i in this._conditions){
var sql = "INSERT INTO savedSearchConditions (savedSearchID, "
+ "searchConditionID, condition, operator, value, required) "
+ "VALUES (?,?,?,?,?,?)";
// Convert condition and mode to "condition[/mode]" // Convert condition and mode to "condition[/mode]"
var condition = this._conditions[i].mode ? let conditionString = condition.mode ?
this._conditions[i].condition + '/' + this._conditions[i].mode : condition.condition + '/' + condition.mode :
this._conditions[i].condition condition.condition
var sqlParams = [ var sqlParams = [
searchID, searchID,
i, i,
condition, conditionString,
this._conditions[i].operator ? this._conditions[i].operator : null, condition.operator ? condition.operator : null,
this._conditions[i].value ? this._conditions[i].value : null, condition.value ? condition.value : null,
this._conditions[i].required ? 1 : null condition.required ? 1 : null
]; ];
yield Zotero.DB.queryAsync(sql, sqlParams); yield Zotero.DB.queryAsync(sql, sqlParams);
i++;
} }
}); });
@ -274,6 +262,7 @@ Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env)
return id; return id;
} }
yield this.loadPrimaryData(true);
yield this.reload(); yield this.reload();
this._clearChanged(); this._clearChanged();
@ -400,11 +389,13 @@ Zotero.Search.prototype.addCondition = function (condition, operator, value, req
mode: mode, mode: mode,
operator: operator, operator: operator,
value: value, value: value,
required: required required: !!required
}; };
this._sql = null; this._sql = null;
this._sqlParams = false; this._sqlParams = false;
this._markFieldChange('conditions', this._conditions);
this._changed.conditions = true;
return searchConditionID; return searchConditionID;
} }
@ -426,8 +417,8 @@ Zotero.Search.prototype.setScope = function (searchObj, includeChildren) {
* @param {Boolean} [required] * @param {Boolean} [required]
* @return {Promise} * @return {Promise}
*/ */
Zotero.Search.prototype.updateCondition = Zotero.Promise.coroutine(function* (searchConditionID, condition, operator, value, required){ Zotero.Search.prototype.updateCondition = function (searchConditionID, condition, operator, value, required) {
yield this.loadPrimaryData(); this._requireData('conditions');
if (typeof this._conditions[searchConditionID] == 'undefined'){ if (typeof this._conditions[searchConditionID] == 'undefined'){
throw new Error('Invalid searchConditionID ' + searchConditionID); throw new Error('Invalid searchConditionID ' + searchConditionID);
@ -447,23 +438,27 @@ Zotero.Search.prototype.updateCondition = Zotero.Promise.coroutine(function* (se
mode: mode, mode: mode,
operator: operator, operator: operator,
value: value, value: value,
required: required required: !!required
}; };
this._sql = null; this._sql = null;
this._sqlParams = false; this._sqlParams = false;
}); this._markFieldChange('conditions', this._conditions);
this._changed.conditions = true;
}
Zotero.Search.prototype.removeCondition = Zotero.Promise.coroutine(function* (searchConditionID){ Zotero.Search.prototype.removeCondition = function (searchConditionID) {
yield this.loadPrimaryData(); this._requireData('conditions');
if (typeof this._conditions[searchConditionID] == 'undefined'){ if (typeof this._conditions[searchConditionID] == 'undefined'){
throw ('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()'); throw ('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()');
} }
delete this._conditions[searchConditionID]; delete this._conditions[searchConditionID];
}); this._markFieldChange('conditions', this._conditions);
this._changed.conditions = true;
}
/* /*
@ -896,38 +891,37 @@ Zotero.Search.prototype.loadConditions = Zotero.Promise.coroutine(function* (rel
this._maxSearchConditionID = conditions[conditions.length - 1].searchConditionID; this._maxSearchConditionID = conditions[conditions.length - 1].searchConditionID;
} }
// Reindex conditions, in case they're not contiguous in the DB this._conditions = {};
var conditionID = 1;
// Reindex conditions, in case they're not contiguous in the DB
for (let i=0; i<conditions.length; i++) { for (let i=0; i<conditions.length; i++) {
// Parse "condition[/mode]" let condition = conditions[i];
var [condition, mode] =
Zotero.SearchConditions.parseCondition(conditions[i]['condition']);
var cond = Zotero.SearchConditions.get(condition); // Parse "condition[/mode]"
let [conditionName, mode] = Zotero.SearchConditions.parseCondition(condition.condition);
let cond = Zotero.SearchConditions.get(conditionName);
if (!cond || cond.noLoad) { if (!cond || cond.noLoad) {
Zotero.debug("Invalid saved search condition '" + condition + "' -- skipping", 2); Zotero.debug("Invalid saved search condition '" + conditionName + "' -- skipping", 2);
continue; continue;
} }
// Convert itemTypeID to itemType // Convert itemTypeID to itemType
// //
// TEMP: This can be removed at some point // TEMP: This can be removed at some point
if (condition == 'itemTypeID') { if (conditionName == 'itemTypeID') {
condition = 'itemType'; conditionName = 'itemType';
conditions[i].value = Zotero.ItemTypes.getName(conditions[i].value); condition.value = Zotero.ItemTypes.getName(condition.value);
} }
this._conditions[conditionID] = { this._conditions[i] = {
id: conditionID, id: i,
condition: condition, condition: conditionName,
mode: mode, mode: mode,
operator: conditions[i].operator, operator: condition.operator,
value: conditions[i].value, value: condition.value,
required: conditions[i].required required: !!condition.required
}; };
conditionID++;
} }
this._loaded.conditions = true; this._loaded.conditions = true;

View File

@ -1809,7 +1809,7 @@ var ZoteroPane = new function()
}); });
this.editSelectedCollection = function () { this.editSelectedCollection = Zotero.Promise.coroutine(function* () {
if (!this.canEdit()) { if (!this.canEdit()) {
this.displayCannotEditLibraryMessage(); this.displayCannotEditLibraryMessage();
return; return;
@ -1832,22 +1832,32 @@ var ZoteroPane = new function()
} }
} }
else { else {
var s = new Zotero.Search(); let s = new Zotero.Search();
s.id = row.ref.id; s.id = row.ref.id;
s.loadPrimaryData() yield s.loadPrimaryData();
.then(function () { yield s.loadConditions();
return s.loadConditions(); let groups = [];
}) // Promises don't work in the modal dialog, so get the group name here, if
.then(function () { // applicable, and pass it in. We only need the group that this search belongs
var io = {dataIn: {search: s, name: row.getName()}, dataOut: null}; // to, if any, since the library drop-down is disabled for saved searches.
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io); if (Zotero.Libraries.getType(s.libraryID) == 'group') {
if (io.dataOut) { groups.push(yield Zotero.Groups.getByLibraryID(s.libraryID));
this.onCollectionSelected(); //reload itemsView }
} var io = {
}.bind(this)); dataIn: {
search: s,
name: row.getName(),
groups: groups
},
dataOut: null
};
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
if (io.dataOut) {
this.onCollectionSelected(); //reload itemsView
}
} }
} }
} });
this.copySelectedItemsToClipboard = function (asCitations) { this.copySelectedItemsToClipboard = function (asCitations) {

View File

@ -37,11 +37,61 @@ describe("Zotero.Search", function() {
yield s.loadConditions(); yield s.loadConditions();
var conditions = s.getConditions(); var conditions = s.getConditions();
assert.lengthOf(Object.keys(conditions), 1); assert.lengthOf(Object.keys(conditions), 1);
assert.property(conditions, "1"); // searchConditionIDs start at 1 assert.property(conditions, "0");
var condition = conditions[1]; var condition = conditions[0];
assert.propertyVal(condition, 'condition', 'title') assert.propertyVal(condition, 'condition', 'title')
assert.propertyVal(condition, 'operator', 'is') assert.propertyVal(condition, 'operator', 'is')
assert.propertyVal(condition, 'value', 'test') assert.propertyVal(condition, 'value', 'test')
assert.propertyVal(condition, 'required', false)
});
it("should add a condition to an existing search", function* () {
// Save search
var s = new Zotero.Search;
s.libraryID = Zotero.Libraries.userLibraryID;
s.name = "Test";
s.addCondition('title', 'is', 'test');
var id = yield s.save();
assert.typeOf(id, 'number');
// Add condition
s = yield Zotero.Searches.getAsync(id);
yield s.loadConditions();
s.addCondition('title', 'contains', 'foo');
var saved = yield s.save();
assert.isTrue(saved);
// Check saved search
s = yield Zotero.Searches.getAsync(id);
yield s.loadConditions();
var conditions = s.getConditions();
assert.lengthOf(Object.keys(conditions), 2);
});
it("should remove a condition from an existing search", function* () {
// Save search
var s = new Zotero.Search;
s.libraryID = Zotero.Libraries.userLibraryID;
s.name = "Test";
s.addCondition('title', 'is', 'test');
s.addCondition('title', 'contains', 'foo');
var id = yield s.save();
assert.typeOf(id, 'number');
// Remove condition
s = yield Zotero.Searches.getAsync(id);
yield s.loadConditions();
s.removeCondition(0);
var saved = yield s.save();
assert.isTrue(saved);
// Check saved search
s = yield Zotero.Searches.getAsync(id);
yield s.loadConditions();
var conditions = s.getConditions();
assert.lengthOf(Object.keys(conditions), 1);
assert.property(conditions, "0");
assert.propertyVal(conditions[0], 'value', 'foo')
}); });
}); });
}); });