Move Advanced Search and saved search editing to the main window (#5658)

---------

Co-authored-by: Dan Stillman <dstillman@zotero.org>
This commit is contained in:
Abe Jellinek 2025-12-12 10:35:03 -05:00 committed by Dan Stillman
parent d2f1c56250
commit 00527332c4
31 changed files with 1016 additions and 718 deletions

View file

@ -1,194 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
import CollectionViewItemTree from 'zotero/collectionViewItemTree';
import { COLUMNS } from 'zotero/itemTreeColumns';
var ZoteroAdvancedSearch = new function () {
this.onLoad = onLoad;
this.search = search;
this.clear = clear;
this.onItemActivate = onItemActivate;
this.itemsView = false;
this._loadedDeferred = Zotero.Promise.defer();
var _searchBox;
var _libraryID;
var _searchCounter = 0;
async function onLoad() {
_searchBox = document.getElementById('zotero-search-box');
// Set font size from pref
var sbc = document.getElementById('zotero-search-box-container');
Zotero.UIProperties.registerRoot(sbc);
_searchBox.onLibraryChange = this.onLibraryChange;
var io = window.arguments[0];
io.dataIn.search.loadPrimaryData()
.then(function () {
_searchBox.search = io.dataIn.search;
});
var elem = document.getElementById('zotero-items-tree');
const columns = COLUMNS.map((column) => {
column = Object.assign({}, column);
column.hidden = !['title', 'firstCreator', 'year', 'hasAttachment'].includes(column.dataKey);
return column;
});
this.itemsView = await CollectionViewItemTree.init(elem, {
id: "advanced-search",
dragAndDrop: true,
columnPicker: true,
onActivate: this.onItemActivate.bind(this),
columns,
});
await this.itemsView.changeCollectionTreeRow({
id: 'advanced-search-' + _searchCounter++,
ref: _searchBox.search,
visibilityGroup: 'default',
isSearchMode: () => true,
getItems: async () => [],
isLibrary: () => false,
isCollection: () => false,
isSearch: () => true,
isPublications: () => false,
isDuplicates: () => false,
isFeed: () => false,
isFeeds: () => false,
isFeedsOrFeed: () => false,
isRecentlyRead: () => false,
isSortable: () => true,
isShare: () => false,
isTrash: () => false,
isSearch: () => true
});
// Focus the first field in the window
Services.focus.moveFocus(window, null, Services.focus.MOVEFOCUS_FORWARD, 0);
this._loadedDeferred.resolve();
}
this.onUnload = function () {
this.itemsView.unregister();
}
function search() {
_searchBox.updateSearch();
_searchBox.active = true;
return this.itemsView.changeCollectionTreeRow({
id: 'advanced-search-' + _searchCounter++,
ref: _searchBox.search,
visibilityGroup: 'default',
isSearchMode: () => true,
isSearch: () => true,
getItems: async function () {
await Zotero.Libraries.get(_libraryID).waitForDataLoad('item');
var search = _searchBox.search.clone();
search.libraryID = _libraryID;
var ids = await search.search();
return Zotero.Items.get(ids);
}
});
}
function clear() {
this.itemsView.changeCollectionTreeRow(null);
var s = new Zotero.Search();
// Don't clear the selected library
s.libraryID = _searchBox.search.libraryID;
s.addCondition('title', 'contains', '');
_searchBox.search = s;
_searchBox.active = false;
}
this.save = async function () {
_searchBox.updateSearch();
var promptService = Services.prompt;
var libraryID = _searchBox.search.libraryID;
var searches = await Zotero.Searches.getAll(libraryID);
var prefix = Zotero.getString('pane.collections.untitled');
var name = Zotero.Utilities.Internal.getNextName(
prefix,
searches.map(s => s.name).filter(n => n.startsWith(prefix))
);
name = { value: name };
var result = promptService.prompt(window,
Zotero.getString('pane.collections.newSavedSeach'),
Zotero.getString('pane.collections.savedSearchName'), name, "", {});
if (!result) {
return;
}
if (!name.value) {
name.value = 'untitled';
}
var s = _searchBox.search.clone();
s.name = name.value;
await s.saveTx();
window.close();
};
this.onLibraryChange = function (libraryID) {
_libraryID = libraryID;
var library = Zotero.Libraries.get(libraryID);
var isEditable = library.editable && library.libraryType != 'publications';
document.getElementById('zotero-search-save').disabled = !isEditable;
}
function onItemActivate(event, items)
{
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
if (!lastWin) {
return;
}
lastWin.ZoteroPane.selectItems(items.map(item => item.id));
lastWin.focus();
}
}

View file

@ -1,58 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css"?>
<!DOCTYPE window [
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%zoteroDTD;
<!ENTITY % searchboxDTD SYSTEM "chrome://zotero/locale/searchbox.dtd">
%searchboxDTD;
]>
<window
id="zotero-advanced-search-dialog"
title="&zotero.toolbar.advancedSearch;"
orient="vertical"
persist="screenX screenY width height"
onload="ZoteroAdvancedSearch.onLoad()"
onunload="ZoteroAdvancedSearch.onUnload();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="zotero:search"
style="display: flex;">
<script src="include.js"/>
<script>
</script>
<script src="customElements.js"/>
<script src="advancedSearch.js"/>
<popupset>
<panel is="autocomplete-richlistbox-popup"
id="search-autocomplete-popup"
type="autocomplete-richlistbox"
noautofocus="true"/>
</popupset>
<vbox id="zotero-search-box-container" flex="1">
<vbox id="zotero-search-box-controls">
<zoterosearch id="zotero-search-box" oncommand="if (this.active) { ZoteroAdvancedSearch.search(); }" flex="1"/>
<hbox id="zotero-search-buttons">
<button label="&zotero.search.search;" default="true" oncommand="ZoteroAdvancedSearch.search()"/>
<button label="&zotero.search.clear;" oncommand="ZoteroAdvancedSearch.clear()"/>
<button id="zotero-search-save" label="&zotero.search.saveSearch;" oncommand="ZoteroAdvancedSearch.save()"/>
</hbox>
</vbox>
<hbox class="virtualized-table-container" flex="1">
<html:div id="zotero-items-tree"/>
</hbox>
</vbox>
<keyset>
<key id="key_close" key="W" modifiers="accel" oncommand="window.close()"/>
</keyset>
</window>

View file

@ -152,6 +152,9 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider {
case 'citation-search':
changed = this.collectionTreeRow.setSearch(data, 'fields');
break;
case 'advanced-search':
changed = this.collectionTreeRow.setAdvancedSearch(data);
break;
case 'tags':
changed = this.collectionTreeRow.setTags(data);
break;

View file

@ -33,6 +33,8 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemTreeMe
{
// https://searchfox.org/mozilla-central/rev/8e885f04a0a4ff6d64ea59741c10d9b8e45d9ff8/toolkit/content/customElements.js#826-832
for (let [tag, script] of [
['advanced-search-deck', 'chrome://zotero/content/elements/advancedSearchDeck.js'],
['advanced-search-pane', 'chrome://zotero/content/elements/advancedSearchPane.js'],
['attachment-box', 'chrome://zotero/content/elements/attachmentBox.js'],
['attachment-preview', 'chrome://zotero/content/elements/attachmentPreview.js'],
['attachment-preview-box', 'chrome://zotero/content/elements/attachmentPreviewBox.js'],

View file

@ -0,0 +1,83 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2025 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
{
class AdvancedSearchDeck extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<deck>
<advanced-search-pane id="zotero-temporary-advanced-search-pane" type="temporary"/>
<advanced-search-pane id="zotero-saved-advanced-search-pane" type="saved"/>
</deck>
`);
init() {
this.hidden = true;
this._state = 'closed';
this.deck = this.firstChild;
}
get state() {
return this._state;
}
set state(state) {
switch (state) {
case 'open':
this.hidden = false;
break;
case 'collapsed':
case 'closed':
this.hidden = true;
break;
default:
throw new Error('Invalid state: ' + state);
}
this._state = state;
}
get selectedSearchType() {
return this.deck.selectedIndex === 0 ? 'temporary' : 'saved';
}
set selectedSearchType(selectedSearchType) {
switch (selectedSearchType) {
case 'temporary':
this.deck.selectedIndex = 0;
break;
case 'saved':
this.deck.selectedIndex = 1;
break;
}
}
get pane() {
return this.deck.selectedPanel;
}
}
customElements.define("advanced-search-deck", AdvancedSearchDeck);
}

View file

@ -0,0 +1,209 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2025 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
{
class AdvancedSearchPane extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<hbox class="saved-search-name-row">
<label control="saved-search-name" data-l10n-id="new-collection-name"/>
<html:input type="text" id="saved-search-name"/>
</hbox>
<zoterosearch/>
<hbox class="advanced-search-buttons">
<button class="cancel-button" data-l10n-id="cancel-button"/>
<button class="search-button" data-l10n-id="search-button" default="true"/>
<button class="clear-button" data-l10n-id="clear-button"/>
<button class="save-button" data-l10n-id="save-search-button"/>
</hbox>
`);
_active = false;
init() {
this._nameField = this.querySelector('#saved-search-name');
this._searchElem = this.querySelector('zoterosearch');
this._cancelButton = this.querySelector('.cancel-button');
this._searchButton = this.querySelector('.search-button');
this._clearButton = this.querySelector('.clear-button');
this._saveButton = this.querySelector('.save-button');
this._searchElem.addEventListener('input', () => this._searchElem.updateSearch());
this._searchElem.addEventListener('command', () => this._searchElem.updateSearch());
this._cancelButton.addEventListener('command', () => this.cancel());
this._searchButton.addEventListener('command', () => this.submit());
this._clearButton.addEventListener('command', () => this.clear());
this._saveButton.addEventListener('command', () => this.save());
if (!['temporary', 'saved'].includes(this.type)) {
throw new Error(`Invalid type: ${this.type}`);
}
if (this.type === 'saved') {
this._saveButton.setAttribute('default', 'true');
}
this.addEventListener('keydown', this._handleKeyDown);
}
_handleKeyDown = async (event) => {
this._searchElem.updateSearch();
if (event.key === 'Enter') {
if (this.type === 'temporary') {
await this.submit();
}
else {
await this.save();
}
}
};
/**
* @returns {'temporary' | 'saved'}
*/
get type() {
return this.getAttribute('type');
}
get search() {
return this._search;
}
/**
* Whether the search has been submitted and should filter the items list
*/
get active() {
return this._active;
}
set search(search) {
this._active = false;
if (this.type === 'saved') {
if (!search?.id) {
throw new Error('Cannot edit unsaved search');
}
this._searchID = search.id;
this._search = search.clone();
this._nameField.value = search.name;
}
else if (search) {
this._search = search.clone();
}
else {
this._search = new Zotero.Search();
this._search.addCondition('title', 'contains', '');
}
this._searchElem.search = this._search;
}
_ensureSearch() {
if (!this._search && this.type === 'temporary') {
this.search = null;
}
}
refresh() {
this._ensureSearch();
let libraryID = ZoteroPane.getSelectedLibraryID();
// Keep the previous library when the selected row doesn't have one (e.g., Feeds)
if (libraryID) {
this._search.libraryID = libraryID;
}
this._searchElem.search = this._search;
// There would be no way to scope a saved search to anything but the library root,
// so disable the option to save.
// Somewhat unfortunate - revisit when/if we support nested condition sets in the UI.
this._saveButton.disabled = this.type === 'temporary' && !ZoteroPane.getCollectionTreeRow().isLibrary();
}
async cancel() {
await ZoteroPane.setSavedSearchEditorState('closed');
}
async submit() {
if (this.type === 'saved') {
throw new Error('submit() is unsupported for saved search');
}
this._searchElem.updateSearch();
this._active = true;
await ZoteroPane.itemsView.setFilter('advanced-search', this._search);
ZoteroPane.itemsView.focus();
}
async clear() {
if (this.type === 'saved') {
throw new Error('clear() is unsupported for saved search');
}
this.search = null;
await ZoteroPane.itemsView.setFilter('advanced-search', null);
}
async save() {
this._searchElem.updateSearch();
if (this.type === 'saved') {
let search = Zotero.Searches.get(this._searchID);
if (!search) {
throw new Error('Missing search');
}
search.fromJSON(this._search.toJSON());
search.name = this._nameField.value;
await search.saveTx();
await ZoteroPane.setSavedSearchEditorState('closed');
Zotero_Tabs.rename('zotero-pane', search.name);
return;
}
let collectionTreeRow = ZoteroPane.getCollectionTreeRow();
if (!collectionTreeRow.isLibrary()) {
throw new Error('Can only save in library root');
}
this._ensureSearch();
let libraryID = collectionTreeRow.ref.libraryID;
let searches = await Zotero.Searches.getAll(libraryID);
let prefix = Zotero.getString('pane.collections.untitled');
let name = Zotero.Utilities.Internal.getNextName(
prefix,
searches.map(s => s.name).filter(n => n.startsWith(prefix))
);
let search = this._search.clone(libraryID);
search.name = name;
await search.saveTx();
await ZoteroPane.setAdvancedSearchState('closed');
}
focus(options) {
this._searchElem.querySelector('#conditionsmenu').focus(options);
}
}
customElements.define("advanced-search-pane", AdvancedSearchPane);
}

View file

@ -33,8 +33,15 @@
this.searchTextbox = null;
MozXULElement.insertFTLIfNeeded("zotero.ftl");
this.content = MozXULElement.parseXULToFragment(`
<hbox id="search-wrapper" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
</hbox>
<deck id="search-deck">
<hbox id="search-wrapper">
</hbox>
<hbox id="advanced-search-indicator">
<label id="advanced-search-label"/>
<toolbarbutton class="zotero-clicky advanced-collapse-button" tabindex="0"/>
<toolbarbutton class="zotero-clicky advanced-close-button" tabindex="0"/>
</hbox>
</deck>
`, ['chrome://zotero/locale/zotero.dtd']);
}
@ -95,6 +102,30 @@
wrapper.appendChild(dropmarkerHost);
wrapper.appendChild(searchBox);
// Add Advanced Search button at the end of the field in main window
if (document.documentElement.getAttribute('windowtype') === 'navigator:browser') {
let advancedButton = document.createXULElement('toolbarbutton');
advancedButton.id = 'zotero-tb-search-advanced-button';
advancedButton.tabIndex = 0;
document.l10n.setAttributes(advancedButton, 'quicksearch-advanced-search-button');
advancedButton.addEventListener('command', (event) => {
// Don't trigger a quick search via the oncommand handler
event.stopPropagation();
ZoteroPane.toggleAdvancedSearchState('open');
});
wrapper.appendChild(advancedButton);
}
this.deck = this.firstElementChild;
this.querySelector('.advanced-collapse-button').addEventListener('command', (event) => {
event.stopPropagation();
ZoteroPane.toggleAdvancedSearchState('collapsed');
});
this.querySelector('.advanced-close-button').addEventListener('command', (event) => {
event.stopPropagation();
ZoteroPane.toggleAdvancedSearchState('closed');
});
// If Alt-Up/Down, show popup
this.addEventListener('keypress', (event) => {
@ -132,20 +163,13 @@
popup.append(item);
}
// Add Advanced Search menu item in main window
if (document.documentElement.getAttribute('windowtype') === 'navigator:browser') {
let separator = document.createXULElement('menuseparator');
popup.append(separator);
let advancedSearchOption = document.createXULElement('menuitem');
advancedSearchOption.label = Zotero.getString("zotero.toolbar.advancedSearch");
advancedSearchOption.addEventListener("command", () => {
ZoteroPane.openAdvancedSearchWindow();
});
popup.append(advancedSearchOption);
}
return this._searchModePopup = popup;
}
onCollectionSelected() {
this.searchTextbox.value = '';
this.updateMode();
}
updateMode() {
let mode = Zotero.Prefs.get("search.quicksearch-mode");
@ -158,6 +182,29 @@
this.searchModePopup.querySelector(`menuitem[value="${mode}"]`)
.setAttribute('checked', 'true');
document.l10n.setAttributes(this.searchTextbox.inputField, "quicksearch-input", { placeholder: this._searchModes[mode] });
let advancedSearchDeck = document.getElementById('zotero-advanced-search-pane-deck');
if (advancedSearchDeck) {
let state = advancedSearchDeck.state;
let selectedSearchType = advancedSearchDeck.selectedSearchType;
document.l10n.setAttributes(
this.querySelector('#advanced-search-label'),
selectedSearchType === 'temporary' ? 'advanced-search' : 'edit-saved-search',
);
this.deck.selectedIndex = state === 'closed' ? 0 : 1;
this.querySelector('#advanced-search-indicator').dataset.collapsed = state === 'collapsed';
this.querySelector('.advanced-collapse-button').hidden = selectedSearchType === 'saved';
}
}
focus(options) {
if (this.deck.selectedIndex === 0) {
this._searchModePopup.flattenedTreeParentNode.focus(options);
}
else {
Services.focus.moveFocus(window, this.deck.selectedPanel, Services.focus.MOVEFOCUS_FORWARD, 0);
}
}
_id(id) {

View file

@ -37,35 +37,24 @@
class ZoteroSearch extends SearchElementBase {
content = MozXULElement.parseXULToFragment(`
<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
id="search-box" flex="1" onkeypress="this.closest('zoterosearch').handleKeyPress(event)">
<hbox align="center">
<label id="libraryMenu-label" value="&zotero.search.searchInLibrary;" control="libraryMenu"/>
<menulist id="libraryMenu" aria-labelledby="libraryMenu-label" oncommand="this.closest('zoterosearch').updateLibrary();" native="true">
<menupopup/>
<groupbox>
<caption align="center">
<label id="joinModeMenu-label" value="&zotero.search.joinMode.prefix;"/>
<menulist id="joinModeMenu" aria-labelledby="joinModeMenu-label" oncommand="this.closest('zoterosearch').updateJoinMode();" native="true">
<menupopup>
<menuitem label="&zotero.search.joinMode.any;" value="any"/>
<menuitem label="&zotero.search.joinMode.all;" value="all" selected="true"/>
</menupopup>
</menulist>
</hbox>
<groupbox>
<caption align="center">
<label id="joinModeMenu-label" value="&zotero.search.joinMode.prefix;"/>
<menulist id="joinModeMenu" aria-labelledby="joinModeMenu-label" oncommand="this.closest('zoterosearch').updateJoinMode();" native="true">
<menupopup>
<menuitem label="&zotero.search.joinMode.any;" value="any"/>
<menuitem label="&zotero.search.joinMode.all;" value="all" selected="true"/>
</menupopup>
</menulist>
<label value="&zotero.search.joinMode.suffix;"/>
</caption>
<vbox id="conditions"/>
</groupbox>
<hbox>
<checkbox id="recursiveCheckbox" label="&zotero.search.recursive.label;" oncommand="this.closest('zoterosearch').updateCheckbox('recursive');" native="true"/>
<checkbox id="noChildrenCheckbox" label="&zotero.search.noChildren;" oncommand="this.closest('zoterosearch').updateCheckbox('noChildren');" native="true"/>
</hbox>
<hbox>
<checkbox id="includeParentsAndChildrenCheckbox" label="&zotero.search.includeParentsAndChildren;" oncommand="this.closest('zoterosearch').updateCheckbox('includeParentsAndChildren');" native="true"/>
</hbox>
</vbox>
<label value="&zotero.search.joinMode.suffix;"/>
</caption>
<vbox id="conditions"/>
</groupbox>
<hbox id="search-option-checkboxes">
<checkbox id="recursiveCheckbox" label="&zotero.search.recursive.label;" oncommand="this.closest('zoterosearch').updateCheckbox('recursive');" native="true"/>
<checkbox id="noChildrenCheckbox" label="&zotero.search.noChildren;" oncommand="this.closest('zoterosearch').updateCheckbox('noChildren');" native="true"/>
<checkbox id="includeParentsAndChildrenCheckbox" label="&zotero.search.includeParentsAndChildren;" oncommand="this.closest('zoterosearch').updateCheckbox('includeParentsAndChildren');" native="true"/>
</hbox>
`, ['chrome://zotero/locale/zotero.dtd', 'chrome://zotero/locale/searchbox.dtd']);
get search() {
@ -75,18 +64,12 @@
set search(val) {
this.searchRef = val;
var libraryMenu = this.querySelector('#libraryMenu');
var libraries = Zotero.Libraries.getAll();
Zotero.Utilities.Internal.buildLibraryMenu(
libraryMenu, libraries, this.searchRef.libraryID
);
if (this.searchRef.id) {
libraryMenu.disabled = true;
}
this.updateLibrary();
this.querySelector('#joinModeMenu').removeAttribute('condition');
this.querySelector('#joinModeMenu').value = 'all';
this.querySelector('#recursiveCheckbox').checked = false;
this.querySelector('#noChildrenCheckbox').checked = false;
this.querySelector('#includeParentsAndChildrenCheckbox').checked = false;
var conditionsBox = this.querySelector('#conditions');
while (conditionsBox.hasChildNodes()) {
@ -118,6 +101,10 @@
}
}
}
init() {
this.addEventListener('keypress', event => this.handleKeyPress(event));
}
addCondition(ref) {
var conditionsBox = this.querySelector('#conditions');
@ -167,20 +154,6 @@
}
}
updateLibrary() {
var menu = this.querySelector('#libraryMenu');
var libraryID = parseInt(menu.selectedItem.value);
if (this.onLibraryChange) {
this.onLibraryChange(libraryID);
}
if (!this.searchRef.id) {
this.searchRef.libraryID = libraryID;
}
[...this.querySelector('#conditions').childNodes].forEach(x => x.onLibraryChange());
}
updateJoinMode() {
var menu = this.querySelector('#joinModeMenu');
if (menu.hasAttribute('condition')) this.search.updateCondition(menu.getAttribute('condition'), 'joinMode', menu.value, null);
@ -586,6 +559,11 @@
if (this.value) {
valueMenu.value = this.value;
// If the value isn't in the menu (e.g., a collection from another
// library after a library change), fall back to the first item
if (!valueMenu.selectedItem) {
valueMenu.selectedIndex = 0;
}
}
}
@ -747,8 +725,6 @@
onRemoveClicked() {
if (this.parent) {
window.resizeBy(0, -1 * this.getBoundingClientRect().height);
window.dispatchEvent(new CustomEvent('resize'));
this.parent.removeCondition(this.conditionID);
}
}
@ -764,7 +740,6 @@
)
);
this.parent.addCondition(ref);
window.resizeBy(0, this.getBoundingClientRect().height);
}
}

