diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index 431f0e75b1..a7df980cd0 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -225,7 +225,7 @@ var Zotero_File_Interface_Bibliography = new function() { updateLocaleMenu(selectedStyleObj); // - // For integrationDocPrefs.xul and rtfScan.xul + // For integrationDocPrefs.xul and rtfScan.xhtml // if (isDocPrefs) { // update status of displayAs box based on style class diff --git a/chrome/content/zotero/components/progressQueueTable.jsx b/chrome/content/zotero/components/progressQueueTable.jsx index 4b08ee4c44..c010c7639e 100644 --- a/chrome/content/zotero/components/progressQueueTable.jsx +++ b/chrome/content/zotero/components/progressQueueTable.jsx @@ -27,7 +27,7 @@ import PropTypes from 'prop-types'; import { getDOMElement } from 'components/icons'; import VirtualizedTable, { renderCell } from 'components/virtualized-table'; -import { noop } from './utils'; +import { nextHTMLID, noop } from './utils'; function getImageByStatus(status) { @@ -45,8 +45,9 @@ function getImageByStatus(status) { const ProgressQueueTable = ({ onActivate = noop, progressQueue }) => { const treeRef = useRef(null); + const htmlID = useRef(nextHTMLID()); - const getRowCount = useCallback(() => progressQueue.getRows().length, [progressQueue]); + const getRowCount = useCallback(() => progressQueue.getTotal(), [progressQueue]); const rowToTreeItem = useCallback((index, selection, oldDiv = null, columns) => { let rows = progressQueue.getRows(); @@ -92,6 +93,7 @@ const ProgressQueueTable = ({ onActivate = noop, progressQueue }) => { progressQueue.addListener('rowadded', refreshTree); progressQueue.addListener('rowupdated', refreshTree); progressQueue.addListener('rowdeleted', refreshTree); + return () => { progressQueue.removeListener('rowadded', refreshTree); progressQueue.removeListener('rowupdated', refreshTree); @@ -103,7 +105,7 @@ const ProgressQueueTable = ({ onActivate = noop, progressQueue }) => { . ***** END LICENSE BLOCK ***** */ -'use strict'; - const noop = () => {}; + function getDragTargetOrient(event, target) { const elem = target || event.target; const {y, height} = elem.getBoundingClientRect(); @@ -72,9 +71,30 @@ function createDragHandler({ handleDrag, handleDragStop }) { return { start: onDragStart, stop: onDragStop - } + }; } -export { - noop, getDragTargetOrient, createDragHandler +var _htmlID = 1; + +const nextHTMLID = (prefix = 'id-') => prefix + _htmlID++; + +const scrollIntoViewIfNeeded = (element, container, opts = {}) => { + const containerTop = container.scrollTop; + const containerBottom = containerTop + container.clientHeight; + const elementTop = element.offsetTop; + const elementBottom = elementTop + element.clientHeight; + + if (elementTop < containerTop || elementBottom > containerBottom) { + const before = container.scrollTop; + element.scrollIntoView(opts); + const after = container.scrollTop; + return after - before; + } + return 0; +}; + +const stopPropagation = ev => ev.stopPropagation(); + +export { + nextHTMLID, noop, getDragTargetOrient, createDragHandler, scrollIntoViewIfNeeded, stopPropagation }; diff --git a/chrome/content/zotero/elements/base.js b/chrome/content/zotero/elements/base.js index 35ed5839bc..ebe1a5049c 100644 --- a/chrome/content/zotero/elements/base.js +++ b/chrome/content/zotero/elements/base.js @@ -58,6 +58,11 @@ class XULElementBase extends XULElement { shadow.append(content); } + MozXULElement.insertFTLIfNeeded("zotero.ftl"); + if (document.l10n) { + document.l10n.connectRoot(this.shadowRoot); + } + this.init(); } diff --git a/chrome/content/zotero/elements/publicationsLicenseInfo.js b/chrome/content/zotero/elements/publicationsLicenseInfo.js new file mode 100644 index 0000000000..2a325ab871 --- /dev/null +++ b/chrome/content/zotero/elements/publicationsLicenseInfo.js @@ -0,0 +1,153 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2022 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 . + + ***** END LICENSE BLOCK ***** +*/ + +/* global XULElementBase: false */ + +{ + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + Services.scriptloader.loadSubScript("chrome://zotero/content/elements/base.js", this); + + const links = { + cc: 'https://wiki.creativecommons.org/Considerations_for_licensors_and_licensees', + cc0: 'https://wiki.creativecommons.org/CC0_FAQ' + }; + + const getLicenseData = (license) => { + var name, img, url, id; + + switch (license) { + case 'reserved': + url = null; + name = 'All rights reserved'; + img = 'chrome://zotero/skin/licenses/reserved.png'; + id = null; + break; + case 'cc': + url = 'https://creativecommons.org/'; + name = 'Creative Commons'; + img = 'chrome://zotero/skin/licenses/cc-srr.png'; + id = null; + break; + + case 'cc0': + url = "https://creativecommons.org/publicdomain/zero/1.0/"; + name = null; + img = 'chrome://zotero/skin/licenses/' + license + ".svg"; + id = 'licenses-cc-0'; + break; + + default: + url = 'https://creativecommons.org/licenses/' + license.replace(/^cc-/, '') + '/4.0/'; + name = null; + img = 'chrome://zotero/skin/licenses/' + license + ".svg"; + id = `licenses-${license}`; + break; + } + + return { url, name, img, id }; + }; + + const makeLicenseInfo = (url, name, img, id) => { + const licenseInfo = `
` + + (id ? `
` : `
${name}
`); + + return MozXULElement.parseXULToFragment( + url + ? `${licenseInfo}` + : `
${licenseInfo}
` + ); + }; + + const makeLicenseMoreInfo = (license) => { + const needsMoreInfo = license.startsWith('cc') && license !== 'cc'; + const ccType = license === 'cc0' ? 'cc0' : 'cc'; + + return MozXULElement.parseXULToFragment(needsMoreInfo + ? `
+ +
` + : '' + ); + }; + + class PublicationsLicenseInfo extends XULElementBase { + get stylesheets() { + return [ + 'chrome://global/skin/global.css', + 'chrome://zotero/skin/elements/license-info.css' + ]; + } + + content = MozXULElement.parseXULToFragment(` +
+
+ `); + + validLicenses = new Set(['cc', 'cc-by', 'cc-by-sa', 'cc-by-nd', 'cc-by-nc', 'cc-by-nc-sa', 'cc-by-nc-nd', 'cc0', 'reserved']); + + get license() { + return this._license; + } + + set license(val) { + if (!this.validLicenses.has(val)) { + throw new Zotero.Error(`"${val}" is invalid value for attribute "license" in `); + } + this._license = val; + this.update(); + } + + get licenseName() { + return this.shadowRoot.querySelector('.license-name').getAttribute('label') + ? this.shadowRoot.querySelector('.license-name').getAttribute('label') + : this.shadowRoot.querySelector('.license-name').textContent; + } + + async init() { + this.license = this.getAttribute('license'); + this.shadowRoot.getElementById('license-info').addEventListener('click', this.onURLInteract.bind(this)); + this.shadowRoot.getElementById('license-info').addEventListener('keydown', this.onURLInteract.bind(this)); + } + + update() { + const { url, name, img, id } = getLicenseData(this.license); + const licenseInfoEl = makeLicenseInfo(url, name, img, id); + const licenseMoreEl = makeLicenseMoreInfo(this.license); + this.shadowRoot.getElementById('license-info').replaceChildren(licenseInfoEl, licenseMoreEl); + } + + onURLInteract(ev) { + const aEl = ev.target.closest('[href]'); + if (aEl && (ev.type === 'click' || (ev.type === 'keydown' && ev.key === ' '))) { + ev.preventDefault(); + Zotero.launchURL(aEl.getAttribute('href')); + } + } + } + customElements.define('publications-license-info', PublicationsLicenseInfo); +} diff --git a/chrome/content/zotero/elements/styleConfigurator.js b/chrome/content/zotero/elements/styleConfigurator.js new file mode 100644 index 0000000000..3ac1e52b3a --- /dev/null +++ b/chrome/content/zotero/elements/styleConfigurator.js @@ -0,0 +1,262 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2022 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 . + + ***** END LICENSE BLOCK ***** +*/ + +/* global XULElementBase: false */ + +{ + Services.scriptloader.loadSubScript("chrome://zotero/content/elements/base.js", this); + + class StyleSelector extends XULElementBase { + get stylesheets() { + return [ + 'chrome://global/skin/global.css', + 'chrome://zotero/skin/elements/style-configurator.css' + ]; + } + + content = MozXULElement.parseXULToFragment(` +
+
+ +
+
+ `); + + set value(val) { + this.shadowRoot.getElementById('style-list').value = val; + } + + get value() { + return this.shadowRoot.getElementById('style-list').value; + } + + async init() { + await Zotero.Styles.init(); + const styleListEl = this.shadowRoot.getElementById('style-list'); + + Zotero.Styles.getVisible().forEach((so) => { + const value = so.styleID; + // Add acronyms to APA and ASA to avoid confusion + // https://forums.zotero.org/discussion/comment/357135/#Comment_357135 + const label = so.title + .replace(/^American Psychological Association/, "American Psychological Association (APA)") + .replace(/^American Sociological Association/, "American Sociological Association (ASA)"); + + styleListEl.appendChild(MozXULElement.parseXULToFragment(` + ${label} + `)); + }); + this.value = this.getAttribute('value'); + this.shadowRoot.getElementById('style-list').addEventListener("select", () => { + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + } + } + + class LocaleSelector extends XULElementBase { + content = MozXULElement.parseXULToFragment(` +
+
+ + + +
+
+ `); + + get stylesheets() { + return [ + 'chrome://global/skin/global.css', + 'chrome://zotero/skin/elements/style-configurator.css' + ]; + } + + get value() { + return this.localeListEl.value; + } + + set value(val) { + this._value = val; + const styleData = this._style ? Zotero.Styles.get(this._style) : null; + this.localeListEl.value = styleData && styleData.locale || this._value; + } + + get style() { + return this.style; + } + + set style(style) { + this._style = style; + const styleData = style ? Zotero.Styles.get(style) : null; + this.localeListEl.disabled = !style || !!styleData.locale; + this.localeListEl.value = styleData && styleData.locale || this._value || this.fallbackLocale; + } + + connectedCallback() { + super.connectedCallback(); + this.localeListEl = this.shadowRoot.getElementById('locale-list'); + this.localePopupEl = this.shadowRoot.querySelector('#locale-list > menupopup'); + } + + async init() { + this._style = this.getAttribute('style'); + this._value = this.getAttribute('value'); + + await Zotero.Styles.init(); + this.fallbackLocale = Zotero.Styles?.primaryDialects[Zotero.locale] || Zotero.locale; + + const menuLocales = Zotero.Utilities.deepCopy(Zotero.Styles.locales); + const menuLocalesKeys = Object.keys(menuLocales).sort(); + + // Make sure that client locale is always available as a choice + if (this.fallbackLocale && !(this.fallbackLocale in menuLocales)) { + menuLocales[this.fallbackLocale] = this.fallbackLocale; + menuLocalesKeys.unshift(this.fallbackLocale); + } + + menuLocalesKeys.forEach((key) => { + const label = menuLocales[key]; + + this.localePopupEl.appendChild(MozXULElement.parseXULToFragment(` + + `)); + }); + + this.value = this._value; + this.style = this._style; + + this.localeListEl.addEventListener("command", (_event) => { + this._value = this.localeListEl.value; + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + } + } + + class StyleConfigurator extends XULElementBase { + content = MozXULElement.parseXULToFragment(` +
+
+ `); + + get stylesheets() { + return [ + 'chrome://global/skin/global.css', + 'chrome://zotero/skin/elements/style-configurator.css' + ]; + } + + set style(val) { + this.shadowRoot.getElementById('style-selector').value = val; + this.handleStyleChanged(val); + } + + get style() { + return this.shadowRoot.getElementById('style-selector').value; + } + + set locale(val) { + this.shadowRoot.getElementById('locale-selector').value = val; + } + + get locale() { + return this.shadowRoot.getElementById('locale-selector').value; + } + + set displayAs(val) { + this.shadowRoot.getElementById('display-as').value = val; + } + + get displayAs() { + return this.shadowRoot.getElementById('display-as').value; + } + + async init() { + this.shadowRoot.getElementById('style-configurator').style.display = 'none'; + await Zotero.Styles.init(); + this.shadowRoot.getElementById('style-configurator').style.display = ''; + this.shadowRoot.getElementById('style-selector').addEventListener('select', (_event) => { + this.handleStyleChanged(_event.target.value); + + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + + this.shadowRoot.getElementById('locale-selector').addEventListener('select', (_event) => { + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + + this.shadowRoot.getElementById('display-as').addEventListener('select', (_event) => { + const event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + }); + } + + handleStyleChanged(style) { + this.shadowRoot.getElementById('locale-selector').style = style; + const styleData = style ? Zotero.Styles.get(style) : null; + const isNoteStyle = (styleData || {}).class === 'note'; + this.shadowRoot.getElementById('display-as-wrapper').style.display = isNoteStyle ? '' : 'none'; + } + } + + customElements.define('locale-selector', LocaleSelector); + customElements.define('style-selector', StyleSelector); + customElements.define('style-configurator', StyleConfigurator); +} diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js index 86b9ca2a9b..3db3587504 100644 --- a/chrome/content/zotero/fileInterface.js +++ b/chrome/content/zotero/fileInterface.js @@ -389,7 +389,7 @@ var Zotero_File_Interface = new function() { }; args.wrappedJSObject = args; - Services.ww.openWindow(null, "chrome://zotero/content/import/importWizard.xul", + Services.ww.openWindow(null, "chrome://zotero/content/import/importWizard.xhtml", "importFile", "chrome,dialog=yes,centerscreen,width=600,height=400,modal", args); }; @@ -448,6 +448,15 @@ var Zotero_File_Interface = new function() { translation.createNewCollection = createNewCollection; translation.mendeleyCode = options.mendeleyCode; } + else if (options.folder) { + Components.utils.import("chrome://zotero/content/import/folderImport.js"); + translation = new Zotero_Import_Folder({ + folder: options.folder, + recreateStructure: options.recreateStructure, + fileTypes: options.fileTypes, + mimeTypes: options.mimeTypes, + }); + } else { // Check if the file is an SQLite database var sample = yield Zotero.File.getSample(file.path); @@ -976,71 +985,6 @@ var Zotero_File_Interface = new function() { } } - this.authenticateMendeleyOnlinePoll = function (win) { - if (win && win[0] && win[0].location) { - const matchResult = win[0].location.toString().match(/mendeley_oauth_redirect.html(?:.*?)(?:\?|&)code=(.*?)(?:&|$)/i); - if (matchResult) { - const mendeleyCode = matchResult[1]; - Zotero.getMainWindow().setTimeout(() => this.showImportWizard({ mendeleyCode }), 0); - - // Clear all cookies to remove access - // - // This includes unrelated cookies in the central cookie store, but that's fine for - // the moment, since we're not purposely using cookies for anything else. - // - // TODO: Switch to removeAllSince() once >Fx60 - try { - Cc["@mozilla.org/cookiemanager;1"] - .getService(Ci.nsICookieManager) - .removeAll(); - } - catch (e) { - Zotero.logError(e); - } - - win.close(); - return; - } - } - - if (win && !win.closed) { - Zotero.getMainWindow().setTimeout(this.authenticateMendeleyOnlinePoll.bind(this, win), 200); - } - }; - - this.authenticateMendeleyOnline = function () { - const uri = `https://api.mendeley.com/oauth/authorize?client_id=5907&redirect_uri=https%3A%2F%2Fzotero-static.s3.amazonaws.com%2Fmendeley_oauth_redirect.html&response_type=code&state=&scope=all`; - var win = Services.wm.getMostRecentWindow("zotero:basicViewer"); - if (win) { - win.loadURI(uri); - } - else { - const ww = Services.ww; - const arg = Components.classes["@mozilla.org/supports-string;1"] - .createInstance(Components.interfaces.nsISupportsString); - arg.data = uri; - win = ww.openWindow(null, "chrome://zotero/content/standalone/basicViewer.xhtml", - "basicViewer", "chrome,dialog=yes,resizable,centerscreen,menubar,scrollbars", arg); - } - - let browser; - let func = function () { - win.removeEventListener("load", func); - browser = win.document.documentElement.getElementsByTagName('browser')[0]; - browser.addEventListener("pageshow", innerFunc); - }; - let innerFunc = function () { - browser.removeEventListener("pageshow", innerFunc); - win.outerWidth = Math.max(640, Math.min(1000, win.screen.availHeight)); - win.outerHeight = Math.max(480, Math.min(800, win.screen.availWidth)); - }; - - win.addEventListener("load", func); - - // polling executed by the main window because current (wizard) window will be closed - Zotero.getMainWindow().setTimeout(this.authenticateMendeleyOnlinePoll.bind(this, win), 200); - }; - /** * Generate an error string reporting a translation failure. Includes the * label of the running translator if available. diff --git a/chrome/content/zotero/import/folderImport.js b/chrome/content/zotero/import/folderImport.js new file mode 100644 index 0000000000..26f7c6ac16 --- /dev/null +++ b/chrome/content/zotero/import/folderImport.js @@ -0,0 +1,247 @@ +var EXPORTED_SYMBOLS = ["Zotero_Import_Folder"]; // eslint-disable-line no-unused-vars + +Components.utils.import("resource://gre/modules/Services.jsm"); +Services.scriptloader.loadSubScript("chrome://zotero/content/include.js"); + +// matches "*" and "?" wildcards of a glob pattern, case-insensitive +function simpleGlobMatch(filename, patterns) { + for (const pattern of patterns) { + // Convert glob pattern to regex pattern + const regexPattern = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex characters + .replace(/\*/g, '.*') // Replace * with regex equivalent + .replace(/\?/g, '.'); // Replace ? with regex equivalent + + if (new RegExp(`^${regexPattern}$`, 'i').test(filename)) { + return true; + } + } + return false; +} + +const collectFilesRecursive = async (dirPath, parents = [], files = []) => { + await Zotero.File.iterateDirectory(dirPath, async ({ isDir, _isSymlink, name, path }) => { + if (isDir) { + await collectFilesRecursive(path, [...parents, name], files); + } + // TODO: Also check for hidden file attribute on windows? + else if (!name.startsWith('.')) { + files.push({ parents, path, name }); + } + }); + return files; +}; + +const findCollection = (libraryID, parentCollectionID, collectionName) => { + const collections = parentCollectionID + ? Zotero.Collections.getByParent(parentCollectionID) + : Zotero.Collections.getByLibrary(libraryID); + + return collections.find(c => c.name === collectionName); +}; + +// @TODO +const findItemByHash = async (libraryID, hash) => { + return null; +}; + +class Zotero_Import_Folder { // eslint-disable-line camelcase,no-unused-vars + constructor({ mimeTypes = ['application/pdf'], fileTypes, folder, libraryID, recreateStructure }) { + this.folder = folder; + this.libraryID = libraryID; + this.newItems = []; + this.recreateStructure = recreateStructure; + this.fileTypes = fileTypes && fileTypes.length ? fileTypes.split(',').map(ft => ft.trim()) : []; + this._progress = 0; + this._progressMax = 0; + this._itemDone = () => {}; + this.types = mimeTypes; // whitelist of mime types to process + } + + setLocation(folder) { + this.folder = folder; + } + + setHandler(name, handler) { + switch (name) { + case 'itemDone': + this._itemDone = handler; + break; + } + } + + setTranslator() {} + + getProgress() { + return this._progress / this._progressMax * 100; + } + + async getTranslators() { + return [{ label: 'Folder import' }]; + } + + async translate({ collections = [], linkFiles = false } = {}) { + // https://github.com/zotero/zotero/pull/2862#discussion_r1141324302 + throw new Error('Folder import is not supported yet'); + const libraryID = this.libraryID || Zotero.Libraries.userLibraryID; + const files = await collectFilesRecursive(this.folder); + + // import is done in four phases: sniff for mime type, calculate md5, import as attachment, recognize. + // hence number of files is multiplied by 4 to determine max progress + this._progressMax = files.length * 4; + + const mimeTypes = await Promise.all(files.map( + async ({ path }) => { + const mimeType = Zotero.MIME.sniffForMIMEType(await Zotero.File.getSample(path)); + this._progress++; + this._itemDone(); + return mimeType; + } + )); + + const fileHashes = await Promise.all(files.map( + async ({ name, path }, index) => { + const contentType = mimeTypes[index]; + this._progress++; + if (!(this.types.includes(contentType) || simpleGlobMatch(name, this.fileTypes))) { + // don't bother calculating a hash for file that will be ignored + return null; + } + const md5Hash = await Zotero.Utilities.Internal.md5Async(path); + this._itemDone(); + return md5Hash; + } + )); + + files.forEach((fileData, index) => { + fileData.parentCollectionIDs = (collections && collections.length) ? [...collections] : []; + fileData.mimeType = mimeTypes[index]; + }); + + if (this.recreateStructure) { + for (const fileData of files) { + const { parents } = fileData; + let prevParentCollectionID = null; + if (parents.length) { + prevParentCollectionID = (collections && collections.length) ? collections[0] : null; + for (const parentName of parents) { + const parentCollection = findCollection(libraryID, prevParentCollectionID, parentName) || new Zotero.Collection; + parentCollection.libraryID = libraryID; + parentCollection.name = parentName; + if (prevParentCollectionID) { + parentCollection.parentID = prevParentCollectionID; + } + await parentCollection.saveTx({ skipSelect: true }); //eslint-disable-line no-await-in-loop + prevParentCollectionID = parentCollection.id; + } + } + if (prevParentCollectionID) { + fileData.parentCollectionIDs = [prevParentCollectionID]; + } + } + } + + // index files by hash to avoid importing duplicate files. Keep track of where duplicates were found so that + // duplicate item is still added to one collection per folder + const fileDataByHash = {}; + files.forEach((fileData, index) => { + const hash = fileHashes[index]; + if (hash in fileDataByHash) { + fileDataByHash[hash].parentCollectionIDs.push(...fileData.parentCollectionIDs); + } + else { + fileDataByHash[hash] = fileData; + } + }); + + // advance progress to account for duplicates found within file structure + // these files won't be imported nor recognized so advance 2 ticks per file + this._progress += 2 * (files.length - Object.keys(fileDataByHash).length); + this._itemDone(); + + const attachmentItemHashLookup = {}; + const attachmentItems = await Promise.all(Object.entries(fileDataByHash).map( + async ([hash, { name, path, parentCollectionIDs, mimeType }]) => { + const options = { + collections: parentCollectionIDs, + contentType: mimeType, + file: path, + libraryID, + }; + + let attachmentItem = null; + + if ((this.types.includes(mimeType) || simpleGlobMatch(name, this.fileTypes))) { + const existingItem = await findItemByHash(libraryID, hash); + + if (existingItem) { + existingItem.setCollections([...existingItem.getCollections(), ...parentCollectionIDs]); + await existingItem.saveTx({ skipSelect: true }); + } + else { + if (linkFiles) { + attachmentItem = await Zotero.Attachments.linkFromFile(options); + } + else { + attachmentItem = await Zotero.Attachments.importFromFile(options); + } + + this.newItems.push(attachmentItem); + attachmentItemHashLookup[attachmentItem.id] = hash; + } + } + + if (attachmentItem && !Zotero.RecognizePDF.canRecognize(attachmentItem)) { + // @TODO: store hash of an item that cannot be recognized + await attachmentItem.saveTx({ skipSelect: true }); + attachmentItem = null; + } + this._progress++; + this._itemDone(); + return attachmentItem; + } + )); + + + // discard unrecognizable items, increase progress for discarded items + const recognizableItems = attachmentItems.filter(item => item !== null); + this._progress += attachmentItems.length - recognizableItems.length; + this._itemDone(); + + const recognizeQueue = Zotero.ProgressQueues.get('recognize'); + const itemsToSavePostRecognize = []; + + const processRecognizedItem = ({ status, id }) => { + const updatedItem = recognizableItems.find(i => i.id === id); + if (status === Zotero.ProgressQueue.ROW_SUCCEEDED) { + const recognizedItem = updatedItem.parentItem; + if (recognizedItem && id in attachmentItemHashLookup) { + // @TODO: Store hash of an attachment (attachmentItemHashLookup[id]) for this recognized item + itemsToSavePostRecognize.push(recognizedItem); + } + } + if (status === Zotero.ProgressQueue.ROW_FAILED) { + if (updatedItem && id in attachmentItemHashLookup) { + // @TODO: Store hash of a file that failed to be recognized (attachmentItemHashLookup[id]) + itemsToSavePostRecognize.push(updatedItem); + } + } + if ([Zotero.ProgressQueue.ROW_FAILED, Zotero.ProgressQueue.ROW_SUCCEEDED].includes(status)) { + this._progress++; + this._itemDone(); + } + }; + + recognizeQueue.addListener('rowupdated', processRecognizedItem); + try { + await Zotero.RecognizePDF.recognizeItems(recognizableItems); + } + finally { + recognizeQueue.removeListener('rowupdated', processRecognizedItem); + } + + for (const item of itemsToSavePostRecognize) { + await item.saveTx({ skipSelect: true }); + } + } +} diff --git a/chrome/content/zotero/import/importWizard.js b/chrome/content/zotero/import/importWizard.js index 48b1c83cb2..28ea2181d5 100644 --- a/chrome/content/zotero/import/importWizard.js +++ b/chrome/content/zotero/import/importWizard.js @@ -1,211 +1,249 @@ -import FilePicker from 'zotero/modules/filePicker'; +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2022 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 . + + ***** END LICENSE BLOCK ***** +*/ -var Zotero_Import_Wizard = { - _wizard: null, - _dbs: null, - _file: null, - _translation: null, - _mendeleyOnlineRedirectURLWithCode: null, - _mendeleyCode: null, - - - init: async function () { - this._wizard = document.getElementById('import-wizard'); - var dbs = await Zotero_File_Interface.findMendeleyDatabases(); - if (dbs.length) { - // Local import disabled - //document.getElementById('radio-import-source-mendeley').hidden = false; +import FilePicker from 'zotero/filePicker'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import ProgressQueueTable from 'components/progressQueueTable'; + +/* eslint camelcase: ["error", {allow: ["Zotero_File_Interface", "Zotero_Import_Wizard"]} ] */ +/* global Zotero_File_Interface: false */ + + +const Zotero_Import_Wizard = { // eslint-disable-line no-unused-vars + wizard: null, + folder: null, + file: null, + mendeleyCode: null, + libraryID: null, + translation: null, + + async getShouldCreateCollection() { + const sql = "SELECT ROWID FROM collections WHERE libraryID=?1 " + + "UNION " + + "SELECT ROWID FROM items WHERE libraryID=?1 " + // Not in trash + + "AND itemID NOT IN (SELECT itemID FROM deletedItems) " + // And not a child item (which doesn't necessarily show up in the trash) + + "AND itemID NOT IN (SELECT itemID FROM itemNotes WHERE parentItemID IS NOT NULL) " + + "AND itemID NOT IN (SELECT itemID FROM itemAttachments WHERE parentItemID IS NOT NULL) " + + "LIMIT 1"; + return Zotero.DB.valueQueryAsync(sql, this.libraryID); + }, + + async init() { + const { mendeleyCode, libraryID } = window.arguments[0].wrappedJSObject ?? {}; + + this.libraryID = libraryID; + this.mendeleyCode = mendeleyCode; + + this.wizard = document.getElementById('import-wizard'); + this.wizard.getPageById('page-start') + .addEventListener('pageadvanced', this.onImportSourceAdvance.bind(this)); + this.wizard.getPageById('page-mendeley-online-intro') + .addEventListener('pagerewound', this.goToStart.bind(this)); + this.wizard.getPageById('page-mendeley-online-intro') + .addEventListener('pageadvanced', this.openMendeleyAuthWindow.bind(this)); + this.wizard.getPageById('page-options') + .addEventListener('pageshow', this.onOptionsPageShow.bind(this)); + this.wizard.getPageById('page-options') + .addEventListener('pageadvanced', this.startImport.bind(this)); + this.wizard.getPageById('page-progress') + .addEventListener('pageshow', this.onProgressPageShow.bind(this)); + + document + .getElementById('other-files') + .addEventListener('keyup', (ev) => { + document.getElementById('import-other').checked = ev.currentTarget.value.length > 0; + }); + document + .querySelector('#page-done-error-mendeley > a') + .addEventListener('click', this.onURLInteract.bind(this)); + document + .querySelector('#page-done-error-mendeley > a') + .addEventListener('keydown', this.onURLInteract.bind(this)); + document + .querySelector('#page-done-error > button') + .addEventListener('click', this.onReportErrorInteract.bind(this)); + document + .querySelector('#page-done-error > button') + .addEventListener('keydown', this.onReportErrorInteract.bind(this)); + + this.wizard.addEventListener('pageshow', this.updateFocus.bind(this)); + this.wizard.addEventListener('wizardcancel', this.onCancel.bind(this)); + + const shouldCreateCollection = await this.getShouldCreateCollection(); + document.getElementById('create-collection').checked = shouldCreateCollection; + + // wizard.shadowRoot content isn't exposed to our css + this.wizard.shadowRoot + .querySelector('.wizard-header-label').style.fontSize = '16px'; + + if (mendeleyCode) { + this.wizard.goTo('page-options'); } - - // If no existing collections or non-trash items in the library, don't create a new - // collection by default - var args = window.arguments[0].wrappedJSObject; - if (args && args.libraryID) { - let sql = "SELECT ROWID FROM collections WHERE libraryID=?1 " - + "UNION " - + "SELECT ROWID FROM items WHERE libraryID=?1 " - // Not in trash - + "AND itemID NOT IN (SELECT itemID FROM deletedItems) " - // And not a child item (which doesn't necessarily show up in the trash) - + "AND itemID NOT IN (SELECT itemID FROM itemNotes WHERE parentItemID IS NOT NULL) " - + "AND itemID NOT IN (SELECT itemID FROM itemAttachments WHERE parentItemID IS NOT NULL) " - + "LIMIT 1"; - if (!await Zotero.DB.valueQueryAsync(sql, args.libraryID)) { - document.getElementById('create-collection-checkbox').removeAttribute('checked'); + }, + + skipToDonePage(label, description, showReportErrorButton = false, isMendeleyError = false) { + this.wizard.getPageById('page-done').dataset.headerLabelId = label; + + if (!isMendeleyError) { + if (Array.isArray(description)) { + document.getElementById('page-done-description').dataset.l10nId = description[0]; + document.getElementById('page-done-description').dataset.l10nArgs = JSON.stringify(description[1]); + } + else { + document.getElementById('page-done-description').dataset.l10nId = description; } } - if (args && args.mendeleyCode) { - this._mendeleyCode = args.mendeleyCode; - this._wizard.goTo('page-options'); - } - - // Update labels - document.getElementById('radio-import-source-mendeley-online').label - = `Mendeley Reference Manager (${Zotero.getString('import.onlineImport')})`; - document.getElementById('radio-import-source-mendeley').label - = `Mendeley Desktop (${Zotero.getString('import.localImport')})`; - document.getElementById('file-handling-store').label = Zotero.getString( - 'import.fileHandling.store', - Zotero.appName - ); - document.getElementById('file-handling-link').label = Zotero.getString('import.fileHandling.link'); - document.getElementById('file-handling-description').textContent = Zotero.getString( - 'import.fileHandling.description', - Zotero.appName - ); - - Zotero.Translators.init(); // async - }, + document.getElementById('page-done-error-mendeley').style.display = isMendeleyError ? 'block' : 'none'; + document.getElementById('page-done-error').style.display = showReportErrorButton ? 'block' : 'none'; - onCancel: function () { - if (this._translation && this._translation.interrupt) { - this._translation.interrupt(); - } - }, - - onModeChosen: async function () { - var wizard = this._wizard; + const doneQueueContainer = document.getElementById('done-queue-container'); + const doneQueue = document.getElementById('done-queue'); - var mode = document.getElementById('import-source').selectedItem.id; - try { - switch (mode) { - case 'radio-import-source-file': - await this.chooseFile(); - break; - - case 'radio-import-source-mendeley-online': - wizard.goTo('mendeley-online-explanation'); - wizard.canRewind = true; - break; - - case 'radio-import-source-mendeley': - this._dbs = await Zotero_File_Interface.findMendeleyDatabases(); - // This shouldn't happen, because we only show the wizard if there are databases - if (!this._dbs.length) { - throw new Error("No databases found"); - } - this._populateFileList(this._dbs); - document.getElementById('file-options-header').textContent - = Zotero.getString('fileInterface.chooseAppDatabaseToImport', 'Mendeley') - wizard.goTo('page-file-list'); - wizard.canRewind = true; - this._enableCancel(); - break; - - default: - throw new Error(`Unknown mode ${mode}`); - } - } - catch (e) { - this._onDone( - Zotero.getString('general.error'), - Zotero.getString('fileInterface.importError'), - true + if (this.folder && !showReportErrorButton) { + doneQueueContainer.style.display = 'flex'; + ReactDOM.render( + , + doneQueue ); - throw e; } + else { + doneQueueContainer.style.display = 'none'; + } + + this.wizard.goTo('page-done'); + this.wizard.canRewind = false; }, - onMendeleyOnlineShow: async function () { - document.getElementById('mendeley-online-description').textContent = Zotero.getString( - 'import.online.intro', [Zotero.appName, 'Mendeley Reference Manager', 'Mendeley'] - ); - document.getElementById('mendeley-online-description2').textContent = Zotero.getString( - 'import.online.intro2', [Zotero.appName, 'Mendeley'] - ); + goToStart() { + this.wizard.goTo('page-start'); + this.wizard.canAdvance = true; }, - onMendeleyOnlineAdvance: function () { - if (!this._mendeleyOnlineRedirectURLWithCode) { - Zotero_File_Interface.authenticateMendeleyOnline(); - window.close(); - } - }, - - goToStart: function () { - this._wizard.goTo('page-start'); - this._wizard.canAdvance = true; - return false; - }, - - - chooseFile: async function (translation) { - var translation = new Zotero.Translate.Import(); - var translators = await translation.getTranslators(); - var fp = new FilePicker(); + async chooseFile() { + const translation = new Zotero.Translate.Import(); + const translators = await translation.getTranslators(); + const fp = new FilePicker(); fp.init(window, Zotero.getString("fileInterface.import"), fp.modeOpen); - fp.appendFilters(fp.filterAll); - var collation = Zotero.getLocaleCollation(); - + // Add Mendeley DB, which isn't a translator - var mendeleyFilter = { + const mendeleyFilter = { label: "Mendeley Database", // TODO: Localize target: "*.sqlite" }; - var filters = [...translators]; + const filters = [...translators]; filters.push(mendeleyFilter); - + filters.sort((a, b) => collation.compareString(1, a.label, b.label)); for (let filter of filters) { fp.appendFilter(filter.label, "*." + filter.target); } - - var rv = await fp.show(); + + const rv = await fp.show(); if (rv !== fp.returnOK && rv !== fp.returnReplace) { - return false; + return; } - + Zotero.debug(`File is ${fp.file}`); - - this._file = fp.file; - this._wizard.canAdvance = true; - this._wizard.goTo('page-options'); + this.file = fp.file; + this.wizard.canAdvance = true; + this.wizard.goTo('page-options'); }, - - - /** - * When a file is clicked on in the file list - */ - onFileSelected: async function () { - var index = document.getElementById('file-list').selectedIndex; - if (index != -1) { - this._file = this._dbs[index].path; - this._wizard.canAdvance = true; + + async chooseFolder() { + const fp = new FilePicker(); + fp.init(window, Zotero.getString('attachmentBasePath.selectDir'), fp.modeGetFolder); + fp.appendFilters(fp.filterAll); + + const rv = await fp.show(); + if (rv !== fp.returnOK && rv !== fp.returnReplace) { + return; } - else { - this._file = null; - this._wizard.canAdvance = false; + + Zotero.debug(`Folder is ${fp.file}`); + + this.folder = fp.file; + this.wizard.canAdvance = true; + this.wizard.goTo('page-options'); + }, + + async onImportSourceAdvance(ev) { + const selectedMode = document.getElementById('import-source-group').selectedItem.value; + ev.preventDefault(); + ev.stopPropagation(); + try { + switch (selectedMode) { + case 'file': + this.folder = null; + await this.chooseFile(); + break; + case 'folder': + this.file = null; + await this.chooseFolder(); + break; + case 'mendeleyOnline': + this.file = null; + this.folder = null; + this.wizard.goTo('page-mendeley-online-intro'); + this.wizard.canRewind = true; + break; + default: + throw new Error(`Unknown mode ${selectedMode}`); + } + } + catch (e) { + this.skipToDonePage('general-error', 'file-interface-import-error', true); + throw e; } }, - - - /** - * When the user clicks "Other…" to choose a file not in the list - */ - chooseMendeleyDB: async function () { - document.getElementById('file-list').selectedIndex = -1; - var fp = new FilePicker(); - fp.init(window, Zotero.getString('fileInterface.import'), fp.modeOpen); - fp.appendFilter("Mendeley Database", "*.sqlite"); // TODO: Localize - var rv = await fp.show(); - if (rv != fp.returnOK) { - return false; - } - this._file = fp.file; - this._wizard.canAdvance = true; - this._wizard.advance(); + + onOptionsPageShow() { + document.getElementById('page-options-folder-import').style.display = this.folder ? 'block' : 'none'; + document.getElementById('page-options-file-handling').style.display = this.mendeleyCode ? 'none' : 'block'; + this.wizard.canRewind = false; }, - - - onOptionsShown: function () { - document.getElementById('file-handling-options').hidden = !!this._mendeleyCode; + + openMendeleyAuthWindow(ev) { + ev.preventDefault(); + + const arg = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + arg.data = 'mendeleyImport'; + + window.close(); + + Services.ww.openWindow(null, "chrome://zotero/content/standalone/basicViewer.xhtml", + "basicViewer", "chrome,dialog=yes,centerscreen,width=1000,height=700,modal", arg); }, - - - onBeforeImport: async function (translation) { + + async onBeforeImport(translation) { // Unrecognized translator if (!translation) { // Allow error dialog to be displayed, and then close window @@ -214,159 +252,112 @@ var Zotero_Import_Wizard = { }); return; } - - this._translation = translation; - + + this.translation = translation; + // Switch to progress pane - this._wizard.goTo('page-progress'); - var pm = document.getElementById('import-progressmeter'); - - translation.setHandler('itemDone', function () { - pm.value = translation.getProgress(); + this.wizard.goTo('page-progress'); + translation.setHandler('itemDone', () => { + document.getElementById('import-progress').value = translation.getProgress(); }); }, - - - onImportStart: async function () { - if (!this._file && !this._mendeleyCode) { - let index = document.getElementById('file-list').selectedIndex; - this._file = this._dbs[index].path; + + async onProgressPageShow() { + this.wizard.canAdvance = false; + this.wizard.canRewind = false; + const progressQueueContainer = document.getElementById('progress-queue-container'); + const progressQueue = document.getElementById('progress-queue'); + if (this.folder) { + progressQueueContainer.style.display = 'flex'; + ReactDOM.render( + , + progressQueue + ); } - this._disableCancel(); - this._wizard.canRewind = false; - this._wizard.canAdvance = false; + else { + progressQueueContainer.style.display = 'none'; + } + }, + + onURLInteract(ev) { + if (ev.type === 'click' || (ev.type === 'keydown' && ev.key === ' ')) { + Zotero.launchURL(ev.currentTarget.getAttribute('href')); + window.close(); + ev.preventDefault(); + } + }, + + onReportErrorInteract(ev) { + if (ev.type === 'click' || (ev.type === 'keydown' && ev.key === ' ')) { + Zotero.getActiveZoteroPane().reportErrors(); + window.close(); + } + }, + + onCancel() { + if (this.translation && this.translation.interrupt) { + this.translation.interrupt(); + } + }, + + updateFocus() { + (this.wizard.currentPage.querySelector('radiogroup:not([disabled]),checkbox:not([disabled])') ?? this.wizard.currentPage).focus(); + }, + + async startImport() { + this.wizard.canAdvance = false; + this.wizard.canRewind = false; + + const linkFiles = document.getElementById('file-handling').selectedItem.id === 'link'; + const recreateStructure = document.getElementById('recreate-structure').checked; + const shouldCreateCollection = document.getElementById('create-collection').checked; + const mimeTypes = document.getElementById('import-pdf').checked + ? ['application/pdf'] + : []; + const fileTypes = document.getElementById('import-other').checked + ? document.getElementById('other-files').value + : null; try { - let result = await Zotero_File_Interface.importFile({ - file: this._file, + const result = await Zotero_File_Interface.importFile({ + addToLibraryRoot: !shouldCreateCollection, + file: this.file, + fileTypes, + folder: this.folder, + linkFiles, + mendeleyCode: this.mendeleyCode, + mimeTypes, onBeforeImport: this.onBeforeImport.bind(this), - addToLibraryRoot: !document.getElementById('create-collection-checkbox') - .hasAttribute('checked'), - linkFiles: document.getElementById('file-handling-radio').selectedIndex == 1, - mendeleyCode: this._mendeleyCode + recreateStructure }); - + // Cancelled by user or due to error if (!result) { window.close(); return; } - - let numItems = this._translation.newItems.length; - this._onDone( - Zotero.getString('fileInterface.importComplete'), - Zotero.getString(`fileInterface.itemsWereImported`, numItems, numItems) + + const numItems = this.translation.newItems.length; + this.skipToDonePage( + 'file-interface-import-complete', + ['file-interface-items-were-imported', { numItems }] ); } catch (e) { if (e.message == 'Encrypted Mendeley database') { - let url = 'https://www.zotero.org/support/kb/mendeley_import'; - let elem = document.createElement('div'); - elem.innerHTML = `The selected Mendeley database cannot be read, likely because it ` - + `is encrypted. See
How do I import a ` - + `Mendeley library into Zotero? for more information.` - this._onDone(Zotero.getString('general.error'), elem); + this.skipToDonePage('general.error', [], false, true); } else { - this._onDone( - Zotero.getString('general.error'), - Zotero_File_Interface.makeImportErrorString(this._translation), + const translatorLabel = this.translation?.translator?.[0]?.label; + this.skipToDonePage( + 'general.error', + translatorLabel + ? ['file-interface-import-error-translator', { translator: translatorLabel }] + : 'file-interface-import-error', true ); } throw e; } }, - - - reportError: function () { - Zotero.getActiveZoteroPane().reportErrors(); - window.close(); - }, - - - _populateFileList: async function (files) { - var listbox = document.getElementById('file-list'); - - // Remove existing entries - var items = listbox.getElementsByTagName('listitem'); - for (let item of items) { - listbox.removeChild(item); - } - - for (let file of files) { - let li = document.createElement('listitem'); - - let name = document.createElement('listcell'); - // Simply filenames - let nameStr = file.name - .replace(/\.sqlite$/, '') - .replace(/@www\.mendeley\.com$/, ''); - if (nameStr == 'online') { - nameStr = Zotero.getString('dataDir.default', 'online.sqlite'); - } - name.setAttribute('label', nameStr + ' '); - li.appendChild(name); - - let lastModified = document.createElement('listcell'); - lastModified.setAttribute('label', file.lastModified.toLocaleString() + ' '); - li.appendChild(lastModified); - - let size = document.createElement('listcell'); - size.setAttribute( - 'label', - Zotero.getString('general.nMegabytes', (file.size / 1024 / 1024).toFixed(1)) + ' ' - ); - li.appendChild(size); - - listbox.appendChild(li); - } - - if (files.length == 1) { - listbox.selectedIndex = 0; - } - }, - - - _enableCancel: function () { - this._wizard.getButton('cancel').disabled = false; - }, - - - _disableCancel: function () { - this._wizard.getButton('cancel').disabled = true; - }, - - - _onDone: function (label, description, showReportErrorButton) { - var wizard = this._wizard; - wizard.getPageById('page-done').setAttribute('label', label); - - var xulElem = document.getElementById('result-description'); - var htmlElem = document.getElementById('result-description-html'); - - if (description instanceof HTMLElement) { - htmlElem.appendChild(description); - Zotero.Utilities.Internal.updateHTMLInXUL(htmlElem, { callback: () => window.close() }); - xulElem.hidden = true; - htmlElem.setAttribute('display', 'block'); - } - else { - xulElem.textContent = description; - xulElem.hidden = false; - htmlElem.setAttribute('display', 'none'); - } - document.getElementById('result-description'); - - if (showReportErrorButton) { - let button = document.getElementById('result-report-error'); - button.setAttribute('label', Zotero.getString('errorReport.reportError')); - button.hidden = false; - } - - // When done, move to last page and allow closing - wizard.canAdvance = true; - wizard.goTo('page-done'); - wizard.canRewind = false; - } }; diff --git a/chrome/content/zotero/import/importWizard.xhtml b/chrome/content/zotero/import/importWizard.xhtml new file mode 100644 index 0000000000..e25452794c --- /dev/null +++ b/chrome/content/zotero/import/importWizard.xhtml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + @@ -165,6 +170,13 @@ - + + + diff --git a/chrome/content/zotero/xpcom/data/relations.js b/chrome/content/zotero/xpcom/data/relations.js index c830091fb1..ab14cd38fe 100644 --- a/chrome/content/zotero/xpcom/data/relations.js +++ b/chrome/content/zotero/xpcom/data/relations.js @@ -33,7 +33,8 @@ Zotero.Relations = new function () { this._namespaces = { dc: 'http://purl.org/dc/elements/1.1/', owl: 'http://www.w3.org/2002/07/owl#', - mendeleyDB: 'http://zotero.org/namespaces/mendeleyDB#' + mendeleyDB: 'http://zotero.org/namespaces/mendeleyDB#', + zotero: 'http://zotero.org/namespaces/zotero' }; var _types = ['collection', 'item']; diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index c4f6f2882a..8a86bd5f14 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -4752,7 +4752,7 @@ var ZoteroPane = new function() } } io.hasRights = allItemsHaveRights ? 'all' : (noItemsHaveRights ? 'none' : 'some'); - window.openDialog('chrome://zotero/content/publicationsDialog.xul','','chrome,modal', io); + window.openDialog('chrome://zotero/content/publicationsDialog.xhtml','','chrome,modal', io); return io.license ? io : false; }; diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml index 351dcb52fc..686ecf8bc2 100644 --- a/chrome/content/zotero/zoteroPane.xhtml +++ b/chrome/content/zotero/zoteroPane.xhtml @@ -702,7 +702,7 @@ oncommand="ZoteroPane_Local.copySelectedItemsToClipboard();" disabled="true"/> - + diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd index e8e6b3de3e..abec546bbb 100644 --- a/chrome/locale/en-US/zotero/zotero.dtd +++ b/chrome/locale/en-US/zotero/zotero.dtd @@ -180,6 +180,8 @@ + + diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl new file mode 100644 index 0000000000..309ab55edd --- /dev/null +++ b/chrome/locale/en-US/zotero/zotero.ftl @@ -0,0 +1,178 @@ +-app-name = Zotero + +import-wizard = + .title = "Import" + +import-where-from = Where do you want to import from? +import-online-intro-title = Introduction + +import-source-file = + .label = A file (BibTeX, RIS, Zotero RDF, etc.) + +import-source-folder = + .label = A folder of PDFs or other files + +import-source-online = + .label = { $targetApp } online import + +import-options = Options +import-importing = Importing… + +import-create-collection = + .label = Place imported collections and items into new collection + +import-recreate-structure = + .label = Recreate folder structure as collections + +import-fileTypes-header = File Types to Import: + +import-fileTypes-pdf = + .label = PDFs + +import-fileTypes-other = + .placeholder = Other files by pattern, comma-separated (e.g., *.jpg,*.png) + +import-file-handling = File Handling +import-file-handling-store = + .label = Copy files to the { -app-name } storage folder +import-file-handling-link = + .label = Link to files in original location +import-fileHandling-description = Linked files cannot be synced by { -app-name }. + +general-error = Error +file-interface-import-error = An error occurred while trying to import the selected file. Please ensure that the file is valid and try again. +file-interface-import-complete = Import Complete +file-interface-items-were-imported = { $numItems -> + [one] item was imported + *[other] { $numItems } items were imported + } + +import-mendeley-encrypted = The selected Mendeley database cannot be read, likely because it is encrypted. + See How do I import a Mendeley library into Zotero? for more information. + +file-interface-import-error-translator = An error occurred importing the selected file with “{ $translator }”. Please ensure that the file is valid and try again. + +# Variables: +# $targetAppOnline (String) +# $targetApp (String) +import-online-intro=In the next step you will be asked to log in to { $targetAppOnline } and grant { -app-name } access. This is necessary to import your { $targetApp } library into { -app-name }. +import-online-intro2={ -app-name } will never see or store your { $targetApp } password. + +report-error = + .label = Report Error… + +rtfScan-wizard = + .title = RTF Scan + +rtfScan-introPage-description = Zotero can automatically extract and reformat citations and insert a bibliography into RTF files. To get started, choose an RTF file below. +rtfScan-introPage-description2 = To get started, select an RTF input file and an output file below: + +rtfScan-input-file = Input File +rtfScan-output-file = Output File + +zotero-file-none-selected = + .value = No file selected + +zotero-file-choose = + .label = Choose File… + +rtfScan-intro-page = + .label = Introduction + +rtfScan-scan-page = + .label = Scanning for Citations + +rtfScan-scanPage-description = Zotero is scanning your document for citations. Please be patient. + +rtfScan-citations-page = + .label = Verify Cited Items + +rtfScan-citations-page-description = Please review the list of recognized citations below to ensure that Zotero has selected the corresponding items correctly. Any unmapped or ambiguous citations must be resolved before proceeding to the next step. + +rtfScan-style-page = + .label = Document Formatting + +rtfScan-format-page = + .label = Formatting Citations + +rtfScan-format-page-description = Zotero is processing and formatting your RTF file. Please be patient. + +rtfScan-complete-page = + .label = RTF Scan Complete + +rtfScan-complete-page-description = Your document has now been scanned and processed. Please ensure that it is formatted correctly. + + +bibliography-style-label = Citation Style: +bibliography-locale-label = Language: + +integration-prefs-displayAs-label = Display Citations As: +integration-prefs-footnotes = + .label = Footnotes +integration-prefs-endnotes = + .label = Endnotes + + +publications-intro-page = + .label = My Publications + +publications-intro = Items you add to My Publications will be shown on your profile page on zotero.org. If you choose to include attached files, they will be made publicly available under the license you specify. Only add work you yourself have created, and only include files if you have the rights to distribute them and wish to do so. +publications-include-checkbox-files = + .label = Include files +publications-include-checkbox-notes = + .label = Include notes + +publications-include-adjust-at-any-time = You can adjust what to show at any time from the My Publications collection. +publications-intro-authorship = + .label = I created this work. +publications-intro-authorship-files = + .label = I created this work and have the rights to distribute included files. + +publications-sharing-page = + .label = Choose how your work may be shared + +publications-sharing-keep-rights-field = + .label = Keep the existing Rights field +publications-sharing-keep-rights-field-where-available = + .label = Keep the existing Rights field where available +publications-sharing-text = You can reserve all rights to your work, license it under a Creative Commons license, or dedicate it to the public domain. In all cases, the work will be made publicly available via zotero.org. +publications-sharing-prompt = Would you like to allow your work to be shared by others? +publications-sharing-reserved = + .label = No, only publish my work on zotero.org +publications-sharing-cc = + .label = Yes, under a Creative Commons license +publications-sharing-cc0 = + .label = Yes, and place my work in the public domain + +publications-license-page = + .label = Choose a Creative Commons license +publications-choose-license-text = A Creative Commons license allows others to copy and redistribute your work as long as they give appropriate credit, provide a link to the license, and indicate if changes were made. Additional conditions can be specified below. +publications-choose-license-adaptations-prompt = Allow adaptations of your work to be shared? + +publications-choose-license-yes = + .label = Yes + .accesskey = Y +publications-choose-license-no = + .label = No + .accesskey = N +publications-choose-license-sharealike = + .label = Yes, as long as others share alike + .accesskey = S + +publications-choose-license-commercial-prompt = Allow commercial uses of your work? +publications-buttons-add-to-my-publications = + .label = Add to My Publications +publications-buttons-next-sharing = + .label = Next: Sharing +publications-buttons-next-choose-license = + .label = Choose a License + +licenses-cc-0 = CC0 1.0 Universal Public Domain Dedication +licenses-cc-by = Creative Commons Attribution 4.0 International License +licenses-cc-by-nd = Creative Commons Attribution-NoDerivatives 4.0 International License +licenses-cc-by-sa = Creative Commons Attribution-ShareAlike 4.0 International License +licenses-cc-by-nc = Creative Commons Attribution-NonCommercial 4.0 International License +licenses-cc-by-nc-nd = Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License +licenses-cc-by-nc-sa = Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License +licenses-cc-more-info = Be sure you have read the Creative Commons Considerations for licensors before placing your work under a CC license. Note that the license you apply cannot be revoked, even if you later choose different terms or cease publishing the work. +licenses-cc0-more-info = Be sure you have read the Creative Commons CC0 FAQ before applying CC0 to your work. Please note that dedicating your work to the public domain is irreversible, even if you later choose different terms or cease publishing the work. \ No newline at end of file diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index adb5b8ee7b..40bc7bc4ff 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -808,14 +808,6 @@ fileInterface.exportError = An error occurred while trying to export the selecte fileInterface.importOPML = Import Feeds from OPML fileInterface.OPMLFeedFilter = OPML Feed List -import.onlineImport = online import -import.localImport = local import -import.fileHandling.store = Copy files to the %S storage folder -import.fileHandling.link = Link to files in original location -import.fileHandling.description = Linked files cannot be synced by %S. -import.online.intro = In the next step you will be asked to log in to %2$S and grant %1$S access. This is necessary to import your %3$S library into %1$S. -import.online.intro2 = %1$S will never see or store your %2$S password. - quickCopy.copyAs = Copy as %S quickSearch.mode.titleCreatorYear = Title, Creator, Year diff --git a/chrome/skin/default/zotero/importWizard.css b/chrome/skin/default/zotero/importWizard.css deleted file mode 100644 index ec8d6d27a8..0000000000 --- a/chrome/skin/default/zotero/importWizard.css +++ /dev/null @@ -1,82 +0,0 @@ -.wizard-header-label { - font-size: 16px; - font-weight: bold; -} - -/* Start */ -wizard[currentpageid="page-start"] .wizard-header-label { - padding-top: 24px; -} - -wizard[currentpageid="page-start"] .wizard-page-box { - margin-top: -2px; - padding-top: 0; -} - -radiogroup { - font-size: 14px; - margin-top: 4px; -} - -radio { - padding-top: 5px; -} - -/* File list */ -wizard[currentpageid="page-file-list"] .wizard-header { - display: none; -} - -#file-options-header { - font-size: 15px; - font-weight: bold; - margin-bottom: 6px; -} - -#file-handling-options { - margin-top: 2em; -} - -#file-handling-options > label { - font-size: 14px; -} - -#file-handling-options radiogroup { - font-size: 13px; - margin-left: 1em; -} - -#file-handling-options description { - margin-top: .6em; - font-size: 11px; -} - -listbox, #result-description, #result-description-html { - font-size: 13px; -} - -#result-description-html { - line-height: 1.5; -} - -#mendeley-online-description, #mendeley-online-description2 { - font-size: 13px; - line-height: 1.5; -} - -#mendeley-online-description2 { - margin-top: 1em; -} - -#result-description-html a { - text-decoration: underline; -} - -button, checkbox { - font-size: 13px; -} - -#result-report-error { - margin-top: 13px; - margin-left: 0; -} diff --git a/package.json b/package.json index 23e96cae40..61b3f32b5c 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "colors": "^1.4.0", "eslint": "^8.5.0", "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.0.4", "fs-extra": "^3.0.1", "globby": "^6.1.0", "jspath": "^0.4.0", diff --git a/resource/config.js b/resource/config.js index 452fe1870b..67c3a5760f 100644 --- a/resource/config.js +++ b/resource/config.js @@ -33,8 +33,9 @@ var ZOTERO_CONFIG = { PLUGINS_URL: 'https://www.zotero.org/support/plugins', }; -if (typeof process === 'object' && process + '' === '[object process]'){ +if (typeof exports === 'object' && typeof module !== 'undefined') { module.exports = ZOTERO_CONFIG; -} else { +} +else { var EXPORTED_SYMBOLS = ["ZOTERO_CONFIG"]; -} \ No newline at end of file +} diff --git a/scripts/config.js b/scripts/config.js index d09363f581..dab51d0a0d 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -97,7 +97,14 @@ const browserifyConfigs = [ config: { standalone: 'chaiAsPromised' } - } + }, + { + src: 'node_modules/multimatch/index.js', + dest: 'resource/multimatch.js', + config: { + standalone: 'multimatch' + } + }, ]; // exclude mask used for js, copy, symlink and sass tasks diff --git a/scss/_zotero-react-client.scss b/scss/_zotero-react-client.scss index bd0ceb0c9d..05aaa4d01a 100644 --- a/scss/_zotero-react-client.scss +++ b/scss/_zotero-react-client.scss @@ -31,11 +31,14 @@ @import "components/editable"; @import "components/exportOptions"; @import "components/icons"; +@import "components/import-wizard"; @import "components/item-tree"; @import "components/longTagFixer"; @import "components/mainWindow"; @import "components/notesList"; @import "components/progressMeter"; +@import "components/publications-dialog.scss"; +@import "components/rtfScan.scss"; @import "components/search"; @import "components/syncButtonTooltip"; @import "components/tabBar"; diff --git a/scss/components/_import-wizard.scss b/scss/components/_import-wizard.scss new file mode 100644 index 0000000000..f485ca8cf4 --- /dev/null +++ b/scss/components/_import-wizard.scss @@ -0,0 +1,142 @@ +.import-wizard { + font-size: 12px; + + radiogroup { + radio { + padding-top: 5px; + } + } + + wizardpage { + display: flex; + flex-direction: column; + + > div { + display: block; + } + } + + a { + display: inline; + color: -moz-nativehyperlinktext; + cursor: pointer; + + &:hover, &:active, &:focus { + outline:none; + text-decoration: $link-hover-decoration; + } + } + + checkbox { + margin-left: 4px; // indent required for correct focus ring rendering + } + + button { + font: menu; + } + + h2 { + font-size: 14px; + font-weight: normal; + margin-bottom: 6px; + margin: 0; + } + + #other-files { + margin-left: -1px; + width: 400px; + } + + + #page-start { + radiogroup { + margin-top: 4px; + } + } + + #page-options-folder-import { + fieldset { + display: block; + border: none; + margin-left: 1em; + } + + #recreate-structure { + margin-top: .25em; + } + + .import-file-types-header { + margin-top: 1em; + } + + .page-options-file-type { + display: flex; + margin-top: .25em; + + &:first-child { + margin-top: 1em; + } + } + } + + #page-options-file-handling { + padding-top: 1.5em; + + .radioset { + // font-size: 13px; + margin-left: 1em; + } + } + + .page-options-file-handling-description { + margin-top: .6em; + font-size: 11px; + } + + #page-done-error { + margin-top: 2em; + text-align: right; + } + + .mendeley-online-intro { + // font-size: 13px; + + & +.mendeley-online-intro { + margin-top: 1em; + } + } + + #import-progress { + margin-top: 1em; + } + + #page-progress { + display: flex; + flex-direction: column; + } + + // TODO: deduplicate with rtfscan + .table-container { + display: flex; + height: 0; + flex-direction: column; + flex: 1 0 auto; + margin-top: 1.5em; + + > div { + display: flex; + flex: 1 0 auto; + background-color: -moz-field; + overflow: hidden; + position: relative; + } + + .virtualized-table-body { + display: flex; + + .windowed-list { + flex: 1 0 auto; + } + } + } +} \ No newline at end of file diff --git a/scss/components/_publications-dialog.scss b/scss/components/_publications-dialog.scss new file mode 100644 index 0000000000..72868e229a --- /dev/null +++ b/scss/components/_publications-dialog.scss @@ -0,0 +1,46 @@ +.publications-dialog-wizard { + font-size: 12px; + + div { + display: block; + } + + h2 { + margin: 1em 0 0; + padding: 0; + font-size: 13px; + font-weight: normal; + } + + h2 + radiogroup { + margin-top: 0; + } + + p.description { + display: block; + margin: 1em 0; + } + + checkbox { + margin-left: 4px; // indent required for correct focus ring rendering + } + + radiogroup { + font-size: 12px; + margin-top: 1em; + + radio:first-child { + margin-top: 9px; + } + } + + wizardpage { + display: flex; + flex-direction: column; + } + + #include-files, + #include-notes { + margin-top: 5px; + } +} \ No newline at end of file diff --git a/scss/components/_rtfScan.scss b/scss/components/_rtfScan.scss new file mode 100644 index 0000000000..ff66ffa835 --- /dev/null +++ b/scss/components/_rtfScan.scss @@ -0,0 +1,85 @@ +.rtfscan-wizard { + wizardpage { + display: flex; + flex-direction: column; + + > div { + display: block; + } + } + + p { + display: inline; + } + + .example, .page-start-1, .page-start-2, .file-input-label { + display: block; + } + + .example { + line-height: 1.5em + } + + .page-start-1 { + margin-bottom: 1em; + } + + .page-start-2 { + margin-top: 1em; + } + + .file-input-label { + margin: 1em $space-xs $space-min; + } + + .file-input-container { + background-color: $input-group-background-color; + border-radius: $border-radius-base; + border: 1px solid $input-group-border-color; + display: flex; + margin: auto $space-min auto; + padding: .5em; + + > input { + flex: 1 0 auto; + } + + > button { + flex: 0 1 auto; + margin-left: 1em; + } + } + + .citations-page-description { + margin-bottom: 1em; + } + + .citations-page > .wizard-body { + display: flex; + flex-direction: column; + } + + .table-container { + display: flex; + height: 0; + flex-direction: column; + flex: 1 0 auto; + margin-top: 1.5em; + + >div { + display: flex; + flex: 1 0 auto; + background-color: -moz-field; + overflow: hidden; + position: relative; + } + + .virtualized-table-body { + display: flex; + + .windowed-list { + flex: 1 0 auto; + } + } + } +} \ No newline at end of file diff --git a/scss/elements/license-info.scss b/scss/elements/license-info.scss new file mode 100644 index 0000000000..6ec74e099a --- /dev/null +++ b/scss/elements/license-info.scss @@ -0,0 +1,50 @@ +@import "../abstracts/variables"; +@import "../abstracts/functions"; +@import "../abstracts/mixins"; +@import "../abstracts/placeholders"; +@import "../abstracts/utilities"; +@import "../themes/light"; + +:host { + display: block; + width: 100%; +} + +.license-info { + display: flex; + padding: var(--license-info-padding, 0 20px); + margin: var(--license-info-margin, 1em 0 0 0); + + .license-icon { + flex: 0 1 auto; + + > img { + max-width: var(--license-icon-max-width, 88px); + } + } + + .license-name { + margin-left: var(--license-info-name-margin-left, 12px); + flex: 1 1 auto; + display: flex; + align-items: center; + } +} + +.license-more-info { + margin: var(--license-more-info-margin, 1.5em 0 0 0); + font-size: var(--license-more-info-font-size, 11px); +} + +a { + display: inline; + color: var(--license-info-link-color, -moz-nativehyperlinktext); + text-decoration: var(--license-info-link-decoration, none); + + &:hover, + &:active, + &:focus { + outline: none; + text-decoration: var(--license-info-link-decoration-hover, $link-hover-decoration); + } +} \ No newline at end of file diff --git a/scss/elements/style-configurator.scss b/scss/elements/style-configurator.scss new file mode 100644 index 0000000000..5e72da7805 --- /dev/null +++ b/scss/elements/style-configurator.scss @@ -0,0 +1,58 @@ +@import "../abstracts/variables"; +@import "../abstracts/functions"; +@import "../abstracts/mixins"; +@import "../abstracts/placeholders"; +@import "../abstracts/utilities"; +@import "../themes/light"; + +:host { + display: block; + width: 100%; +} + +richlistbox { + padding: var(--style-configurator-richlistbox-padding, 2px); + max-height: var(--style-configurator-richlistitem-max-height, 260px); + overflow: var(--style-configurator-richlistitem-overflow, auto scroll); + + richlistitem { + line-height: var(--style-configurator-richlistitem-line-height, 1.5em); + } + + @media (-moz-platform: macos) { + &:not(:focus) richlistitem[selected="true"] { + background-color: -moz-mac-secondaryhighlight; + } + } +} + +#locale-selector-wrapper, +#style-selector-wrapper, +#display-as-wrapper { + background-color: var(--style-configurator-field-wrapper-background-color, $input-group-background-color); + border-radius: var(--style-configurator-field-wrapper-border-radius, $border-radius-base); + border: var(--style-configurator-field-wrapper-border, 1px solid $input-group-border-color); + margin: var(--style-configurator-field-margin, 1.5em 0 0 0); + padding: var(--style-configurator-field-padding, $space-xs); +} + +label[for="style-selector"] { + margin: var(--style-configurator-label-margin, 1.5em 0 0 0); + font-size: var(--style-configurator-label-font-size, 13px); +} + +#style-selector-wrapper { + margin: var(--style-configurator-style-field-margin, 0); +} + +#locale-selector-wrapper { + display: flex; + align-items: center; +} + +#display-as-wrapper { + radiogroup { + display: flex; + flex-direction: row; + } +} \ No newline at end of file diff --git a/scss/themes/_light.scss b/scss/themes/_light.scss index a6b7fdcab6..6476edac22 100644 --- a/scss/themes/_light.scss +++ b/scss/themes/_light.scss @@ -156,6 +156,8 @@ $input-bg: $body-bg; $input-border-color: $shade-3; $input-focus-color: $secondary; $placeholder-color: $shade-5; +$input-group-background-color: #e5e5e5; +$input-group-border-color: darken($input-group-background-color, 5%); // Editable $editable-color: $text-color; diff --git a/test/tests/folderImportTest.js b/test/tests/folderImportTest.js new file mode 100644 index 0000000000..c81e269f4c --- /dev/null +++ b/test/tests/folderImportTest.js @@ -0,0 +1,140 @@ +/* global Zotero_Import_Folder: false */ + +describe('Zotero_Import_Folder', function () { + var tmpDir; + const uc = name => 'Zotero_Import_Folder_' + name; + + before(async () => { + tmpDir = await getTempDirectory(); + + await OS.File.makeDir(OS.Path.join(tmpDir, uc('dir1'))); + await OS.File.makeDir(OS.Path.join(tmpDir, uc('dir1'), uc('subdir1'))); + await OS.File.makeDir(OS.Path.join(tmpDir, uc('dir2'))); + + await OS.File.copy( + OS.Path.join(getTestDataDirectory().path, 'recognizePDF_test_title.pdf'), + OS.Path.join(tmpDir, 'recognizePDF_test_title.pdf') + ); + await OS.File.copy( + OS.Path.join(getTestDataDirectory().path, 'recognizePDF_test_title.pdf'), + OS.Path.join(tmpDir, uc('dir1'), 'recognizePDF_test_title.pdf') + ); + await OS.File.copy( + OS.Path.join(getTestDataDirectory().path, 'recognizePDF_test_arXiv.pdf'), + OS.Path.join(tmpDir, uc('dir1'), uc('subdir1'), 'recognizePDF_test_arXiv.pdf') + ); + await OS.File.copy( + OS.Path.join(getTestDataDirectory().path, 'recognizePDF_test_title.pdf'), + OS.Path.join(tmpDir, uc('dir2'), 'recognizePDF_test_title.pdf') + ); + await OS.File.copy( + OS.Path.join(getTestDataDirectory().path, 'test.png'), + OS.Path.join(tmpDir, uc('dir2'), 'test.png') + ); + await OS.File.copy( + OS.Path.join(getTestDataDirectory().path, 'test.html'), + OS.Path.join(tmpDir, uc('dir2'), 'test.html') + ); + await OS.File.copy( + OS.Path.join(getTestDataDirectory().path, 'test.txt'), + OS.Path.join(tmpDir, uc('dir2'), 'test.txt') + ); + + Components.utils.import('chrome://zotero/content/import/folderImport.js'); + }); + + describe('#import', () => { + it('should import PDFs from a folder and recreate structure without creating duplicates', async function () { + // @TODO: re-enable when folder import is ready + this.skip(); + this.timeout(30000); + if (Zotero.automatedTest) { + this.skip(); + } + + const importer = new Zotero_Import_Folder({ + folder: tmpDir, + recreateStructure: true, + }); + + await importer.translate({ + libraryID: Zotero.Libraries.userLibraryID, + linkFiles: true, + }); + + assert.equal(importer.newItems.length, 2); + + const firstPDFAttachment = importer.newItems.find(ni => ni.getField('title') === 'recognizePDF_test_arXiv.pdf'); + const firstPDFItem = await Zotero.Items.getAsync(firstPDFAttachment.parentID); + const firstPDFCollections = await Zotero.Collections.getAsync(firstPDFItem.getCollections()); + assert.equal(firstPDFItem.getField('title'), 'Scaling study of an improved fermion action on quenched lattices'); + assert.equal(firstPDFCollections.length, 1); + assert.equal(firstPDFCollections[0].name, uc('subdir1')); + assert.equal((await Zotero.Collections.getAsync(firstPDFCollections[0].parentID)).name, uc('dir1')); + + const secondPDFAttachment = importer.newItems.find(ni => ni.getField('title') === 'recognizePDF_test_title.pdf'); + const secondPDFItem = await Zotero.Items.getAsync(secondPDFAttachment.parentID); + const secondPDFCollections = await Zotero.Collections.getAsync(secondPDFItem.getCollections()); + assert.equal(secondPDFItem.getField('title'), 'Bitcoin: A Peer-to-Peer Electronic Cash System'); + assert.equal(secondPDFCollections.length, 2); + assert.sameMembers(secondPDFCollections.map(c => c.name), [uc('dir1'), uc('dir2')]); + + assert.sameMembers( + Zotero.Collections.getByLibrary(Zotero.Libraries.userLibraryID, true) + .map(c => c.name) + .filter(c => c.startsWith('Zotero_Import_Folder')), + [uc('dir1'), uc('dir2'), uc('subdir1')] + ); + + const importer2 = new Zotero_Import_Folder({ + folder: tmpDir, + recreateStructure: true, + }); + + await importer2.translate({ + libraryID: Zotero.Libraries.userLibraryID, + linkFiles: true, + }); + + assert.lengthOf(importer2.newItems, 0); + assert.sameMembers( + Zotero.Collections.getByLibrary(Zotero.Libraries.userLibraryID, true) + .map(c => c.name) + .filter(c => c.startsWith('Zotero_Import_Folder')), + [uc('dir1'), uc('dir2'), uc('subdir1')] + ); + }); + + it('should only import specified file types from a folder', async function () { + // @TODO: re-enable when folder import is ready + this.skip(); + this.timeout(30000); + if (Zotero.automatedTest) { + this.skip(); + } + const importer = new Zotero_Import_Folder({ + folder: tmpDir, + recreateStructure: false, + fileTypes: '*.png,*.TXT', // should match case-insensitively + mimeTypes: [] + }); + + await importer.translate({ + libraryID: Zotero.Libraries.userLibraryID, + linkFiles: true, + }); + + assert.equal(importer.newItems.length, 2); + const pngItem = importer.newItems.find(ni => ni.getField('title') === 'test.png'); + assert.isDefined(pngItem); + assert.isFalse(pngItem.parentID); + + const txtItem = importer.newItems.find(ni => ni.getField('title') === 'test.txt'); + assert.isDefined(txtItem); + assert.isFalse(txtItem.parentID); + + const htmlItem = importer.newItems.find(ni => ni.getField('title') === 'test.html'); + assert.isUndefined(htmlItem); + }); + }); +});