diff --git a/chrome/content/zotero/elements/zoteroSearch.js b/chrome/content/zotero/elements/zoteroSearch.js
index a73b56e1b7..d71e02a2fe 100644
--- a/chrome/content/zotero/elements/zoteroSearch.js
+++ b/chrome/content/zotero/elements/zoteroSearch.js
@@ -363,6 +363,7 @@
case 'lastRead':
case 'itemType':
case 'fileTypeID':
+ case 'attachmentStorageType':
case 'publicationTitle':
case 'tag':
case 'note':
@@ -490,6 +491,16 @@
this.createValueMenu(rows);
break;
}
+ case 'attachmentStorageType':
+ {
+ let rows = ['stored', 'linked'].map(type => ({
+ name: Zotero.getString('attachment-storage-type-' + type),
+ value: type
+ }));
+
+ this.createValueMenu(rows);
+ break;
+ }
default:
{
if (operatorsList.value == 'isInTheLast') {
@@ -522,7 +533,8 @@
// Drop-down menu
if (this.selectedCondition == 'collection'
|| this.selectedCondition == 'itemType'
- || this.selectedCondition == 'fileTypeID') {
+ || this.selectedCondition == 'fileTypeID'
+ || this.selectedCondition == 'attachmentStorageType') {
this.querySelector('#valuefield').hidden = true;
this.querySelector('#valuemenu').hidden = false;
this.querySelector('#value-date-age').hidden = true;
diff --git a/chrome/content/zotero/xpcom/data/search.js b/chrome/content/zotero/xpcom/data/search.js
index 594d1cbede..eb36d7b2d8 100644
--- a/chrome/content/zotero/xpcom/data/search.js
+++ b/chrome/content/zotero/xpcom/data/search.js
@@ -1424,6 +1424,37 @@ Zotero.Search.prototype._buildQuery = async function () {
}
skipOperators = true;
break;
+
+ case 'attachmentStorageType': {
+ let linkModes;
+ switch (condition.value) {
+ case 'stored':
+ linkModes = [
+ Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
+ Zotero.Attachments.LINK_MODE_IMPORTED_URL
+ ];
+ break;
+
+ case 'linked':
+ linkModes = [
+ Zotero.Attachments.LINK_MODE_LINKED_FILE,
+ Zotero.Attachments.LINK_MODE_LINKED_URL
+ ];
+ break;
+
+ default:
+ throw ("Invalid attachmentStorageType '" + condition.value
+ + "' specified in search.js");
+ }
+ condSQL += 'linkMode ';
+ if (condition.operator == 'isNot') {
+ condSQL += 'NOT ';
+ }
+ condSQL += `IN (${linkModes.map(() => '?').join(', ')})`;
+ condSQLParams.push(...linkModes);
+ skipOperators = true;
+ break;
+ }
case 'tag':
condSQL += "tagID IN (SELECT tagID FROM tags WHERE ";
diff --git a/chrome/content/zotero/xpcom/data/searchConditions.js b/chrome/content/zotero/xpcom/data/searchConditions.js
index abab69709a..aa2ca3c76b 100644
--- a/chrome/content/zotero/xpcom/data/searchConditions.js
+++ b/chrome/content/zotero/xpcom/data/searchConditions.js
@@ -355,6 +355,16 @@ Zotero.SearchConditions = new function () {
table: 'itemAttachments',
field: 'fileTypeID'
},
+
+ {
+ name: 'attachmentStorageType',
+ operators: {
+ is: true,
+ isNot: true
+ },
+ table: 'itemAttachments',
+ field: 'linkMode'
+ },
{
name: 'tagID',
diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl
index 2dab80efec..aece919f61 100644
--- a/chrome/locale/en-US/zotero/zotero.ftl
+++ b/chrome/locale/en-US/zotero/zotero.ftl
@@ -815,6 +815,7 @@ search-conditions-dateModified = Date Modified
search-conditions-fulltextContent = Attachment Content
search-conditions-programmingLanguage = Programming Language
search-conditions-fileTypeID = Attachment File Type
+search-conditions-attachmentStorageType = Attachment Storage Type
search-conditions-lastRead = Attachment Last Read
search-conditions-annotationText = Annotation Text
search-conditions-annotationComment = Annotation Comment
@@ -851,6 +852,9 @@ file-type-presentation = Presentation
file-type-document = Document
file-type-ebook = Ebook
+attachment-storage-type-stored = Stored
+attachment-storage-type-linked = Linked
+
post-upgrade-message = You’ve been upgraded to { -app-name } { $version }! Learn about what’s new.
post-upgrade-remind-me-later =
.label = { general-remind-me-later }
@@ -897,4 +901,3 @@ banner-close-button =
plugins-blocked-plugin =
.message = This plugin has been disabled by { -app-name }.
-
diff --git a/chrome/locale/en-US/zotero/zotero.json b/chrome/locale/en-US/zotero/zotero.json
index 29286ce9eb..b79a21528f 100644
--- a/chrome/locale/en-US/zotero/zotero.json
+++ b/chrome/locale/en-US/zotero/zotero.json
@@ -1073,6 +1073,9 @@
"search-conditions-fileTypeID": {
"string": "Attachment File Type"
},
+ "search-conditions-attachmentStorageType": {
+ "string": "Attachment Storage Type"
+ },
"search-conditions-lastRead": {
"string": "Attachment Last Read"
},
@@ -1127,6 +1130,12 @@
"file-type-ebook": {
"string": "Ebook"
},
+ "attachment-storage-type-stored": {
+ "string": "Stored"
+ },
+ "attachment-storage-type-linked": {
+ "string": "Linked"
+ },
"post-upgrade-message": {
"string": "You’ve been upgraded to { app-name } { version }! Learn about what’s new."
},
diff --git a/test/tests/searchTest.js b/test/tests/searchTest.js
index cc296eb5b5..195d17b81a 100644
--- a/test/tests/searchTest.js
+++ b/test/tests/searchTest.js
@@ -127,6 +127,9 @@ describe("Zotero.Search", function () {
var fooItem;
var foobarItem;
var bazItem;
+ var importedURLItem;
+ var linkedFileItem;
+ var linkedURLItem;
var fooItemGroup;
var foobarItemGroup;
var bazItemGroup;
@@ -141,6 +144,25 @@ describe("Zotero.Search", function () {
foobarItem = await importFileAttachment("search/foobar.html");
bazItem = await importFileAttachment("search/baz.pdf");
userLibraryID = fooItem.libraryID;
+ let testPDF = getTestDataDirectory();
+ testPDF.append('test.pdf');
+ importedURLItem = await Zotero.Attachments.importSnapshotFromFile({
+ file: testPDF,
+ libraryID: userLibraryID,
+ title: 'imported-url-pdf',
+ url: 'http://example.com/imported-url.pdf',
+ contentType: 'application/pdf',
+ singleFile: true
+ });
+ linkedFileItem = await Zotero.Attachments.linkFromFile({
+ file: OS.Path.join(getTestDataDirectory().path, 'test.pdf'),
+ title: 'linked-file-pdf'
+ });
+ linkedURLItem = await Zotero.Attachments.linkFromURL({
+ url: 'http://example.com/linked-url',
+ title: 'linked-url',
+ contentType: 'text/html'
+ });
let group = await getGroup();
fooItemGroup = await importFileAttachment("search/foo.html", { libraryID: group.libraryID });
@@ -152,6 +174,9 @@ describe("Zotero.Search", function () {
yield fooItem.eraseTx();
yield foobarItem.eraseTx();
yield bazItem.eraseTx();
+ yield importedURLItem.eraseTx();
+ yield linkedFileItem.eraseTx();
+ yield linkedURLItem.eraseTx();
yield fooItemGroup.eraseTx();
yield foobarItemGroup.eraseTx();
yield bazItemGroup.eraseTx();
@@ -264,6 +289,42 @@ describe("Zotero.Search", function () {
assert.sameMembers(matches, [fooItem.id, foobarItem.id]);
});
});
+
+ describe("attachmentStorageType", function () {
+ it("should search by stored attachment storage type", async function () {
+ let s = new Zotero.Search();
+ s.libraryID = userLibraryID;
+ s.addCondition('attachmentStorageType', 'is', 'stored');
+ let matches = await s.search();
+ assert.sameMembers(matches, [
+ fooItem.id,
+ foobarItem.id,
+ bazItem.id,
+ importedURLItem.id
+ ]);
+ });
+
+ it("should search by linked attachment storage type", async function () {
+ let s = new Zotero.Search();
+ s.libraryID = userLibraryID;
+ s.addCondition('attachmentStorageType', 'is', 'linked');
+ let matches = await s.search();
+ assert.sameMembers(matches, [linkedFileItem.id, linkedURLItem.id]);
+ });
+
+ it("should support isNot", async function () {
+ let s = new Zotero.Search();
+ s.libraryID = userLibraryID;
+ s.addCondition('attachmentStorageType', 'isNot', 'linked');
+ let matches = await s.search();
+ assert.sameMembers(matches, [
+ fooItem.id,
+ foobarItem.id,
+ bazItem.id,
+ importedURLItem.id
+ ]);
+ });
+ });
describe("fulltextContent", function () {
it("should find text in HTML files", async function () {