Add "Include Annotations" checkbox to export options dialog

This changes the attachment saveFile() function in translators to be
async. In order for errors to be properly caught, translators will need
to be changed to make doExport() async and await on saveFile() calls.
(The translation architecture theoretically already allows doExport() to
be async.)
This commit is contained in:
Dan Stillman 2020-12-27 20:03:48 -05:00
parent 688298af7f
commit 55c6de23ba
8 changed files with 102 additions and 29 deletions

View file

@ -34,8 +34,6 @@ const OPTION_PREFIX = "export-option-";
// Class to provide options for export // Class to provide options for export
var Zotero_File_Interface_Export = new function() { var Zotero_File_Interface_Export = new function() {
this.init = init;
this.updateOptions = updateOptions;
this.accept = accept; this.accept = accept;
this.cancel = cancel; this.cancel = cancel;
@ -44,7 +42,7 @@ var Zotero_File_Interface_Export = new function() {
/* /*
* add options to export * add options to export
*/ */
function init() { this.init = function () {
// Set font size from pref // Set font size from pref
var sbc = document.getElementById('zotero-export-options-container'); var sbc = document.getElementById('zotero-export-options-container');
Zotero.setFontSize(sbc); Zotero.setFontSize(sbc);
@ -82,10 +80,25 @@ var Zotero_File_Interface_Export = new function() {
// right now, option interface supports only boolean values, which // right now, option interface supports only boolean values, which
// it interprets as checkboxes // it interprets as checkboxes
if(typeof(translators[i].displayOptions[option]) == "boolean") { if(typeof(translators[i].displayOptions[option]) == "boolean") {
var checkbox = document.createElement("checkbox"); let checkbox = document.createElement("checkbox");
checkbox.setAttribute("id", OPTION_PREFIX+option); checkbox.setAttribute("id", OPTION_PREFIX+option);
checkbox.setAttribute("label", optionLabel); checkbox.setAttribute("label", optionLabel);
optionsBox.insertBefore(checkbox, charsetBox); optionsBox.insertBefore(checkbox, charsetBox);
// Add "Include Annotations" after "Export Files"
if (option == 'exportFileData') {
checkbox.onclick = () => {
setTimeout(() => this.updateAnnotationsCheckbox());
};
checkbox = document.createElement("checkbox");
checkbox.setAttribute("id", OPTION_PREFIX + 'includeAnnotations');
checkbox.setAttribute(
"label",
Zotero.getString('exportOptions.includeAnnotations')
);
optionsBox.insertBefore(checkbox, charsetBox);
}
} }
addedOptions[option] = true; addedOptions[option] = true;
@ -108,13 +121,13 @@ var Zotero_File_Interface_Export = new function() {
_charsets = Zotero_Charset_Menu.populate(document.getElementById(OPTION_PREFIX+"exportCharset"), true); _charsets = Zotero_Charset_Menu.populate(document.getElementById(OPTION_PREFIX+"exportCharset"), true);
} }
updateOptions(Zotero.Prefs.get("export.translatorSettings")); this.updateOptions(Zotero.Prefs.get("export.translatorSettings"));
} }
/* /*
* update translator-specific options * update translator-specific options
*/ */
function updateOptions(optionString) { this.updateOptions = function (optionString) {
// get selected translator // get selected translator
var index = document.getElementById("format-menu").selectedIndex; var index = document.getElementById("format-menu").selectedIndex;
var translatorOptions = window.arguments[0].translators[index].displayOptions; var translatorOptions = window.arguments[0].translators[index].displayOptions;
@ -133,7 +146,9 @@ var Zotero_File_Interface_Export = new function() {
var node = optionsBox.childNodes[i]; var node = optionsBox.childNodes[i];
// skip non-options // skip non-options
if(node.id.length <= OPTION_PREFIX.length if(node.id.length <= OPTION_PREFIX.length
|| node.id.substr(0, OPTION_PREFIX.length) != OPTION_PREFIX) { || node.id.substr(0, OPTION_PREFIX.length) != OPTION_PREFIX
// Handled separately by updateAnnotationsCheckbox()
|| node.id == 'export-option-includeAnnotations') {
continue; continue;
} }
@ -161,6 +176,10 @@ var Zotero_File_Interface_Export = new function() {
} }
} }
this.updateAnnotationsCheckbox(
(options && options.includeAnnotations) ? options.includeAnnotations : false
);
// handle charset popup // handle charset popup
if(_charsets && translatorOptions && translatorOptions.exportCharset) { if(_charsets && translatorOptions && translatorOptions.exportCharset) {
optionsBox.hidden = undefined; optionsBox.hidden = undefined;
@ -181,6 +200,21 @@ var Zotero_File_Interface_Export = new function() {
window.sizeToContent(); window.sizeToContent();
} }
this.updateAnnotationsCheckbox = function (defaultValue) {
var filesCheckbox = document.getElementById(OPTION_PREFIX + 'exportFileData');
var annotationsCheckbox = document.getElementById(OPTION_PREFIX + 'includeAnnotations');
if (filesCheckbox.hidden) {
annotationsCheckbox.hidden = true;
annotationsCheckbox.checked = false;
return;
}
annotationsCheckbox.hidden = false;
annotationsCheckbox.disabled = !filesCheckbox.checked;
if (defaultValue !== undefined) {
annotationsCheckbox.checked = defaultValue;
}
};
/* /*
* make option array reflect status * make option array reflect status
*/ */
@ -210,6 +244,13 @@ var Zotero_File_Interface_Export = new function() {
} }
} }
// If "Export Files" is shown, add "Include Annotations" checkbox value
if (optionsAvailable && optionsAvailable.exportFileData !== undefined) {
let elem1 = document.getElementById(OPTION_PREFIX + 'exportFileData');
let elem2 = document.getElementById(OPTION_PREFIX + 'includeAnnotations');
displayOptions.includeAnnotations = elem1.checked && elem2.checked;
}
// save options // save options
var optionString = JSON.stringify(displayOptions); var optionString = JSON.stringify(displayOptions);
Zotero.Prefs.set("export.translatorSettings", optionString); Zotero.Prefs.set("export.translatorSettings", optionString);

View file

@ -1,5 +1,7 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
<!DOCTYPE window [ <!DOCTYPE window [
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd" > <!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd" >
%zoteroDTD; %zoteroDTD;

View file

@ -2541,8 +2541,14 @@ Zotero.Translate.Export.prototype._prepareTranslation = Zotero.Promise.method(fu
function rest() { function rest() {
// export file data, if requested // export file data, if requested
if(this._displayOptions["exportFileData"]) { if (this._displayOptions.exportFileData) {
this.location = this._itemGetter.exportFiles(this.location, this.translator[0].target); this.location = this._itemGetter.exportFiles(
this.location,
this.translator[0].target,
{
includeAnnotations: this._displayOptions.includeAnnotations
}
);
} }
// initialize IO // initialize IO

View file

@ -1048,9 +1048,10 @@ Zotero.Translate.ItemGetter.prototype = {
this.numItems = this._itemsLeft.length; this.numItems = this._itemsLeft.length;
}), }),
"exportFiles":function(dir, extension) { exportFiles: function (dir, extension, { includeAnnotations }) {
// generate directory // generate directory
this._exportFileDirectory = dir.parent.clone(); this._exportFileDirectory = dir.parent.clone();
this._includeAnnotations = includeAnnotations;
// delete this file if it exists // delete this file if it exists
if(dir.exists()) { if(dir.exists()) {
@ -1079,6 +1080,7 @@ Zotero.Translate.ItemGetter.prototype = {
var attachmentArray = Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy); var attachmentArray = Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy);
var linkMode = attachment.attachmentLinkMode; var linkMode = attachment.attachmentLinkMode;
if(linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { if(linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
let includeAnnotations = attachment.isPDFAttachment() && this._includeAnnotations;
attachmentArray.localPath = attachment.getFilePath(); attachmentArray.localPath = attachment.getFilePath();
if(this._exportFileDirectory) { if(this._exportFileDirectory) {
@ -1114,7 +1116,7 @@ Zotero.Translate.ItemGetter.prototype = {
* file to be overwritten. If true, the file will be silently overwritten. * file to be overwritten. If true, the file will be silently overwritten.
* defaults to false if not provided. * defaults to false if not provided.
*/ */
attachmentArray.saveFile = function(attachPath, overwriteExisting) { attachmentArray.saveFile = async function (attachPath, overwriteExisting) {
// Ensure a valid path is specified // Ensure a valid path is specified
if(attachPath === undefined || attachPath == "") { if(attachPath === undefined || attachPath == "") {
throw new Error("ERROR_EMPTY_PATH"); throw new Error("ERROR_EMPTY_PATH");
@ -1162,18 +1164,13 @@ Zotero.Translate.ItemGetter.prototype = {
var directory = targetFile.parent; var directory = targetFile.parent;
// The only attachments that can have multiple supporting files are imported // For snapshots with supporting files, check if any of the supporting
// attachments of mime type text/html // files would cause a name conflict, and build a list of transfers
//
// TEMP: This used to check getNumFiles() here, but that's now async.
// It could be restored (using hasMultipleFiles()) when this is made
// async, but it's probably not necessary. (The below can also be changed
// to use OS.File.DirectoryIterator.)
if(attachment.attachmentContentType == "text/html"
&& linkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) {
// Attachment is a snapshot with supporting files. Check if any of the
// supporting files would cause a name conflict, and build a list of transfers
// that should be performed // that should be performed
//
// TODO: Change the below to use OS.File.DirectoryIterator?
if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE
&& await Zotero.Attachments.hasMultipleFiles(attachment)) {
var copySrcs = []; var copySrcs = [];
var files = attachment.getFile().parent.directoryEntries; var files = attachment.getFile().parent.directoryEntries;
while (files.hasMoreElements()) { while (files.hasMoreElements()) {
@ -1204,11 +1201,23 @@ Zotero.Translate.ItemGetter.prototype = {
for(var i = 0; i < copySrcs.length; i++) { for(var i = 0; i < copySrcs.length; i++) {
copySrcs[i].copyTo(directory, copySrcs[i].leafName); copySrcs[i].copyTo(directory, copySrcs[i].leafName);
} }
} else { }
// Attachment is a single file // For single files, just copy to the specified location
// Copy the file to the specified location else {
if (includeAnnotations) {
// TODO: Make export async
try {
await Zotero.PDFWorker.export(attachment.id, targetFile.path);
}
catch (e) {
Zotero.logError(e);
throw e;
}
}
else {
attachFile.copyTo(directory, targetFile.leafName); attachFile.copyTo(directory, targetFile.leafName);
} }
}
attachmentArray.path = targetFile.path; attachmentArray.path = targetFile.path;
}; };

View file

@ -803,6 +803,7 @@ fulltext.indexState.queued = Queued
exportOptions.exportNotes = Export Notes exportOptions.exportNotes = Export Notes
exportOptions.exportFileData = Export Files exportOptions.exportFileData = Export Files
exportOptions.includeAnnotations = Include Annotations
exportOptions.useJournalAbbreviation = Use Journal Abbreviation exportOptions.useJournalAbbreviation = Use Journal Abbreviation
charset.UTF8withoutBOM = Unicode (UTF-8 without BOM) charset.UTF8withoutBOM = Unicode (UTF-8 without BOM)
charset.autoDetect = (auto detect) charset.autoDetect = (auto detect)

View file

@ -25,6 +25,7 @@
@import "components/button"; @import "components/button";
@import "components/createParent"; @import "components/createParent";
@import "components/editable"; @import "components/editable";
@import "components/exportOptions";
@import "components/icons"; @import "components/icons";
@import "components/mainWindow"; @import "components/mainWindow";
@import "components/notesList"; @import "components/notesList";

View file

@ -0,0 +1,10 @@
#zotero-export-options {
#export-option-includeAnnotations {
margin-top: 2px;
margin-left: 17px;
}
checkbox[disabled=true] label {
opacity: .5;
}
}

View file

@ -2059,7 +2059,7 @@ describe("Zotero.Translate.ItemGetter", function() {
// saveFile function // saveFile function
assert.isFunction(attachment.saveFile, prefix + 'has saveFile function' + suffix); assert.isFunction(attachment.saveFile, prefix + 'has saveFile function' + suffix);
attachment.saveFile(attachment.defaultPath); yield attachment.saveFile(attachment.defaultPath);
assert.equal(attachment.path, OS.Path.join(exportDir, OS.Path.normalize(attachment.defaultPath)), prefix + 'path is set correctly after saveFile call' + suffix); assert.equal(attachment.path, OS.Path.join(exportDir, OS.Path.normalize(attachment.defaultPath)), prefix + 'path is set correctly after saveFile call' + suffix);
let fileExists = yield OS.File.exists(attachment.path); let fileExists = yield OS.File.exists(attachment.path);
@ -2067,8 +2067,11 @@ describe("Zotero.Translate.ItemGetter", function() {
fileExists = yield OS.File.exists(attachment.localPath); fileExists = yield OS.File.exists(attachment.localPath);
assert.isTrue(fileExists, prefix + 'file was not removed from original location' + suffix); assert.isTrue(fileExists, prefix + 'file was not removed from original location' + suffix);
assert.throws(attachment.saveFile.bind(attachment, attachment.defaultPath), /^ERROR_FILE_EXISTS /, prefix + 'saveFile does not overwrite existing file by default' + suffix); let e;
assert.throws(attachment.saveFile.bind(attachment, 'file/../../'), /./, prefix + 'saveFile does not allow exporting outside export directory' + suffix); e = yield getPromiseError(attachment.saveFile(attachment.defaultPath));
assert.match(e.message, /^ERROR_FILE_EXISTS /, prefix + 'saveFile does not overwrite existing file by default' + suffix);
e = yield getPromiseError(attachment.saveFile('file/../../'));
assert.match(e.message, /./, prefix + 'saveFile does not allow exporting outside export directory' + suffix);
/** TODO: check if overwriting existing file works **/ /** TODO: check if overwriting existing file works **/
} }