Add "Convert Linked Files to Stored Files…" menu option

In new File → Manage Attachments submenu

Closes #1637
This commit is contained in:
Dan Stillman 2019-08-18 16:22:39 -04:00
parent eca2822651
commit bb59429664
11 changed files with 417 additions and 61 deletions

View file

@ -91,6 +91,23 @@ const ZoteroStandalone = new function() {
});
}
this.onFileMenuOpen = function () {
var active = false;
try {
let zp = Zotero.getActiveZoteroPane();
if (zp) {
active = !!zp.getSelectedItems().filter((item) => {
return item.isAttachment()
|| (item.isRegularItem() && item.getAttachments().length);
}).length;
}
}
catch (e) {}
this.updateMenuItemEnabled('manage-attachments-menu', active);
};
/**
* Builds new item menu
*/
@ -138,6 +155,43 @@ const ZoteroStandalone = new function() {
}
this.onManageAttachmentsMenuOpen = function () {
// Convert Linked Files to Stored Files
var active = false;
try {
let zp = Zotero.getActiveZoteroPane();
if (zp) {
active = !!zp.getSelectedItems().filter((item) => {
return item.isLinkedFileAttachment()
|| (item.isRegularItem()
&& item.getAttachments()
.map(id => Zotero.Items.get(id))
.some(att => att.isLinkedFileAttachment()));
}).length;
}
}
catch (e) {}
this.updateMenuItemEnabled('file-menuitem-convert-to-stored', active);
};
this.onManageAttachmentsMenuItemClick = function (event) {
var menuitem = event.originalTarget;
var id = menuitem.id;
var prefix = 'file-menuitem-';
if (menuitem.disabled || !id.startsWith(prefix)) {
return;
}
id = id.substr(prefix.length);
switch (id) {
case 'convert-to-stored':
ZoteroPane.convertLinkedFilesToStoredFiles();
break;
}
};
this.updateQuickCopyOptions = function () {
var selected = false;
try {
@ -181,28 +235,28 @@ const ZoteroStandalone = new function() {
this.onViewMenuOpen = function () {
// Layout mode
var mode = Zotero.Prefs.get('layout');
this.updateMenuItemCheckmark('standard', mode != 'stacked');
this.updateMenuItemCheckmark('stacked', mode == 'stacked');
this.updateMenuItemCheckmark('view-menuitem-standard', mode != 'stacked');
this.updateMenuItemCheckmark('view-menuitem-stacked', mode == 'stacked');
// Panes
this.updateMenuItemCheckmark(
'collections-pane',
'view-menuitem-collections-pane',
document.getElementById('zotero-collections-pane').getAttribute('collapsed') != 'true'
);
this.updateMenuItemCheckmark(
'item-pane',
'view-menuitem-item-pane',
document.getElementById('zotero-item-pane').getAttribute('collapsed') != 'true'
);
this.updateMenuItemCheckmark(
'tag-selector',
'view-menuitem-tag-selector',
document.getElementById('zotero-tag-selector-container').getAttribute('collapsed') != 'true'
);
// Font size
var fontSize = Zotero.Prefs.get('fontSize');
this.updateMenuItemDisabled('font-size-bigger', fontSize >= FONT_SIZES[FONT_SIZES.length - 1]);
this.updateMenuItemDisabled('font-size-smaller', fontSize <= FONT_SIZES[0]);
this.updateMenuItemDisabled('font-size-reset', fontSize == FONT_SIZES[0]);
this.updateMenuItemEnabled('view-menuitem-font-size-bigger', fontSize < FONT_SIZES[FONT_SIZES.length - 1]);
this.updateMenuItemEnabled('view-menuitem-font-size-smaller', fontSize > FONT_SIZES[0]);
this.updateMenuItemEnabled('view-menuitem-font-size-reset', fontSize != FONT_SIZES[0]);
var noteFontSize = Zotero.Prefs.get('note.fontSize');
for (let menuitem of document.querySelectorAll(`#note-font-size-menu menuitem`)) {
@ -213,46 +267,27 @@ const ZoteroStandalone = new function() {
menuitem.removeAttribute('checked');
}
}
this.updateMenuItemDisabled('note-font-size-reset', noteFontSize == NOTE_FONT_SIZE_DEFAULT);
this.updateMenuItemEnabled(
'view-menuitem-note-font-size-reset',
noteFontSize != NOTE_FONT_SIZE_DEFAULT
);
// Recursive collections
this.updateMenuItemCheckmark('recursive-collections', Zotero.Prefs.get('recursiveCollections'));
this.updateMenuItemCheckmark(
'view-menuitem-recursive-collections',
Zotero.Prefs.get('recursiveCollections')
);
};
this.updateMenuItemCheckmark = function (idSuffix, checked) {
var id = 'view-menuitem-' + idSuffix;
var menuitem = document.getElementById(id);
if (checked) {
menuitem.setAttribute('checked', true);
}
else {
menuitem.removeAttribute('checked');
}
};
this.updateMenuItemDisabled = function (idSuffix, disabled) {
var id = 'view-menuitem-' + idSuffix;
var menuitem = document.getElementById(id);
if (disabled) {
menuitem.setAttribute('disabled', true);
}
else {
menuitem.removeAttribute('disabled');
}
};
this.updateViewOption = function (event) {
this.onViewMenuItemClick = function (event) {
var menuitem = event.originalTarget;
var id = menuitem.id;
if (menuitem.disabled || !id.startsWith('view-menuitem-')) {
var prefix = 'view-menuitem-';
if (menuitem.disabled || !id.startsWith(prefix)) {
return;
}
id = id.substr(14);
id = id.substr(prefix.length);
switch (id) {
case 'standard':
@ -328,6 +363,28 @@ const ZoteroStandalone = new function() {
};
this.updateMenuItemCheckmark = function (id, checked) {
var menuitem = document.getElementById(id);
if (checked) {
menuitem.setAttribute('checked', true);
}
else {
menuitem.removeAttribute('checked');
}
};
this.updateMenuItemEnabled = function (id, enabled) {
var menuitem = document.getElementById(id);
if (enabled) {
menuitem.removeAttribute('disabled');
}
else {
menuitem.setAttribute('disabled', true);
}
};
this.toggleBooleanPref = function (pref) {
Zotero.Prefs.set(pref, !Zotero.Prefs.get(pref));
};

View file

@ -120,7 +120,8 @@
<toolbaritem id="menubar-items" align="center">
<menubar id="main-menubar"
style="border:0px;padding:0px;margin:0px;-moz-appearance:none">
<menu id="fileMenu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
<menu id="fileMenu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;"
onpopupshowing="ZoteroStandalone.onFileMenuOpen()">
<menupopup id="menu_FilePopup">
<menu id="menu_newItem" label="&zotero.toolbar.newItem.label;">
<menupopup id="menu_NewItemPopup"
@ -134,6 +135,16 @@
<menuitem id="menu_close" label="&closeCmd.label;" key="key_close"
accesskey="&closeCmd.accesskey;" command="cmd_close"/>
<menuseparator/>
<menu id="manage-attachments-menu" label="&manageAttachments.label;"
onpopupshowing="ZoteroStandalone.onManageAttachmentsMenuOpen()"
oncommand="ZoteroStandalone.onManageAttachmentsMenuItemClick(event)">
<menupopup id="manage-attachments-menupopup">
<menuitem
id="file-menuitem-convert-to-stored"
label="&convertToStored.label;"/>
</menupopup>
</menu>
<menuseparator/>
<menuitem id="menu_import" label="&importCmd.label;"
command="cmd_zotero_import" key="key_import"/>
<menuitem id="menu_importFromClipboard" label="&importFromClipboardCmd.label;"
@ -190,7 +201,7 @@
<menupopup id="menu_viewPopup">
<menu id="layout-menu"
label="&layout.label;">
<menupopup oncommand="ZoteroStandalone.updateViewOption(event)">
<menupopup oncommand="ZoteroStandalone.onViewMenuItemClick(event)">
<menuitem id="view-menuitem-standard" label="&standardView.label;"/>
<menuitem id="view-menuitem-stacked" label="&stackedView.label;"/>
<menuseparator/>
@ -201,7 +212,7 @@
</menu>
<menu id="font-size-menu"
label="&fontSize.label;">
<menupopup oncommand="ZoteroStandalone.updateViewOption(event)">
<menupopup oncommand="ZoteroStandalone.onViewMenuItemClick(event)">
<menuitem id="view-menuitem-font-size-bigger" label="&zotero.general.bigger;"/>
<menuitem id="view-menuitem-font-size-smaller" label="&zotero.general.smaller;"/>
<menuseparator/>
@ -211,7 +222,7 @@
<menu id="note-font-size-menu"
label="&noteFontSize.label;">
<!-- TODO: Maybe switch to Bigger/Smaller once we can update without restarting -->
<!--<menupopup oncommand="ZoteroStandalone.updateViewOption(event)">
<!--<menupopup oncommand="ZoteroStandalone.onViewMenuItemClick(event)">
<menuitem id="view-menuitem-note-font-size-bigger" label="&zotero.general.bigger;"/>
<menuitem id="view-menuitem-note-font-size-smaller" label="&zotero.general.smaller;"/>
<menuseparator/>
@ -232,13 +243,13 @@
<menuitem
id="view-menuitem-note-font-size-reset"
label="&zotero.general.reset;"
oncommand="ZoteroStandalone.updateViewOption(event); event.stopPropagation();"/>
oncommand="ZoteroStandalone.onViewMenuItemClick(event); event.stopPropagation();"/>
</menupopup>
</menu>
<menuseparator/>
<menuitem id="view-menuitem-recursive-collections"
label="&recursiveCollections.label;"
oncommand="ZoteroStandalone.updateViewOption(event)"/>
oncommand="ZoteroStandalone.onViewMenuItemClick(event)"/>
</menupopup>
</menu>

View file

@ -2343,6 +2343,88 @@ Zotero.Attachments = new function(){
});
this.convertLinkedFileToStoredFile = async function (item, options = {}) {
if (item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) {
throw new Error("Not a linked-file attachment");
}
var file = await item.getFilePathAsync();
if (!file) {
Zotero.debug("Linked file not found at " + file);
return false;
}
var json = item.toJSON();
json.linkMode = 'imported_file';
delete json.path;
json.filename = OS.Path.basename(file);
var newItem = new Zotero.Item('attachment');
newItem.libraryID = item.libraryID;
newItem.fromJSON(json);
await newItem.saveTx();
await Zotero.Relations.copyObjectSubjectRelations(item, newItem);
var newFile;
try {
// Transfer file
let destDir = await this.createDirectoryForItem(newItem);
newFile = OS.Path.join(destDir, json.filename);
if (options.move) {
newFile = await Zotero.File.moveToUnique(file, newFile);
}
// Copy file to unique filename, which automatically shortens long filenames
else {
newFile = Zotero.File.copyToUnique(file, newFile);
// TEMP: copyToUnique returns an nsIFile
newFile = newFile.path;
await Zotero.File.setNormalFilePermissions(newFile);
let mtime = (await OS.File.stat(file)).lastModificationDate;
await OS.File.setDates(newFile, null, mtime);
}
}
catch (e) {
Zotero.logError(e);
// Delete new file
if (newFile) {
try {
await Zotero.File.removeIfExists(newFile);
}
catch (e) {
Zotero.logError(e);
}
}
// Delete new item
try {
await newItem.eraseTx();
}
catch (e) {
Zotero.logError(e);
}
return false;
}
try {
await Zotero.DB.executeTransaction(async function () {
await Zotero.Fulltext.transferItemIndex(item, newItem);
});
}
catch (e) {
Zotero.logError(e);
}
if (newFile && json.filename != OS.Path.basename(newFile)) {
Zotero.debug("Filename was changed");
newItem.attachmentFilename = OS.Path.basename(newFile);
await newItem.saveTx();
}
await item.eraseTx();
return newItem;
};
this._getFileNameFromURL = function(url, contentType) {
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL);

View file

@ -2106,7 +2106,7 @@ Zotero.Item.prototype.isWebAttachment = function() {
/**
* @return {Promise<Boolean>}
* @return {Boolean}
*/
Zotero.Item.prototype.isFileAttachment = function() {
if (!this.isAttachment()) {
@ -2116,6 +2116,14 @@ Zotero.Item.prototype.isFileAttachment = function() {
}
/**
* @return {Boolean}
*/
Zotero.Item.prototype.isLinkedFileAttachment = function() {
return this.isAttachment() && this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE;
}
/**
* Returns number of child attachments of item
*

View file

@ -169,6 +169,35 @@ Zotero.Relations = new function () {
});
/**
* For every relation pointing to a given object, create a relation on the subject pointing to a
* new object
*
* @param {Zotero.DataObject} fromObject
* @param {Zotero.DataObject} toObject
* @return {Promise}
*/
this.copyObjectSubjectRelations = async function (fromObject, toObject) {
var objectType = fromObject.objectType;
var ObjectType = Zotero.Utilities.capitalize(objectType);
var fromObjectURI = Zotero.URI[`get${ObjectType}URI`](fromObject);
var toObjectURI = Zotero.URI[`get${ObjectType}URI`](toObject);
var subjectPredicates = await Zotero.Relations.getByObject(objectType, fromObjectURI);
for (let { subject, predicate } of subjectPredicates) {
if (subject.isEditable()) {
subject.addRelation(predicate, toObjectURI);
await subject.saveTx({
skipDateModifiedUpdate: true
});
}
else {
Zotero.debug(`Subject ${objectType} ${subject.libraryKey} is not editable `
+ `-- not copying ${predicate} relation`);
}
}
};
this.updateUser = Zotero.Promise.coroutine(function* (fromUserID, toUserID) {
if (!fromUserID) {
fromUserID = "local/" + Zotero.Users.getLocalUserKey();

View file

@ -1141,6 +1141,39 @@ Zotero.Fulltext = Zotero.FullText = new function(){
});
this.transferItemIndex = async function (fromItem, toItem) {
await this.clearItemWords(toItem.id);
// Copy cache file if it exists
var cacheFile = this.getItemCacheFile(fromItem).path;
if (await OS.File.exists(cacheFile)) {
try {
await OS.File.move(cacheFile, this.getItemCacheFile(toItem).path);
}
catch (e) {
Zotero.logError(e);
return;
}
}
// Update database with new item id
await Zotero.DB.queryAsync("PRAGMA foreign_keys = false");
try {
await Zotero.DB.queryAsync(
"UPDATE fulltextItems SET itemID=? WHERE itemID=?",
[toItem.id, fromItem.id]
);
await Zotero.DB.queryAsync(
"UPDATE fulltextItemWords SET itemID=? WHERE itemID=?",
[toItem.id, fromItem.id]
);
}
catch (e) {
await Zotero.DB.queryAsync("PRAGMA foreign_keys = true");
}
};
/**
* @requireTransaction
*/

View file

@ -2748,7 +2748,7 @@ var ZoteroPane = new function()
'reportMetadata',
'createParent',
'renameAttachments',
'reindexItem'
'reindexItem',
];
var m = {};
@ -2881,20 +2881,19 @@ var ZoteroPane = new function()
show.push(m.sep5);
}
// Block certain actions on files if no access and at least one item
// is an imported attachment
// Block certain actions on files if no access and at least one item is a file
// attachment
if (!collectionTreeRow.filesEditable) {
var hasImportedAttachment = false;
for (var i=0; i<items.length; i++) {
var item = items[i];
if (item.isImportedAttachment()) {
hasImportedAttachment = true;
for (let item of items) {
if (item.isFileAttachment()) {
disable.push(
m.moveToTrash,
m.createParent,
m.renameAttachments
);
break;
}
}
if (hasImportedAttachment) {
disable.push(m.moveToTrash, m.createParent, m.renameAttachments);
}
}
}
@ -2971,10 +2970,11 @@ var ZoteroPane = new function()
this.updateAttachmentButtonMenu(popup);
// Block certain actions on files if no access
if (item.isImportedAttachment() && !collectionTreeRow.filesEditable) {
[m.moveToTrash, m.createParent, m.renameAttachments].forEach(function (x) {
disable.push(x);
});
if (item.isFileAttachment() && !collectionTreeRow.filesEditable) {
[m.moveToTrash, m.createParent, m.renameAttachments]
.forEach(function (x) {
disable.push(x);
});
}
}
}
@ -4580,6 +4580,69 @@ var ZoteroPane = new function()
});
this.convertLinkedFilesToStoredFiles = async function () {
if (!this.canEdit() || !this.canEditFiles()) {
this.displayCannotEditLibraryMessage();
return;
}
var items = this.getSelectedItems();
var attachments = new Set();
for (let item of items) {
// Add all child link attachments of regular items
if (item.isRegularItem()) {
for (let id of item.getAttachments()) {
let attachment = await Zotero.Items.getAsync(id);
if (attachment.isLinkedFileAttachment()) {
attachments.add(attachment);
}
}
}
// And all selected link attachments
else if (item.isLinkedFileAttachment()) {
attachments.add(item);
}
}
var num = attachments.size;
var ps = Services.prompt;
var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
var deleteOriginal = {};
var index = ps.confirmEx(null,
Zotero.getString('attachment.convertToStored.title', [num], num),
Zotero.getString('attachment.convertToStored.text', [num], num),
buttonFlags,
Zotero.getString('general.continue'),
null,
null,
Zotero.getString('attachment.convertToStored.deleteOriginal', [num], num),
deleteOriginal
);
if (index != 0) {
return;
}
for (let item of attachments) {
try {
let converted = await Zotero.Attachments.convertLinkedFileToStoredFile(
item,
{
move: deleteOriginal.value
}
);
if (!converted) {
// Not found
continue;
}
}
catch (e) {
Zotero.logError(e);
continue;
}
}
};
this.relinkAttachment = Zotero.Promise.coroutine(function* (itemID) {
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();

View file

@ -284,6 +284,7 @@
<menuitem class="menuitem-iconic zotero-menuitem-report-metadata" label="&zotero.items.menu.reportMetadata;" oncommand="ZoteroPane.reportMetadataForSelected()"/>
<menuitem class="menuitem-iconic zotero-menuitem-create-parent" oncommand="ZoteroPane_Local.createParentItemsFromSelected();"/>
<menuitem class="menuitem-iconic zotero-menuitem-rename-from-parent" oncommand="ZoteroPane_Local.renameSelectedAttachmentsFromParents()"/>
<menuitem class="menuitem-iconic zotero-menuitem-convert-linked-to-stored" oncommand="ZoteroPane.convertLinkedFilesToStoredFiles()"/>
<menuitem class="menuitem-iconic zotero-menuitem-reindex" oncommand="ZoteroPane_Local.reindexItem();"/>
</menupopup>
</popupset>

View file

@ -24,6 +24,8 @@
<!ENTITY closeCmd.label "Close">
<!ENTITY closeCmd.key "W">
<!ENTITY closeCmd.accesskey "C">
<!ENTITY manageAttachments.label "Manage Attachments">
<!ENTITY convertToStored.label "Convert Linked Files to Stored Files…">
<!ENTITY importCmd.label "Import…">
<!ENTITY importCmd.key "I">
<!ENTITY importFromClipboardCmd.label "Import from Clipboard">

View file

@ -613,6 +613,9 @@ findPDF.noPDFFound = No PDF found
attachment.fullText = Full Text
attachment.acceptedVersion = Accepted Version
attachment.submittedVersion = Submitted Version
attachment.convertToStored.title = Convert to Stored File;Convert to Stored Files
attachment.convertToStored.text = %1$S attachment will be converted from a linked file to a stored file.;%1$S attachments will be converted from linked files to stored files.
attachment.convertToStored.deleteOriginal = Delete original file after storing;Delete original files after storing
db.dbCorrupted = The Zotero database '%S' appears to have become corrupted.
db.dbCorrupted.restart = Please restart %S to attempt an automatic restore from the last backup.

View file

@ -1074,4 +1074,71 @@ describe("Zotero.Attachments", function() {
assert.isTrue(yield OS.File.exists(dir));
});
});
describe("#convertLinkedFileToStoredFile()", function () {
it("should copy a linked file to a stored file", async function () {
var item = await createDataObject('item');
var relatedItem = await createDataObject('item');
var originalFile = OS.Path.join(getTestDataDirectory().path, 'test.pdf');
var attachment = await Zotero.Attachments.linkFromFile({
file: originalFile,
title: 'Title',
parentItemID: item.id
});
attachment.setNote('Note');
attachment.setTags([{ tag: 'Tag' }]);
attachment.addRelatedItem(relatedItem);
await attachment.saveTx();
relatedItem.addRelatedItem(attachment);
await relatedItem.saveTx();
// Make sure we're indexed
await Zotero.Fulltext.indexItems([attachment.id]);
var newAttachment = await Zotero.Attachments.convertLinkedFileToStoredFile(attachment);
assert.isFalse(Zotero.Items.exists(attachment.id));
assert.isTrue(await OS.File.exists(originalFile));
assert.equal(newAttachment.attachmentLinkMode, Zotero.Attachments.LINK_MODE_IMPORTED_FILE);
assert.equal(newAttachment.attachmentContentType, 'application/pdf');
assert.isTrue(await newAttachment.fileExists());
assert.equal(newAttachment.getField('title'), 'Title');
assert.equal(newAttachment.getNote(), 'Note');
assert.sameDeepMembers(newAttachment.getTags(), [{ tag: 'Tag' }]);
assert.sameMembers(newAttachment.relatedItems, [relatedItem.key]);
assert.sameMembers(relatedItem.relatedItems, [newAttachment.key]);
assert.isTrue(await OS.File.exists(Zotero.Fulltext.getItemCacheFile(newAttachment).path));
assert.equal(
await Zotero.Fulltext.getIndexedState(newAttachment),
Zotero.Fulltext.INDEX_STATE_INDEXED
);
});
it("should move a linked file to a stored file with `move: true`", async function () {
var item = await createDataObject('item');
var originalFile = OS.Path.join(Zotero.getTempDirectory().path, 'test.png');
await OS.File.copy(
OS.Path.join(getTestDataDirectory().path, 'test.png'),
originalFile
);
var attachment = await Zotero.Attachments.linkFromFile({
file: originalFile,
parentItemID: item.id
});
var newAttachment = await Zotero.Attachments.convertLinkedFileToStoredFile(
attachment,
{
move: true
}
);
assert.isFalse(Zotero.Items.exists(attachment.id));
assert.isFalse(await OS.File.exists(originalFile));
assert.equal(newAttachment.attachmentLinkMode, Zotero.Attachments.LINK_MODE_IMPORTED_FILE);
assert.isTrue(await newAttachment.fileExists());
});
});
})