Covers saved-search ranking and quick-search precedence, mixed
top-K/collection selections, rank-only behavior with an unavailable
index, reranking on indexer refresh events, scoped 'Match any' saves
keeping the marker at the root, numeric-operator round trips, and
concurrent query embeds sharing one worker call.
A root-level 'bestMatch' condition -- serialized as a marker like
joinMode and resultLevel -- ranks results with the Relevance column via
a "Sort results by best match for" field below the root group's
conditions, composing semantic ranking with Boolean filtering. A
best-match quick search converts to it via the Advanced Search button,
and a selected saved search's own marker activates ranking too,
overridden by an active best-match quick search.
Without a cutoff the condition is rank-only and membership is untouched
-- including while the index is unavailable -- so a saved search acts
identically as a source, and unscoreable items just sort last. With the
optional "keeping top" cutoff, carried in the marker's operator,
membership becomes the K most similar results, applied in search() so
scopes, counts, and the API see the same set. A transient search's
cutoff, which applies uniformly to every selected row, is reapplied
over the merged results so a multi-collection selection returns K
members total; a saved search's cutoff is part of its own membership
and never trims other selected rows. The query embedding is cached
in-flight, so the per-row membership passes and the merged ranking
share one worker embed.
A search could embed the query with one model's prefix on a worker
initialized for another and compare it against vectors from a third.
Tag the worker with the model version it was initialized for, make
scoring wait out an in-progress model switch, refuse to score an index
that wasn't stamped by the active model, and discard results if the
model changes mid-scoring. The items view treats a not-ready index as
an empty result rather than showing an unranked scope.
Semantic similarity has no natural relevance threshold, so instead of
asking the user to pick an arbitrary result count, show every scored
item and surface the ranking directly: a Relevance column appears and
becomes the sort while a best-match search is active, and the previous
sort and columns return when it clears. The merged results are scored
in a single pass in the row provider, so ranks are global across a
multi-collection selection, child items (attachments, notes,
annotations) rank via their top-level item, and equal scores get equal
ranks that order deterministically via the secondary sort fields. Items
without a stored embedding are filtered out.
Each cell renders the score's position within the model's display range
as a bar, so relevant results read as full and the irrelevant tail
reads as empty. The ranges are provisional per-model display constants.
Sorting uses the ranks, which are also exposed to assistive technology
and as the cell tooltip. On a focused selected row the bar
switches to white so the fill doesn't vanish into the accent selection
background.
The embeddings are a local, rebuildable, model-specific index, so they
don't belong in the main database or its backups. Follow the full-text
content index pattern: a lazily attached embeddings.sqlite versioned via
PRAGMA user_version, tied to the main database by localUserKey, with
corruption recovery and idle-maintenance vacuuming via the DBConnection
hooks. Since a cross-database foreign key isn't possible, item deletions
now clear embeddings via the notifier, and the indexed-model identity
moves from a pref into the database's meta table.
Binding is meaningful for a condition that matches at every level -- a
tag bound to an attachment means the tag is on the attachment -- but a
group carrying one lost the binding as soon as the search was
serialized, so "items with an attachment tagged foo" couldn't be built.
An exact-match condition compared with SQLite's case-sensitive '=' and
skipped the normalized shadow columns, so 'publication is "review of
finance"' missed "Review of Finance" while every other kind of search
matched it.
Bug 2037682 removed nsIPK11TokenDB in favor of getting the internal key
token directly from @mozilla.org/security/internalkeytoken;1, and bug
2043434 replaced initPassword()/needsUserInit with changePassword().
Bug 2008041's change to boolean attributes also covers hidden and
collapsed, whose UA selectors became [hidden] and [collapsed], so
setAttribute('hidden', false) now hides the element. Switch the setters
that can be passed a falsy value to toggleAttribute(), read them with
hasAttribute(), and match the [collapsed=true] selectors in our own
stylesheets to the new presence-only form.
Bug 2008041 made them html-style boolean attributes, so presence alone
means true and setAttribute('disabled', false) now disables the element.
Switch the sites that can be passed a falsy value to toggleAttribute(),
read them with hasAttribute(), and drop a disabled="false" from markup.
Sites that only ever pass true are left alone.
Bug 2009806 moved XUL checkbox click handling into C++ and dropped the
CheckboxStateChange event, so listen for command instead. The event now
only fires on user interaction, so the tests have to click the checkboxes
rather than assign to .checked.
findLogins() now throws, and removeLogin()/modifyLogin()/removeAllLogins()
are Async-suffixed and return promises. Without this, Zotero couldn't read
the stored API key and showed the corrupted-logins-database error on
startup.
There's no synchronous read left, so Zotero.Sync.Runner.enabled -- which is
consulted from places that can't await -- now reads a value that
hasCredentials() caches.
Closes#5974.
Additional fixes for broken item tree behaviour when multiple items are
selected, and changing focus with ctrl/cmd-arrow keys.
Aligned Collection Tree/Virtualized Tree collapse/expand behaviour when
multiple containers are selected, one of them is focused, and arrow key
left-right is pressed, to the behaviour in Item Tree - now all of them
are collapsed/expanded.
---------
Co-authored-by: Dan Stillman <dstillman@zotero.org>
The PMC lookup in #5906 was added before the OA lookup, but the OA
lookup often returns a PMC URL of its own (including, at least now, for
the DOI given in the PR), with a direct file URL rather than a page to
scrape, so try it first and fall back to the PMCID-derived URL only when
it turns up nothing.
Since #5906, the 'oa' resolver list can start with a plain URL object
for items with a PMCID, but _getOpenAccessPDFURLs() and
saveOpenAccessAttachment() called resolvers[0](), which threw for those
items -- skipping the lookup silently in the first case and failing the
save in the second.
Show File triggered a file 'open' notification, so revealing a file
moved its item to the top of Recently Read. Use a separate 'reveal'
event, which file syncing still observes to queue a modification check.
https://forums.zotero.org/discussion/133006/
- Fix multiple potential scenarios causing a template engine crash
- Add support for specifying string literals in the template engine
- Validate `if/else/elseif` order and matching clause closures
- Validate to ensure every `{{` is properly closed with a matching `}}`
- When a template is invalid, display a warning, do not offer batch-renaming tools, do not update synced setting
- When a template is invalid, prompt the user to fix or reset the template when
Closes#5965
Menu plugins reading it acted on one arbitrary row of the selection.
Reading it now throws and names collectionTreeRows, which the context
has already provided since multi-collection selection landed.
The context now copies property descriptors rather than values, since
copying values would evaluate the throwing collectionTreeRow getter for
every menu.
Collections and saved searches can be selected together, and the
search's items don't need to be in any of the collections, which would
result in search-only items being moved to the trash.
The view-wide branches (trash, duplicates, feeds, Recently Read) read
the first selected row, which multi-collection selection preserved with
a getter rather than updating. setCollectionTreeRows() now derives the
kind of view the selection adds up to, throwing if the rows disagree,
and those branches test it.
Multi-collection selection left .collectionTreeRow and similar in place
to reduce breakage, but that would just leave plugins and other callers
potentially broken when multiple rows were selected. All singular
getters now throw and say what to use instead. getSelectedLibraryIDs()
was added to replace getSelectedLibraryID().
Collections and saved searches can be selected together, so a
collection-item change called getDescendents() on rows that don't have
it and compared search IDs against collection IDs.
Recently Read rows can span libraries, but read attachments were marked
as matches for the first row's library only, leaving the rest as grayed
context rows.
Code walking the items list assumed every row was an object, so the
headers and spacers shown whenever more than one row is selected got
picked up as items: getSortedItems() passed them to export and report
generation, restoring from the trash called item methods on them, and
the item pane counted them in "N items in this view".
The cache-existence check didn't await getCacheObject(), so the
promise was always truthy and no cache object was ever saved for
objects the server reported as unchanged. Without a cached version,
later remote changes to those objects produced avoidable conflicts.
lastRead conflicts were auto-resolved only when a cached version of
the object was available, so differing read times could still trigger
the conflict resolution window.
Clicking the + button on a row that is part of a multi-item selection
now adds all selected items to the citation, matching the existing
list-mode behavior. Clicking + on an unselected row still adds only
that item.
With focus moving back to the input after a multi-item add, a typed
number was applied as a page locator to every just-added item. It now
starts a search instead. Single-item adds still enable the typed-locator
shortcut.
_scrollItemTreeToRow() parsed the row index from the wrong rowID
segment, so it threw on every call, and adding items from the items list
never returned focus to the input as intended.
Every local API response includes a stable per-database server ID so
that clients can partition cached data, especially local object
versions, by Zotero instance. Write requests must include the ID, and
when it's provided on any request it must match the current server.
---------
Co-authored-by: Dan Stillman <dstillman@zotero.org>
Add support for item, collection, and search writes (POST/PUT/PATCH/
DELETE), tag deletion, full-text writes, and the three-phase file
upload flow. Writes require a local API key granted by the user via a
confirmation dialog (POST /api/local/authorize) and follow the web
API's concurrency model, with If-Unmodified-Since-Version and
per-object version preconditions checked against local versions.
---------
Co-authored-by: Dan Stillman <dstillman@zotero.org>
Add a clientVersion column for items, collections, searches, and
libraries, incremented once per library per transaction on every
object save or deletion. The local API reports these versions instead
of synced versions -- in object JSON, format=versions, since=
filtering, and Last-Modified-Version -- since synced versions don't
reflect local changes and are 0 for unsynced objects. Group metadata
responses keep reporting the synced group version, which has no local
counterpart.
---------
Co-authored-by: Dan Stillman <dstillman@zotero.org>
allowJavaScript: false didn't disable scripts for a system-principal
document, such as a blob: URL created from chrome code, which runs
scripts regardless of the allowJavascript flag. Sandbox the document
instead, forcing a null principal and blocking script execution.
The candidate scan counted items whose Extra content parsed as fields
but couldn't actually be migrated (e.g., fields already set on the
item), so the window could appear on every global schema update without
changing anything.
Also use a dedicated headline instead of "Upgrading database…", and show
the window only if the migration is still running after 500 ms.
A stored file's path is normally 'storage:<filename>', which is parsed
as a string, but some ancient libraries apparently have relative paths
('../.../foo.pdf') that fell through to PathUtils.filename() and threw
NS_ERROR_FILE_UNRECOGNIZED_PATH, breaking syncing.
Resolve stored-file leaves as strings regardless of prefix and reserve
PathUtils.filename() for linked files, whose paths are genuine absolute
paths.
https://forums.zotero.org/discussion/132861/zotero-sync-issue-id-1987012678
ee68452b treated any backslash as a directory separator, in both the
attachmentPath setter and the userdata 128 migration. But backslashes
are technically valid on Linux/macOS and appear in real filenames (due
to LaTeX in titles, etc.), so that would wrongly throw on or truncate
them. Only a forward slash (never valid in a filename) or a Windows
absolute path (drive-letter or UNC prefix) reliably indicates a leaked
directory path, so treat just those as paths and leave bare backslashes
alone. We should make sure we're running getValidFileName() everywhere
to avoid saving those.
These conditions expanded into an OR-group across their underlying
fields, so a "does not contain"/"is not" operator matched almost every
item: any item missing one of the fields satisfied that field's negated
condition. Use an AND-group for negative operators, so the value must be
absent from every field.
https://forums.zotero.org/discussion/132835/
Selecting all libraries pulled in feed rows, which belong to a different
visibility group and so can't be shown together, collapsing the
selection back to a single row. Constrain the all-libraries scope to the
current selection's visibility group. Also preventDefault() the shortcut
so it no longer falls through to the cmd_selectAll key command, which
threw NS_ERROR_FAILURE.
https://forums.zotero.org/discussion/132843/beta-cannot-select-all-libraries