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
var Zotero_File_Interface_Export = new function() {
this.init = init;
this.updateOptions = updateOptions;
this.accept = accept;
this.cancel = cancel;
@ -44,7 +42,7 @@ var Zotero_File_Interface_Export = new function() {
/*
* add options to export
*/
function init() {
this.init = function () {
// Set font size from pref
var sbc = document.getElementById('zotero-export-options-container');
Zotero.setFontSize(sbc);
@ -82,10 +80,25 @@ var Zotero_File_Interface_Export = new function() {
// right now, option interface supports only boolean values, which
// it interprets as checkboxes
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("label", optionLabel);
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;
@ -108,13 +121,13 @@ var Zotero_File_Interface_Export = new function() {
_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
*/
function updateOptions(optionString) {
this.updateOptions = function (optionString) {
// get selected translator
var index = document.getElementById("format-menu").selectedIndex;
var translatorOptions = window.arguments[0].translators[index].displayOptions;
@ -133,7 +146,9 @@ var Zotero_File_Interface_Export = new function() {
var node = optionsBox.childNodes[i];
// skip non-options
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;
}
@ -161,6 +176,10 @@ var Zotero_File_Interface_Export = new function() {
}
}
this.updateAnnotationsCheckbox(
(options && options.includeAnnotations) ? options.includeAnnotations : false
);
// handle charset popup
if(_charsets && translatorOptions && translatorOptions.exportCharset) {
optionsBox.hidden = undefined;
@ -181,6 +200,21 @@ var Zotero_File_Interface_Export = new function() {
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
*/
@ -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
var optionString = JSON.stringify(displayOptions);
Zotero.Prefs.set("export.translatorSettings", optionString);

View file

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

View file

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

View file

@ -1048,9 +1048,10 @@ Zotero.Translate.ItemGetter.prototype = {
this.numItems = this._itemsLeft.length;
}),
"exportFiles":function(dir, extension) {
exportFiles: function (dir, extension, { includeAnnotations }) {
// generate directory
this._exportFileDirectory = dir.parent.clone();
this._includeAnnotations = includeAnnotations;
// delete this file if it exists
if(dir.exists()) {
@ -1079,6 +1080,7 @@ Zotero.Translate.ItemGetter.prototype = {
var attachmentArray = Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy);
var linkMode = attachment.attachmentLinkMode;
if(linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
let includeAnnotations = attachment.isPDFAttachment() && this._includeAnnotations;
attachmentArray.localPath = attachment.getFilePath();
if(this._exportFileDirectory) {
@ -1114,7 +1116,7 @@ Zotero.Translate.ItemGetter.prototype = {
* file to be overwritten. If true, the file will be silently overwritten.
* defaults to false if not provided.
*/
attachmentArray.saveFile = function(attachPath, overwriteExisting) {
attachmentArray.saveFile = async function (attachPath, overwriteExisting) {
// Ensure a valid path is specified
if(attachPath === undefined || attachPath == "") {
throw new Error("ERROR_EMPTY_PATH");
@ -1162,18 +1164,13 @@ Zotero.Translate.ItemGetter.prototype = {
var directory = targetFile.parent;
// The only attachments that can have multiple supporting files are imported
// attachments of mime type text/html
//
// 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
// For snapshots 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
//
// 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 files = attachment.getFile().parent.directoryEntries;
while (files.hasMoreElements()) {
@ -1204,11 +1201,23 @@ Zotero.Translate.ItemGetter.prototype = {
for(var i = 0; i < copySrcs.length; i++) {
copySrcs[i].copyTo(directory, copySrcs[i].leafName);
}
} else {
// Attachment is a single file
// Copy the file to the specified location
}
// For single files, just copy 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);
}
}
attachmentArray.path = targetFile.path;
};

View file

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

View file

@ -25,6 +25,7 @@
@import "components/button";
@import "components/createParent";
@import "components/editable";
@import "components/exportOptions";
@import "components/icons";
@import "components/mainWindow";
@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
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);
let fileExists = yield OS.File.exists(attachment.path);
@ -2067,8 +2067,11 @@ describe("Zotero.Translate.ItemGetter", function() {
fileExists = yield OS.File.exists(attachment.localPath);
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);
assert.throws(attachment.saveFile.bind(attachment, 'file/../../'), /./, prefix + 'saveFile does not allow exporting outside export directory' + suffix);
let e;
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 **/
}