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 () {