View file

@ -214,15 +214,15 @@ var LibraryTree = class LibraryTree extends React.Component {
this.tree && this.tree.scrollToRow(index);
}
_updateHeight = () => {
updateHeight = () => {
this.forceUpdate(() => {
if (this.tree) {
this.tree.rerender();
}
});
}
};
updateHeight = Zotero.Utilities.debounce(this._updateHeight, 200);
updateHeightDebounced = Zotero.Utilities.debounce(this.updateHeight, 200);
updateFontSize() {
this.tree.updateFontSize();

View file

@ -1,70 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
var itemsView;
var collectionsView;
var io;
function doLoad()
{
// Set font size from pref
var sbc = document.getElementById('zotero-search-box-container');
Zotero.UIProperties.registerRoot(sbc);
io = window.arguments[0];
var searchBox = document.getElementById('search-box');
searchBox.groups = io.dataIn.groups;
searchBox.search = io.dataIn.search;
let searchName = document.getElementById('search-name');
searchName.value = io.dataIn.name;
searchName.select();
document.addEventListener('dialogaccept', doAccept);
}
function doUnload()
{
}
function doAccept()
{
document.getElementById('search-box').search.name = document.getElementById('search-name').value;
try {
let searchBox = document.getElementById('search-box');
searchBox.updateSearch();
io.dataOut = {
json: searchBox.search.toJSON()
};
}
catch (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
throw (e);
}
}

View file

@ -1,43 +0,0 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css" type="text/css"?>
<!DOCTYPE bindings SYSTEM "chrome://zotero/locale/searchbox.dtd">
<window
title="&zotero.search.search;"
orient="vertical"
onload="doLoad();"
onunload="doUnload();"
drawintitlebar-platforms="mac"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<dialog
id="zotero-search-dialog"
buttons="cancel,accept">
<script>
Services.scriptloader.loadSubScript("chrome://zotero/content/include.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/titlebar.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/customElements.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/searchDialog.js", this);
</script>
<popupset>
<panel is="autocomplete-richlistbox-popup"
id="search-autocomplete-popup"
type="autocomplete-richlistbox"
noautofocus="true"/>
</popupset>
<vbox id="zotero-search-box-container" flex="1">
<hbox align="center">
<label control="search-name" value="&zotero.search.name;"/>
<html:input id="search-name" type="text" width="275" maxlength="80"/>
</hbox>
<zoterosearch id="search-box" flex="1"/>
</vbox>
</dialog>
</window>

View file

@ -464,7 +464,8 @@ Zotero.CollectionTreeRow.prototype.getSearchObject = async function () {
}
s2.setScope(s, includeScopeChildren);
if (this.searchText) {
// Add Quick Search unless advanced search is enabled
if (this.searchText && !this.advancedSearch) {
let cond = 'quicksearch-'
+ (this.searchMode || Zotero.Prefs.get('search.quicksearch-mode'));
s2.addCondition(cond, 'contains', this.searchText);
@ -476,8 +477,37 @@ Zotero.CollectionTreeRow.prototype.getSearchObject = async function () {
}
}
this._cachedSearch = s2;
return s2;
let s3;
if (this.advancedSearch) {
if (this.advancedSearch.libraryID === null) {
// A library-less search (Feeds pseudo-library) can't be clone()d
s3 = new Zotero.Search();
s3.fromJSON(this.advancedSearch.toJSON());
}
else {
s3 = this.advancedSearch.clone();
}
// Show matches in the trash too. includeDeleted has to be set on the
// scope searches as well, since a search's scope defines the superset
// of possible results. Special condition - unaffected by joinMode.
if (!this.isTrash()) {
if (s == this.ref) {
// Don't modify a saved search object
s = s.clone(this.ref.libraryID);
s2.setScope(s, includeScopeChildren);
}
s.addCondition('includeDeleted', 'true');
s2.addCondition('includeDeleted', 'true');
}
s3.addCondition('includeDeleted', 'true');
s3.setScope(s2, includeScopeChildren);
}
else {
s3 = s2;
}
this._cachedSearch = s3;
return s3;
};
Zotero.CollectionTreeRow.prototype.getChildTags = function () {
@ -537,6 +567,23 @@ Zotero.CollectionTreeRow.prototype.setSearch = function (searchText, mode = null
return true;
}
Zotero.CollectionTreeRow.prototype.setAdvancedSearch = function (advancedSearch) {
this.clearCache();
if (!advancedSearch) {
this.advancedSearch = undefined;
}
else if (this.ref.libraryID === undefined) {
// Feeds pseudo-library -- leave the library unset so that the search
// spans all feed libraries
this.advancedSearch = new Zotero.Search();
this.advancedSearch.fromJSON(advancedSearch.toJSON());
}
else {
this.advancedSearch = advancedSearch.clone(this.ref.libraryID);
}
return true;
};
Zotero.CollectionTreeRow.prototype.setTags = function (tags) {
let oldTags = this.tags instanceof Set ? this.tags : new Set(this.tags || []);
let newTags = tags instanceof Set ? new Set(tags) : new Set(tags || []);
@ -570,8 +617,8 @@ Zotero.CollectionTreeRow.prototype.isSearchMode = function () {
return true;
}
// Quicksearch
if (this.searchText != '') {
// Search filters
if (this.advancedSearch || this.searchText != '') {
return true;
}

View file

@ -597,8 +597,9 @@ Zotero.Search.prototype.search = async function (asTempTable) {
// Run a subsearch to define the superset of possible results
if (this._scope) {
// If subsearch has post-search filter, run and insert ids into temp table
if (this._scope.hasPostSearchFilter()) {
// If subsearch has post-search filter or a recursive scope,
// run and insert ids into temp table
if (this._scope.hasPostSearchFilter() || this._scope._scope) {
var ids = await this._scope.search();
if (!ids) {
return [];

View file

@ -364,7 +364,7 @@ var ZoteroPane = new function () {
'zotero-tb-add': {
ArrowNext: () => document.getElementById("zotero-tb-lookup"),
ArrowPrevious: () => null,
Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(),
Tab: () => document.getElementById("zotero-tb-search").focus(),
ShiftTab: () => {
if (collectionsPane.getAttribute("collapsed")) {
return document.getElementById('zotero-tb-sync');
@ -378,7 +378,7 @@ var ZoteroPane = new function () {
'zotero-tb-lookup': {
ArrowNext: () => document.getElementById("zotero-tb-attachment-add"),
ArrowPrevious: () => document.getElementById("zotero-tb-add"),
Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(),
Tab: () => document.getElementById("zotero-tb-search").focus(),
ShiftTab: () => document.getElementById('zotero-tb-collections-search').click(),
Enter: () => Zotero_Lookup.showPanel(event.target),
' ': () => Zotero_Lookup.showPanel(event.target)
@ -386,13 +386,13 @@ var ZoteroPane = new function () {
'zotero-tb-attachment-add': {
ArrowNext: () => document.getElementById("zotero-tb-note-add"),
ArrowPrevious: () => document.getElementById("zotero-tb-lookup"),
Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(),
Tab: () => document.getElementById("zotero-tb-search").focus(),
ShiftTab: () => document.getElementById('zotero-tb-collections-search').click()
},
'zotero-tb-note-add': {
ArrowNext: () => null,
ArrowPrevious: () => document.getElementById("zotero-tb-attachment-add"),
Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(),
Tab: () => document.getElementById("zotero-tb-search").focus(),
ShiftTab: () => document.getElementById('zotero-tb-collections-search').click()
},
'zotero-tb-search-dropmarker': {
@ -403,9 +403,7 @@ var ZoteroPane = new function () {
},
'zotero-tb-search-textbox': {
Tab: () => document.getElementById("zotero-tb-toggle-item-pane-stacked"),
ShiftTab: () => {
document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus();
}
ShiftTab: () => document.getElementById("zotero-tb-search").focus()
},
'zotero-tb-toggle-item-pane-stacked': {
Tab: () => itemTree.querySelector(".virtualized-table"),
@ -1554,59 +1552,9 @@ var ZoteroPane = new function () {
this.loadURI(Zotero.Groups.addGroupURL);
}
this.newSearch = async function () {
if (Zotero.DB.inTransaction()) {
await Zotero.DB.waitForTransaction();
}
var libraryID = this.getSelectedLibraryID();
var s = new Zotero.Search();
s.libraryID = libraryID;
s.addCondition('title', 'contains', '');
var searches = await Zotero.Searches.getAll(libraryID)
var prefix = Zotero.getString('pane.collections.untitled');
var name = Zotero.Utilities.Internal.getNextName(
prefix,
searches.map(s => s.name).filter(n => n.startsWith(prefix))
);
var io = { dataIn: { search: s, name }, dataOut: null };
window.openDialog('chrome://zotero/content/searchDialog.xhtml','','chrome,modal,centerscreen',io);
if (!io.dataOut) {
return false;
}
s.fromJSON(io.dataOut.json);
await s.saveTx();
return s.id;
};
this.setVirtual = function (libraryID, type, show, select) {
return this.collectionsView.toggleVirtualCollection(libraryID, type, show, select);
};
this.openAdvancedSearchWindow = function () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator('zotero:search');
while (enumerator.hasMoreElements()) {
var win = enumerator.getNext();
}
if (win) {
win.focus();
return;
}
var s = new Zotero.Search();
s.libraryID = this.getSelectedLibraryID();
s.addCondition('title', 'contains', '');
var io = {dataIn: {search: s}, dataOut: null};
window.openDialog('chrome://zotero/content/advancedSearch.xhtml', '', 'chrome,dialog=no,centerscreen', io);
};
this.initItemsTree = async function () {
try {
@ -1688,7 +1636,7 @@ var ZoteroPane = new function () {
this.tagSelector.handleResize();
}
if (this.collectionsView) {
this.collectionsView.updateHeight();
this.collectionsView.updateHeightDebounced();
}
}, 100);
@ -1756,7 +1704,6 @@ var ZoteroPane = new function () {
}
};
this.onCollectionSelected = Zotero.serial(async function () {
var collectionTreeRow = this.getCollectionTreeRow();
if (!collectionTreeRow) {
@ -1772,13 +1719,43 @@ var ZoteroPane = new function () {
return;
}
let advancedSearchDeck = document.getElementById('zotero-advanced-search-pane-deck');
if (this.itemsView.collectionTreeRow?.isSearch()
&& advancedSearchDeck.state === 'open'
&& advancedSearchDeck.selectedSearchType === 'saved') {
let result = Services.prompt.confirmEx(window,
Zotero.getString('saved-search-close-confirmation-title'),
Zotero.getString('saved-search-close-confirmation-body'),
Ci.nsIPromptService.BUTTON_POS_0_DEFAULT
| Ci.nsIPrompt.BUTTON_TITLE_SAVE * Ci.nsIPrompt.BUTTON_POS_0
| Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1
| Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE * Ci.nsIPrompt.BUTTON_POS_2,
null, null, null,
null, {});
switch (result) {
case 0:
await advancedSearchDeck.pane.save();
return;
case 1:
this.collectionsView.selection.selectEventsSuppressed = true;
try {
await this.collectionsView.selectByID(this.itemsView.collectionTreeRow.id);
}
finally {
this.collectionsView.selection.selectEventsSuppressed = false;
}
return;
case 2:
await advancedSearchDeck.pane.cancel();
break;
}
}
// Rename tab
Zotero_Tabs.rename('zotero-pane', collectionTreeRow.getName());
let type = Zotero.Libraries.get(collectionTreeRow.ref.libraryID).libraryType;
// Clear quick search and tag selector when switching views
document.getElementById('zotero-tb-search-textbox').value = "";
document.getElementById('zotero-tb-search').onCollectionSelected();
if (ZoteroPane.tagSelector) {
ZoteroPane.tagSelector.clearTagSelection();
}
@ -1788,6 +1765,7 @@ var ZoteroPane = new function () {
collectionTreeRow.setTags(ZoteroPane.tagSelector.getTagSelection());
}
this._refreshAdvancedSearchPane(collectionTreeRow);
this._updateEnabledActionsForRow(collectionTreeRow);
// If item data not yet loaded for library, load it now.
@ -1815,6 +1793,125 @@ var ZoteroPane = new function () {
Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id);
});
/**
* @param {Zotero.CollectionTreeRow} [collectionTreeRow] - During collection selection,
* the newly selected row, which isn't in the items view yet
*/
this._refreshAdvancedSearchPane = function (collectionTreeRow) {
let deck = document.getElementById('zotero-advanced-search-pane-deck');
deck.pane.refresh();
let search = deck.state === 'closed' || deck.selectedSearchType !== 'temporary' || !deck.pane.active
? null
: deck.pane.search;
if (collectionTreeRow) {
collectionTreeRow.setAdvancedSearch(search);
return undefined;
}
// Apply via the items view, whose row can be a different object from the
// collection tree's current row
return this.itemsView.setFilter('advanced-search', search);
};
/**
* @param {'open' | 'collapsed' | 'closed'} state
*/
this.setAdvancedSearchState = async function (state) {
let deck = document.getElementById('zotero-advanced-search-pane-deck');
let oldState = deck.state;
deck.selectedSearchType = 'temporary';
deck.state = state;
let advancedSearchPane = deck.pane;
document.getElementById('zotero-tb-search').updateMode();
let refreshPromise;
if (state === 'open' && oldState === 'collapsed'
|| state === 'collapsed' && oldState === 'open') {
// State change only causes visual refresh - update the tree height
this.itemsView.updateHeight();
}
else {
// State change changes displayed items - refresh the tree
refreshPromise = this._refreshAdvancedSearchPane();
}
// Update the pane state synchronously, so that a state change initiated
// while the refresh below is pending doesn't see a stale search
if (state === 'closed') {
advancedSearchPane.search = null;
}
else if (state === 'open') {
Zotero_Tabs.select('zotero-pane');
advancedSearchPane.focus();
}
await refreshPromise;
};
/**
* @param {'open' | 'collapsed' | 'closed'} state
*/
this.toggleAdvancedSearchState = async function (state) {
let deck = document.getElementById('zotero-advanced-search-pane-deck');
if (state === deck.state && deck.selectedSearchType !== 'saved') {
// If we're trying to open the pane, and it's already open but not focused,
// focus it
if (state === 'open' && !deck.pane.matches(':focus-within')) {
deck.pane.focus();
return;
}
// Flip the state
switch (state) {
case 'open':
state = 'closed';
break;
case 'collapsed':
case 'closed':
state = 'open';
break;
}
}
await this.setAdvancedSearchState(state);
};
/**
* @param {'open' | 'closed'} state
*/
this.setSavedSearchEditorState = async function (state) {
let collectionTreeRow = this.getCollectionTreeRow();
if (state === 'open' && !collectionTreeRow.isSearch()) {
throw new Error('Cannot show saved search editor outside search row');
}
let deck = document.getElementById('zotero-advanced-search-pane-deck');
deck.selectedSearchType = 'saved';
deck.state = state;
if (state === 'open') {
deck.pane.search = collectionTreeRow.ref;
}
document.getElementById('zotero-tb-search').updateMode();
let refreshPromise = this._refreshAdvancedSearchPane();
if (state === 'open') {
deck.pane.focus();
}
await refreshPromise;
};
this.openAdvancedSearchWindow = function () {
Zotero.debug(`ZoteroPane.openAdvancedSearchWindow() is deprecated -- use ZoteroPane.toggleAdvancedSearchState() instead`);
this.toggleAdvancedSearchState('open');
};
/**
@ -1828,7 +1925,6 @@ var ZoteroPane = new function () {
"menu_noteAdd",
"cmd_zotero_newCollection",
"cmd_zotero_newSavedSearch",
"cmd_zotero_import",
"cmd_zotero_importFromClipboard",
@ -2580,28 +2676,7 @@ var ZoteroPane = new function () {
this.collectionsView.startEditing(row);
}
else {
let s = row.ref.clone();
let groups = [];
// Promises don't work in the modal dialog, so get the group name here, if
// applicable, and pass it in. We only need the group that this search belongs
// to, if any, since the library drop-down is disabled for saved searches.
if (Zotero.Libraries.get(s.libraryID).libraryType == 'group') {
groups.push(Zotero.Groups.getByLibraryID(s.libraryID));
}
var io = {
dataIn: {
search: s,
name: row.getName(),
groups: groups
},
dataOut: null
};
window.openDialog('chrome://zotero/content/searchDialog.xhtml','','chrome,modal,centerscreen',io);
if (io.dataOut) {
row.ref.fromJSON(io.dataOut.json);
await row.ref.saveTx();
Zotero_Tabs.rename("zotero-pane", row.ref.name);
}
this.setSavedSearchEditorState('open');
}
}
};
@ -3274,10 +3349,6 @@ var ZoteroPane = new function () {
id: "newCollection",
command: "cmd_zotero_newCollection"
},
{
id: "newSavedSearch",
command: "cmd_zotero_newSavedSearch"
},
{
id: "newSubcollection",
oncommand: () => {
@ -3564,8 +3635,7 @@ var ZoteroPane = new function () {
show.push(
'sync',
'sep1',
'newCollection',
'newSavedSearch'
'newCollection'
);
}
// Only show "Show Duplicates", "Show Unfiled Items", and "Show Retracted" if rows are hidden
@ -6784,7 +6854,7 @@ var ZoteroPane = new function () {
if (ZoteroPane.itemsView) {
// Need to immediately rerender the items here without any debouncing
// since tree height will have changed
ZoteroPane.itemsView._updateHeight();
ZoteroPane.itemsView.updateHeight();
}
ZoteroContextPane.update();
Zotero_Tabs.updateSidebarLayout();
@ -6970,7 +7040,7 @@ var ZoteroPane = new function () {
var collectionsPaneWidth = collectionsPane.getBoundingClientRect().width;
tagSelector.style.maxWidth = collectionsPaneWidth + 'px';
if (ZoteroPane.itemsView) {
ZoteroPane.itemsView.updateHeight();
ZoteroPane.itemsView.updateHeightDebounced();
}
this.handleTagSelectorResize();

View file

@ -102,7 +102,7 @@
<command id="cmd_zotero_import" oncommand="Zotero_File_Interface.showImportWizard();"/>
<command id="cmd_zotero_importFromClipboard" oncommand="Zotero_File_Interface.importFromClipboard();"/>
<command id="cmd_zotero_exportLibrary" oncommand="Zotero_File_Interface.exportFile();"/>
<command id="cmd_zotero_advancedSearch" oncommand="ZoteroPane_Local.openAdvancedSearchWindow();"/>
<command id="cmd_zotero_advancedSearch" oncommand="ZoteroPane.toggleAdvancedSearchState('open');"/>
<command id="cmd_zotero_copyCitation"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard(true);"
disabled="true"/>
@ -116,7 +116,6 @@
<command id="cmd_zotero_rtfScan" oncommand="window.openDialog('chrome://zotero/content/rtfScan.xhtml', 'rtfScan', 'chrome,centerscreen')"/>
<command id="cmd_zotero_newCollection" oncommand="ZoteroPane_Local.newCollection(ZoteroPane_Local.getSelectedCollection()?.key)"/>
<command id="cmd_zotero_newFeed_fromURL" oncommand="ZoteroPane_Local.newFeedFromURL()"/>
<command id="cmd_zotero_newSavedSearch" oncommand="ZoteroPane_Local.newSearch()"/>
<command id="cmd_zotero_addByIdentifier" oncommand="Zotero_Lookup.showPanel()"/>
<command id="cmd_zotero_newStandaloneFileAttachment" oncommand="ZoteroPane.addAttachmentFromDialog()"/>
<command id="cmd_zotero_newStandaloneLinkedFileAttachment" oncommand="ZoteroPane.addAttachmentFromDialog(true)"/>
@ -428,7 +427,7 @@
key="key_find" accesskey="&findCmd.accesskey;"
oncommand="ZoteroStandalone.currentReader.toggleFindPopup({ open: true });"/>
<menuitem id="menu_advancedSearch"
label="&zotero.toolbar.advancedSearch;"
data-l10n-id="menuitem-advanced-search"
key="key_advancedSearch"
command="cmd_zotero_advancedSearch"/>
<menuseparator hidden="true" id="textfieldDirection-separator"/>
@ -936,7 +935,6 @@
<menuitem class="zotero-menuitem-sync"/>
<menuseparator/>
<menuitem class="zotero-menuitem-new-collection" data-l10n-id="menu-new-collection"/>
<menuitem class="zotero-menuitem-new-saved-search" label="&zotero.toolbar.newSavedSearch.label;"/>
<menuitem class="zotero-menuitem-new-collection" label="&zotero.toolbar.newSubcollection.label;"/>
<menuitem class="zotero-menuitem-refresh-feed"/>
<menuseparator/>
@ -1371,6 +1369,7 @@
</hbox>
</toolbar>
<advanced-search-deck id="zotero-advanced-search-pane-deck"/>
<hbox id="zotero-items-pane" class="virtualized-table-container" flex="1" clickthrough="never">
<html:div id="zotero-items-tree"></html:div>
</hbox>

View file

@ -1,7 +1,5 @@
<!ENTITY zotero.search.name "Name:">
<!ENTITY zotero.search.searchInLibrary "Search in library:">
<!ENTITY zotero.search.joinMode.prefix "Match">
<!ENTITY zotero.search.joinMode.any "any">
<!ENTITY zotero.search.joinMode.all "all">
@ -19,7 +17,3 @@
<!ENTITY zotero.search.date.units.days "days">
<!ENTITY zotero.search.date.units.months "months">
<!ENTITY zotero.search.date.units.years "years">
<!ENTITY zotero.search.search "Search">
<!ENTITY zotero.search.clear "Clear">
<!ENTITY zotero.search.saveSearch "Save Search">

View file

@ -82,7 +82,6 @@
<!ENTITY zotero.toolbar.removeItem.label "Remove Item…">
<!ENTITY zotero.toolbar.newGroup "New Group…">
<!ENTITY zotero.toolbar.newSubcollection.label "New Subcollection…">
<!ENTITY zotero.toolbar.newSavedSearch.label "New Saved Search…">
<!ENTITY zotero.toolbar.emptyTrash.label "Empty Trash">
<!ENTITY zotero.toolbar.tagSelector.label "Show/Hide Tag Selector">
<!ENTITY zotero.toolbar.actions.label "Actions">
@ -93,7 +92,6 @@
<!ENTITY zotero.toolbar.preferences.label "Preferences…">
<!ENTITY zotero.toolbar.supportAndDocumentation "Support and Documentation">
<!ENTITY zotero.toolbar.about.label "About Zotero">
<!ENTITY zotero.toolbar.advancedSearch "Advanced Search">
<!ENTITY zotero.toolbar.openURL.label "Locate">
<!ENTITY zotero.toolbar.openURL.tooltip "Find through your local library">

View file

@ -48,11 +48,15 @@ general-view-troubleshooting-instructions = View Troubleshooting Instructions
general-go-back = Go Back
general-accept = Accept
general-cancel = Cancel
cancel-button =
.label = { general-cancel }
general-show-in-library = Show in Library
general-restartApp = Restart { -app-name }
general-restartInTroubleshootingMode = Restart in Troubleshooting Mode
general-save = Save
general-clear = Clear
clear-button =
.label = { general-clear }
general-update = Update
general-back = Back
general-edit = Edit
@ -193,8 +197,9 @@ item-menu-remove-from-recently-read =
.label = Remove from { recently-read }…
collections-menu-rename-collection =
.label = Rename Collection
edit-saved-search = Edit Saved Search
collections-menu-edit-saved-search =
.label = Edit Saved Search
.label = { edit-saved-search }
collections-menu-move-collection =
.label = Move To
collections-menu-copy-collection =
@ -725,6 +730,13 @@ quicksearch-input =
.placeholder = { $placeholder }
.aria-description = { $placeholder }
advanced-search = Advanced Search
menuitem-advanced-search =
.label = { advanced-search }
quicksearch-advanced-search-button =
.tooltiptext = { advanced-search }
.aria-label = { advanced-search }
item-pane-header-view-as =
.label = View As
item-pane-header-none =
@ -926,3 +938,11 @@ os-keystore-migrate-failed =
[windows] { -app-name } couldnt encrypt your stored credentials. Your credentials remain stored unencrypted on disk. Restart { -app-name } and try again.
*[other] { -app-name } couldnt access your { -os-name } keyring to encrypt your stored credentials. Your credentials remain stored unencrypted on disk. Make sure a keyring service is running and restart { -app-name }.
}
search-button =
.label = Search
save-search-button =
.label = Save Search
saved-search-close-confirmation-title = Editing Saved Search
saved-search-close-confirmation-body = Do you want to save changes you made to this saved search?

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.75 7H18V2H13V3.25L15.8661 3.25L11 8.11612L11.8839 9L16.75 4.13389V7ZM4.13389 16.75L9 11.8839L8.11612 11L3.25 15.8661V13H2V17.375V18H2.625H7V16.75H4.13389Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1339 7.75L18 2.88388L17.1161 2L12.25 6.86612V4H11V8.375V9H11.625H16V7.75H13.1339ZM7.75 13.1339L7.75 16H9L9 11H4L4 12.25H6.86612L2 17.1161L2.88388 18L7.75 13.1339Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 342 B

View file

@ -21,7 +21,6 @@
// Components
// --------------------------------------------------
@import "components/advancedSearch";
@import "components/annotation";
@import "components/autosuggest";
@import "components/banners";
@ -79,6 +78,7 @@
// Elements
// --------------------------------------------------
@import "elements/advancedSearchPane";
@import "elements/attachmentBox";
@import "elements/attachmentPreview";
@import "elements/attachmentPreviewBox";

View file

@ -1,26 +0,0 @@
#zotero-advanced-search-dialog {
@include macOS-normalize-controls;
min-height: 500px;
min-width: 700px;
#zotero-search-box-container {
max-height: 100vh;
}
#zotero-search-box-controls {
padding: 3px;
}
#recursiveCheckbox, #noChildrenCheckbox, #includeParentsAndChildrenCheckbox {
margin: 3px 6px;
}
#zotero-search-buttons {
margin: 3px 0;
button {
margin-inline-start: 6px;
}
}
}

View file

@ -27,7 +27,6 @@ $menu-icons: (
library-lookup: "library-lookup",
new-feed: "feed",
note: "note",
new-saved-search: "saved-search",
show-duplicates: "duplicate",
show-unfiled: "unfiled",
show-retracted: "retracted",

View file

@ -1,8 +1,10 @@
$toolbar-padding-inline: 8px;
.toolbar {
height: $height-toolbar !important; /* Hard-code this to fix toolbar icon compression on Linux */
min-height: $height-toolbar; /* Needed to prevent squashing by stretched tag selector */
margin: 0;
padding: 0px 8px 0px 8px;
padding: 0 $toolbar-padding-inline;
min-width: 1px;
}

View file

@ -0,0 +1,54 @@
advanced-search-deck {
flex-direction: column;
> advanced-search-pane:not(.deck-selected) {
display: none;
}
}
advanced-search-pane {
@include inactive-opacity;
flex-direction: column;
// Inline padding is further down the tree so focus rings work with overflow: auto
padding-block: 8px;
gap: 8px;
background: var(--material-toolbar);
.saved-search-name-row {
align-items: center;
}
.advanced-search-buttons {
padding-inline: 8px;
gap: 8px;
button {
min-width: 100px;
}
}
&[type="temporary"] {
.saved-search-name-row {
display: none;
}
.advanced-search-buttons {
.cancel-button {
display: none;
}
}
}
&[type="saved"] {
.advanced-search-buttons {
.search-button {
display: none;
}
.clear-button {
display: none;
}
}
}
}

View file

@ -28,6 +28,10 @@ quick-search-textbox {
}
}
#zotero-tb-search {
height: 100%;
}
#zotero-tb-search-dropmarker {
position: relative;
// 6px padding + 16px icon width + 2px padding + 8px dropmarker width + 4px padding
@ -70,6 +74,51 @@ quick-search-textbox {
height: 28px;
z-index: 1;
width: 100%;
// Keep input text and the clear icon out from under the Advanced Search button
&:has(~ #zotero-tb-search-advanced-button) {
padding-inline-end: 30px;
}
}
// Match the specificity of the #zotero-items-toolbar toolbarbutton rules
#zotero-tb-search #zotero-tb-search-advanced-button {
position: relative;
width: 28px;
height: 28px;
min-width: 0;
margin: 0;
margin-inline-start: -30px;
margin-inline-end: 2px;
padding: 0;
z-index: 2;
@include svgicon-menu("filter", "universal", "16");
color: var(--fill-secondary);
@include focus-ring;
// Show the hover effect on a chip around the icon, inset within the field,
// rather than on the whole button
&:hover, &:active {
background-color: transparent;
}
.toolbarbutton-icon {
width: 24px;
height: 24px;
// The glyph is weighted slightly to the right within the SVG, so shift it
// a pixel left to optically center it (physical sides -- doesn't flip in RTL)
padding: 5px 6px 5px 4px;
border-radius: 4px;
}
&:hover .toolbarbutton-icon {
background-color: var(--fill-quinary);
}
&:active .toolbarbutton-icon {
background-color: var(--fill-quarternary);
}
}
#zotero-tb-search-spinner {
@ -79,6 +128,59 @@ quick-search-textbox {
visibility: hidden;
}
#search-wrapper {
#search-deck {
width: 100%;
}
}
#search-wrapper {
align-items: center;
}
#advanced-search-indicator {
gap: 8px;
align-items: center;
// Cover the inline-end toolbar padding and the existing bottom border
margin-inline-end: -$toolbar-padding-inline;
padding-inline: $toolbar-padding-inline;
margin-bottom: -1px;
background: var(--material-toolbar);
border-inline-start: var(--material-border);
// Prevent text from shifting when border shows after panel is collapsed
border-block: var(--material-border-transparent);
// This is styled like it's part of the Advanced Search panel, which can't drag the window,
// so match that behavior
-moz-window-dragging: no-drag;
& > label {
flex: 1;
padding-inline: 4px;
font-weight: 700;
}
.zotero-clicky {
width: 28px;
height: 28px;
margin: 0;
padding: 4px;
}
.advanced-collapse-button {
@include svgicon-menu("minimize", "universal", "20");
}
.advanced-close-button {
@include svgicon-menu("x", "universal", "20");
}
&[data-collapsed="true"] {
border-bottom: var(--material-border);
.advanced-collapse-button {
@include svgicon-menu("maximize", "universal", "20");
}
}
}

View file

@ -1,15 +1,31 @@
zoterosearch {
#search-box > hbox, #search-box > groupbox {
margin: 0 6px;
}
flex-direction: column;
padding-inline: 8px;
gap: 8px;
overflow: auto;
max-height: 33vh;
@include macOS-normalize-controls;
groupbox {
margin-top: 0;
padding-top: 0;
gap: 8px;
& > #conditions {
-moz-appearance: groupbox;
padding: 6px;
min-width: fit-content; // Grow with content, don't overflow
padding: 8px;
gap: 8px;
background: var(--fill-senary);
border: var(--material-border-quinary);
border-radius: 6px;
}
}
#search-option-checkboxes {
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
checkbox {
margin: 0;
}
}
@ -23,10 +39,6 @@ zoterosearch {
padding-left: 0 !important;
}
checkbox {
margin-right: .5em;
}
input {
margin-block: 0;
}

View file

@ -1,7 +1,7 @@
"use strict";
describe("Advanced Search", function () {
var win, zp;
var win, zp, deck;
before(function* () {
yield resetDB({
@ -10,56 +10,170 @@ describe("Advanced Search", function () {
});
win = yield loadZoteroPane();
zp = win.ZoteroPane;
deck = win.document.getElementById('zotero-advanced-search-pane-deck');
});
after(function () {
win.close();
});
it("should perform a search", function* () {
var item = yield createDataObject('item', { setTitle: true });
it("should perform a search", async function () {
var item = await createDataObject('item', { setTitle: true });
var otherItem = await createDataObject('item', { setTitle: true });
await zp.toggleAdvancedSearchState('open');
var pane = deck.pane;
// Opening the pane shouldn't filter the items list
await zp.itemsView.waitForLoad();
assert.equal(zp.itemsView.rowCount, 2);
var promise = waitForWindow('chrome://zotero/content/advancedSearch.xhtml');
zp.openAdvancedSearchWindow();
var searchWin = yield promise;
yield searchWin.ZoteroAdvancedSearch._loadedDeferred.promise;
// Add condition
var searchBox = searchWin.document.getElementById('zotero-search-box');
var s = new Zotero.Search();
s.addCondition('title', 'is', item.getField('title'))
searchBox.search = s;
s.libraryID = item.libraryID;
s.addCondition('title', 'is', item.getField('title'));
pane.search = s;
// Run search and wait for results
var o = searchWin.ZoteroAdvancedSearch;
var iv = o.itemsView;
yield iv.waitForLoad();
yield o.search();
yield iv.waitForLoad();
var iv = zp.itemsView;
await pane.submit();
await iv.waitForLoad();
// Check results
assert.equal(iv.rowCount, 1);
var index = iv.getRowIndexByID(item.id);
assert.isNumber(index);
searchWin.close();
// Closing should restore the unfiltered view
await zp.setAdvancedSearchState('closed');
assert.equal(iv.rowCount, 2);
yield item.eraseTx();
await item.eraseTx();
await otherItem.eraseTx();
});
it("should show results in trash", async function () {
var item = await createDataObject('item', { setTitle: true });
item.deleted = true;
await item.saveTx();
await zp.toggleAdvancedSearchState('open');
var pane = deck.pane;
var s = new Zotero.Search();
s.libraryID = item.libraryID;
s.addCondition('title', 'is', item.getField('title'));
pane.search = s;
var iv = zp.itemsView;
await pane.submit();
await iv.waitForLoad();
assert.isNumber(iv.getRowIndexByID(item.id));
await zp.setAdvancedSearchState('closed');
await item.eraseTx();
});
it("shouldn't reapply previous search when reopened while closing", async function () {
var item = await createDataObject('item', { setTitle: true });
await zp.toggleAdvancedSearchState('open');
var s = new Zotero.Search();
s.libraryID = item.libraryID;
s.addCondition('title', 'is', 'nomatch');
deck.pane.search = s;
await deck.pane.submit();
await zp.itemsView.waitForLoad();
assert.equal(zp.itemsView.rowCount, 0);
// Close and immediately reopen, without waiting, as with UI clicks
zp.toggleAdvancedSearchState('closed');
await zp.toggleAdvancedSearchState('open');
await zp.itemsView.waitForLoad();
// The previous search shouldn't have been reapplied
assert.isNumber(zp.itemsView.getRowIndexByID(item.id));
await zp.setAdvancedSearchState('closed');
await item.eraseTx();
});
it("should scope results to the selected saved search", async function () {
var inBoth = await createDataObject('item', { title: "foo bar" });
var inSavedOnly = await createDataObject('item', { title: "foo baz" });
var inAdvancedOnly = await createDataObject('item', { title: "bar qux" });
var saved = new Zotero.Search();
saved.libraryID = Zotero.Libraries.userLibraryID;
saved.name = "Scope Test";
saved.addCondition('title', 'contains', 'foo');
await saved.saveTx();
await select(win, saved);
await zp.toggleAdvancedSearchState('open');
var s = new Zotero.Search();
s.libraryID = saved.libraryID;
s.addCondition('title', 'contains', 'bar');
deck.pane.search = s;
var iv = zp.itemsView;
await deck.pane.submit();
await iv.waitForLoad();
// Only the item matching both the saved search and the advanced search
assert.equal(iv.rowCount, 1);
assert.isNumber(iv.getRowIndexByID(inBoth.id));
// The saved search itself shouldn't have been modified
assert.lengthOf(Object.keys(saved.getConditions()), 1);
await zp.setAdvancedSearchState('closed');
await Zotero.Items.erase([inBoth.id, inSavedOnly.id, inAdvancedOnly.id]);
await saved.eraseTx();
});
it("should search across feeds in Feeds view", async function () {
let feed = await createFeed();
let feedItem = await createDataObject('feedItem', { libraryID: feed.libraryID, setTitle: true }, { skipSelect: true });
let otherFeedItem = await createDataObject('feedItem', { libraryID: feed.libraryID, setTitle: true }, { skipSelect: true });
await zp.collectionsView.selectFeeds();
await waitForItemsLoad(win);
await zp.toggleAdvancedSearchState('open');
var s = new Zotero.Search();
s.libraryID = Zotero.Libraries.userLibraryID;
s.addCondition('title', 'is', feedItem.getField('title'));
deck.pane.search = s;
var iv = zp.itemsView;
await deck.pane.submit();
await iv.waitForLoad();
assert.equal(iv.rowCount, 1);
assert.isNumber(iv.getRowIndexByID(feedItem.id));
await zp.setAdvancedSearchState('closed');
await selectLibrary(win);
await feed.eraseTx();
});
describe("Conditions", function () {
var searchWin, searchBox, conditions;
var pane, searchBox, conditions;
before(function* () {
var promise = waitForWindow('chrome://zotero/content/advancedSearch.xhtml');
zp.openAdvancedSearchWindow();
searchWin = yield promise;
searchBox = searchWin.document.getElementById('zotero-search-box');
before(async function () {
await zp.toggleAdvancedSearchState('open');
pane = deck.pane;
searchBox = pane.querySelector('zoterosearch');
conditions = searchBox.querySelector('#conditions');
});
after(function () {
searchWin.close();
after(async function () {
await zp.setAdvancedSearchState('closed');
});
describe("Collection", function () {
@ -73,8 +187,9 @@ describe("Advanced Search", function () {
// Add condition
var s = new Zotero.Search();
s.libraryID = Zotero.Libraries.userLibraryID;
s.addCondition('title', 'is', '');
searchBox.search = s;
pane.search = s;
var searchCondition = conditions.firstChild;
var conditionsMenu = searchCondition.querySelector('#conditionsmenu');
@ -113,8 +228,9 @@ describe("Advanced Search", function () {
var search = await createDataObject('search', { name: "A" });
var s = new Zotero.Search();
s.libraryID = Zotero.Libraries.userLibraryID;
s.addCondition('savedSearch', 'is', search.key);
searchBox.search = s;
pane.search = s;
var searchCondition = conditions.firstChild;
var conditionsMenu = searchCondition.querySelector('#conditionsmenu');
@ -132,8 +248,9 @@ describe("Advanced Search", function () {
var search = await createDataObject('search', { name: "B" });
var s = new Zotero.Search();
s.libraryID = Zotero.Libraries.userLibraryID;
s.addCondition('title', 'is', '');
searchBox.search = s;
pane.search = s;
var searchCondition = conditions.firstChild;
var conditionsMenu = searchCondition.querySelector('#conditionsmenu');
@ -174,8 +291,9 @@ describe("Advanced Search", function () {
var search2 = await createDataObject('search', { name: "D", libraryID: groupLibraryID });
var s = new Zotero.Search();
s.libraryID = Zotero.Libraries.userLibraryID;
s.addCondition('title', 'is', '');
searchBox.search = s;
pane.search = s;
var searchCondition = conditions.firstChild;
var conditionsMenu = searchCondition.querySelector('#conditionsmenu');
@ -198,18 +316,13 @@ describe("Advanced Search", function () {
}
assert.equal(valueMenu.value, "S" + search1.key);
var libraryMenu = searchWin.document.getElementById('libraryMenu');
for (let i = 0; i < libraryMenu.itemCount; i++) {
let menuitem = libraryMenu.getItemAtIndex(i);
// Switch to group library
if (menuitem.value == groupLibraryID) {
menuitem.click();
break;
}
}
// Switch to the group library in the collection tree, which changes
// the search library and re-renders the conditions
await selectLibrary(win, groupLibraryID);
var values = [];
valueMenu = searchCondition.querySelector('#valuemenu')
searchCondition = conditions.firstChild;
valueMenu = searchCondition.querySelector('#valuemenu');
assert.equal(valueMenu.value, "C" + collection2.key);
for (let i = 0; i < valueMenu.itemCount; i++) {
let menuitem = valueMenu.getItemAtIndex(i);
@ -220,6 +333,8 @@ describe("Advanced Search", function () {
assert.include(values, "C" + collection2.key);
assert.include(values, "S" + search2.key);
await selectLibrary(win);
await Zotero.Collections.erase([collection1.id, collection2.id]);
await Zotero.Searches.erase([search1.id, search2.id]);
});

View file

@ -2783,47 +2783,6 @@ describe("CollectionViewItemTree", function () {
assert.include(text, toplevelItemTwo.getDisplayTitle());
});
});
describe('Advanced Search', function () {
describe('#notify', function () {
it('should resolve the returned promise when an item is selected', async function() {
var item = await createDataObject('item', { setTitle: true });
var promise = waitForWindow('chrome://zotero/content/advancedSearch.xhtml');
zp.openAdvancedSearchWindow();
var searchWin = await promise;
await searchWin.ZoteroAdvancedSearch._loadedDeferred.promise;
// Add condition
var searchBox = searchWin.document.getElementById('zotero-search-box');
var s = new Zotero.Search();
s.addCondition('title', 'is', item.getField('title'))
searchBox.search = s;
// Run search and wait for results
var o = searchWin.ZoteroAdvancedSearch;
var iv = o.itemsView;
await iv.waitForLoad();
await o.search();
await iv.waitForLoad();
// Check results
assert.equal(iv.rowCount, 1);
// Make sure an item is selected (otherwise notify resolves fine)
await iv.selectItem(item.id);
assert.equal(iv.selection.count, 1);
let notifySpy = sinon.spy(iv, 'notify');
await createDataObject('item');
assert.isTrue(notifySpy.calledOnce);
await notifySpy.returnValues[0];
notifySpy.restore();
searchWin.close();
await item.eraseTx();
});
});
});
describe("Search error handling", function () {
var rowProvider;

View file

@ -236,6 +236,9 @@ describe("Zotero.Search", function () {
matches = await s.search();
// Result should be the same
assert.sameMembers(matches, [itemOne.id, itemTwo.id]);
await itemOne.eraseTx();
await itemTwo.eraseTx();
});
});

View file

@ -117,30 +117,22 @@ describe("ZoteroPane", function () {
});
});
describe("#newSearch()", function () {
describe("Advanced Search", function () {
it("should create a saved search", async function () {
var promise = waitForDialog(
// TODO: Test changing a condition
function (dialog) {},
'accept',
'chrome://zotero/content/searchDialog.xhtml'
);
var id = await zp.newSearch();
await promise;
var search = Zotero.Searches.get(id);
assert.ok(search);
assert.isTrue(search.name.startsWith(Zotero.getString('pane.collections.untitled')));
});
it("should handle clicking Cancel in the search window", async function () {
var promise = waitForDialog(
function (dialog) {},
'cancel',
'chrome://zotero/content/searchDialog.xhtml'
);
var id = await zp.newSearch();
await promise;
assert.isFalse(id);
await selectLibrary(win);
await zp.toggleAdvancedSearchState('open');
var deck = doc.getElementById('zotero-advanced-search-pane-deck');
var searchIDs = (await Zotero.Searches.getAll(userLibraryID)).map(s => s.id);
await deck.pane.save();
var newSearches = (await Zotero.Searches.getAll(userLibraryID))
.filter(s => !searchIDs.includes(s.id));
assert.lengthOf(newSearches, 1);
assert.isTrue(newSearches[0].name.startsWith(Zotero.getString('pane.collections.untitled')));
assert.equal(deck.state, 'closed');
await newSearches[0].eraseTx();
});
});
@ -1007,19 +999,26 @@ describe("ZoteroPane", function () {
});
describe("#editSelectedCollection()", function () {
async function editSearchAddCondition(search) {
await select(win, search);
await zp.editSelectedCollection();
var deck = doc.getElementById('zotero-advanced-search-pane-deck');
assert.equal(deck.state, 'open');
assert.equal(deck.selectedSearchType, 'saved');
var pane = deck.pane;
var searchBox = pane.querySelector('zoterosearch');
var c = searchBox.search.getCondition(
searchBox.search.addCondition("title", "contains", "foo")
);
searchBox.addCondition(c);
await pane.save();
}
it("should edit a saved search", async function () {
var search = await createDataObject('search');
await select(win, search);
var promise = waitForWindow('chrome://zotero/content/searchDialog.xhtml', function (win) {
let searchBox = win.document.getElementById('search-box');
var c = searchBox.search.getCondition(
searchBox.search.addCondition("title", "contains", "foo")
);
searchBox.addCondition(c);
win.document.querySelector('dialog').acceptDialog();
});
await zp.editSelectedCollection();
await promise;
await editSearchAddCondition(search);
var conditions = search.getConditions();
assert.lengthOf(Object.keys(conditions), 3);
});
@ -1027,17 +1026,7 @@ describe("ZoteroPane", function () {
it("should edit a saved search in a group", async function () {
var group = await getGroup();
var search = await createDataObject('search', { libraryID: group.libraryID });
await select(win, search);
var promise = waitForWindow('chrome://zotero/content/searchDialog.xhtml', function (win) {
let searchBox = win.document.getElementById('search-box');
var c = searchBox.search.getCondition(
searchBox.search.addCondition("title", "contains", "foo")
);
searchBox.addCondition(c);
win.document.querySelector('dialog').acceptDialog();
});
await zp.editSelectedCollection();
await promise;
await editSearchAddCondition(search);
var conditions = search.getConditions();
assert.lengthOf(Object.keys(conditions), 3);
});