Add an option to create parent item from identifier (#1901)
This commit is contained in:
parent
f393a233e9
commit
86b77cc45e
16 changed files with 429 additions and 54 deletions
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2020 Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
https://digitalscholar.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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { IntlProvider } from "react-intl";
|
||||
|
||||
function CreateParent({ loading, item, toggleAccept }) {
|
||||
// When the input has/does not have characters toggle the accept button on the dialog
|
||||
const handleInput = (e) => {
|
||||
if (e.target.value.trim() !== '') {
|
||||
toggleAccept(true);
|
||||
}
|
||||
else {
|
||||
toggleAccept(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={ Zotero.locale }
|
||||
messages={ Zotero.Intl.strings }
|
||||
>
|
||||
<div className="create-parent-container">
|
||||
<span className="title">
|
||||
{ item.attachmentFilename }
|
||||
</span>
|
||||
<div className="body">
|
||||
<input
|
||||
id="parent-item-identifier"
|
||||
placeholder={ Zotero.getString('createParent.prompt') }
|
||||
size="50"
|
||||
disabled={ loading }
|
||||
onChange={ handleInput }
|
||||
/>
|
||||
<div
|
||||
mode="undetermined"
|
||||
className={ cx('downloadProgress', { hidden: !loading }) }
|
||||
>
|
||||
<div className="progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
CreateParent.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
item: PropTypes.object,
|
||||
toggleAccept: PropTypes.func
|
||||
};
|
||||
|
||||
Zotero.CreateParent = memo(CreateParent);
|
||||
|
||||
|
||||
Zotero.CreateParent.destroy = (domEl) => {
|
||||
ReactDOM.unmountComponentAtNode(domEl);
|
||||
};
|
||||
|
||||
|
||||
Zotero.CreateParent.render = (domEl, props) => {
|
||||
ReactDOM.render(<CreateParent { ...props } />, domEl);
|
||||
};
|
80
chrome/content/zotero/createParentDialog.js
Normal file
80
chrome/content/zotero/createParentDialog.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2009 Center for History and New Media
|
||||
George Mason University, Fairfax, Virginia, USA
|
||||
http://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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var io;
|
||||
let createParent;
|
||||
|
||||
function toggleAccept(enabled) {
|
||||
document.documentElement.getButton("accept").disabled = !enabled;
|
||||
}
|
||||
|
||||
function doLoad() {
|
||||
// Set font size from pref
|
||||
let sbc = document.getElementById('zotero-create-parent-container');
|
||||
Zotero.setFontSize(sbc);
|
||||
|
||||
io = window.arguments[0];
|
||||
|
||||
createParent = document.getElementById('create-parent');
|
||||
Zotero.CreateParent.render(createParent, {
|
||||
loading: false,
|
||||
item: io.dataIn.item,
|
||||
toggleAccept
|
||||
});
|
||||
}
|
||||
|
||||
function doUnload() {
|
||||
Zotero.CreateParent.destroy(createParent);
|
||||
}
|
||||
|
||||
async function doAccept() {
|
||||
let textBox = document.getElementById('parent-item-identifier');
|
||||
let childItem = io.dataIn.item;
|
||||
let newItems = await Zotero_Lookup.addItemsFromIdentifier(
|
||||
textBox,
|
||||
childItem,
|
||||
(on) => {
|
||||
// Render react again with correct loading value
|
||||
Zotero.CreateParent.render(createParent, {
|
||||
loading: on,
|
||||
item: childItem,
|
||||
toggleAccept
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// If we successfully created a parent, return it
|
||||
if (newItems) {
|
||||
io.dataOut = { parent: newItems[0] };
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
function doManualEntry() {
|
||||
io.dataOut = { parent: false };
|
||||
window.close();
|
||||
}
|
34
chrome/content/zotero/createParentDialog.xul
Normal file
34
chrome/content/zotero/createParentDialog.xul
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
|
||||
|
||||
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
|
||||
|
||||
<dialog
|
||||
id="zotero-parent-dialog"
|
||||
title="&zotero.createParent.title;"
|
||||
orient="vertical"
|
||||
buttons="cancel,accept,extra2"
|
||||
buttondisabledaccept="true"
|
||||
buttonlabelextra2="&zotero.createParent.button.manual;"
|
||||
buttonlabelaccept="&zotero.createParent.title;"
|
||||
ondialogaccept="doAccept();return false;"
|
||||
ondialogextra2="doManualEntry();"
|
||||
onload="doLoad();"
|
||||
onunload="doUnload();"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
style="padding:20px 15px;width:400px;">
|
||||
|
||||
<script src="include.js"/>
|
||||
<script src="lookup.js"/>
|
||||
<script src="createParentDialog.js"/>
|
||||
<script src="components/createParent/createParent.js"/>
|
||||
|
||||
<vbox id="zotero-create-parent-container" flex="1">
|
||||
<html:div id="create-parent" />
|
||||
</vbox>
|
||||
</dialog>
|
|
@ -29,9 +29,19 @@
|
|||
*/
|
||||
var Zotero_Lookup = new function () {
|
||||
/**
|
||||
* Performs a lookup by DOI, PMID, or ISBN
|
||||
* Performs a lookup by DOI, PMID, or ISBN on the given textBox value
|
||||
* and adds any items it can.
|
||||
*
|
||||
* If a childItem is passed, then only one identifier is allowed, the
|
||||
* child's library/collection information is used and no attachments are
|
||||
* saved for the parent.
|
||||
*
|
||||
* @param textBox {HTMLElement} - Textbox containing identifiers
|
||||
* @param childItem {Zotero.Item|false} - Child item (optional)
|
||||
* @param toggleProgress {function} - Callback to toggle progress on/off
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
this.accept = Zotero.Promise.coroutine(function* (textBox) {
|
||||
this.addItemsFromIdentifier = async function (textBox, childItem, toggleProgress) {
|
||||
var identifiers = Zotero.Utilities.Internal.extractIdentifiers(textBox.value);
|
||||
if (!identifiers.length) {
|
||||
Zotero.alert(
|
||||
|
@ -41,56 +51,85 @@ var Zotero_Lookup = new function () {
|
|||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
var libraryID = false;
|
||||
var collection = false;
|
||||
try {
|
||||
libraryID = ZoteroPane_Local.getSelectedLibraryID();
|
||||
collection = ZoteroPane_Local.getSelectedCollection();
|
||||
} catch(e) {
|
||||
/** TODO: handle this **/
|
||||
else if (childItem && identifiers.length > 1) {
|
||||
// Only allow one identifier when creating a parent for a child
|
||||
Zotero.alert(
|
||||
window,
|
||||
Zotero.getString("lookup.failure.title"),
|
||||
Zotero.getString("lookup.failureTooMany.description")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
var successful = 0; //counter for successful retrievals
|
||||
var libraryID = false;
|
||||
var collections = false;
|
||||
if (childItem) {
|
||||
libraryID = childItem.libraryID;
|
||||
collections = childItem.collections;
|
||||
}
|
||||
else {
|
||||
try {
|
||||
libraryID = ZoteroPane.getSelectedLibraryID();
|
||||
let collection = ZoteroPane.getSelectedCollection();
|
||||
collections = collection ? [collection.id] : false;
|
||||
}
|
||||
catch (e) {
|
||||
/** TODO: handle this **/
|
||||
}
|
||||
}
|
||||
|
||||
Zotero_Lookup.toggleProgress(true);
|
||||
let newItems = false;
|
||||
toggleProgress(true);
|
||||
|
||||
for (let identifier of identifiers) {
|
||||
await Zotero.Promise.all(identifiers.map(async (identifier) => {
|
||||
var translate = new Zotero.Translate.Search();
|
||||
translate.setIdentifier(identifier);
|
||||
|
||||
// be lenient about translators
|
||||
let translators = yield translate.getTranslators();
|
||||
let translators = await translate.getTranslators();
|
||||
translate.setTranslator(translators);
|
||||
|
||||
try {
|
||||
let newItems = yield translate.translate({
|
||||
newItems = await translate.translate({
|
||||
libraryID,
|
||||
collections: collection ? [collection.id] : false
|
||||
collections,
|
||||
saveAttachments: !childItem
|
||||
});
|
||||
successful++;
|
||||
}
|
||||
// Continue with other ids on failure
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Zotero_Lookup.toggleProgress(false);
|
||||
// TODO: Give indication if some failed
|
||||
if (successful) {
|
||||
document.getElementById("zotero-lookup-panel").hidePopup();
|
||||
}
|
||||
else {
|
||||
toggleProgress(false);
|
||||
if (!newItems) {
|
||||
Zotero.alert(
|
||||
window,
|
||||
Zotero.getString("lookup.failure.title"),
|
||||
Zotero.getString("lookup.failure.description")
|
||||
);
|
||||
}
|
||||
// TODO: Give indication if some, but not all failed
|
||||
|
||||
return newItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Try a lookup and hide popup if successful
|
||||
*/
|
||||
this.accept = async function (textBox) {
|
||||
let newItems = await Zotero_Lookup.addItemsFromIdentifier(
|
||||
textBox,
|
||||
false,
|
||||
on => Zotero_Lookup.toggleProgress(on)
|
||||
);
|
||||
|
||||
if (newItems) {
|
||||
document.getElementById("zotero-lookup-panel").hidePopup();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
this.showPanel = function (button) {
|
||||
|
|
|
@ -2821,24 +2821,12 @@ var ZoteroPane = new function()
|
|||
show.push(m.findPDF, m.sep3);
|
||||
}
|
||||
|
||||
var canCreateParent = true;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let item = items[i];
|
||||
if (!item.isTopLevelItem() || !item.isAttachment() || item.isFeedItem) {
|
||||
canCreateParent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (canCreateParent) {
|
||||
show.push(m.createParent);
|
||||
}
|
||||
|
||||
if (canRename) {
|
||||
show.push(m.renameAttachments);
|
||||
}
|
||||
|
||||
// Add in attachment separator
|
||||
if (canCreateParent || canRecognize || canUnrecognize || canRename || canIndex) {
|
||||
if (canRecognize || canUnrecognize || canRename || canIndex) {
|
||||
show.push(m.sep5);
|
||||
}
|
||||
|
||||
|
@ -2849,7 +2837,6 @@ var ZoteroPane = new function()
|
|||
if (item.isFileAttachment()) {
|
||||
disable.push(
|
||||
m.moveToTrash,
|
||||
m.createParent,
|
||||
m.renameAttachments
|
||||
);
|
||||
break;
|
||||
|
@ -4517,20 +4504,33 @@ var ZoteroPane = new function()
|
|||
};
|
||||
|
||||
|
||||
this.createParentItemsFromSelected = Zotero.Promise.coroutine(function* () {
|
||||
this.createParentItemsFromSelected = async function () {
|
||||
if (!this.canEdit()) {
|
||||
this.displayCannotEditLibraryMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
var items = this.getSelectedItems();
|
||||
for (var i=0; i<items.length; i++) {
|
||||
var item = items[i];
|
||||
if (!item.isTopLevelItem() || item.isRegularItem()) {
|
||||
throw('Item ' + itemID + ' is not a top-level attachment or note in ZoteroPane_Local.createParentItemsFromSelected()');
|
||||
}
|
||||
let item = this.getSelectedItems()[0];
|
||||
if (!item.isAttachment() || !item.isTopLevelItem()) {
|
||||
throw new Error('Item ' + itemID + ' is not a top-level attachment');
|
||||
}
|
||||
|
||||
yield Zotero.DB.executeTransaction(function* () {
|
||||
let io = { dataIn: { item }, dataOut: null };
|
||||
window.openDialog('chrome://zotero/content/createParentDialog.xul', '', 'chrome,modal,centerscreen', io);
|
||||
if (!io.dataOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we made a parent, attach the child
|
||||
if (io.dataOut.parent) {
|
||||
await Zotero.DB.executeTransaction(function* () {
|
||||
item.parentID = io.dataOut.parent.id;
|
||||
yield item.save();
|
||||
});
|
||||
}
|
||||
// If they clicked manual entry then make a dummy parent
|
||||
else {
|
||||
await Zotero.DB.executeTransaction(function* () {
|
||||
// TODO: remove once there are no top-level web attachments
|
||||
if (item.isWebAttachment()) {
|
||||
var parent = new Zotero.Item('webpage');
|
||||
|
@ -4549,7 +4549,7 @@ var ZoteroPane = new function()
|
|||
yield item.save();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
this.renameSelectedAttachmentsFromParents = Zotero.Promise.coroutine(function* () {
|
||||
|
|
|
@ -175,6 +175,9 @@
|
|||
<!ENTITY zotero.lookup.description "Enter ISBNs, DOIs, PMIDs, or arXiv IDs to add to your library:">
|
||||
<!ENTITY zotero.lookup.button.search "Search">
|
||||
|
||||
<!ENTITY zotero.createParent.title "Create Parent Item">
|
||||
<!ENTITY zotero.createParent.button.manual "Manual Entry">
|
||||
|
||||
<!ENTITY zotero.selectitems.title "Select Items">
|
||||
<!ENTITY zotero.selectitems.intro.label "Select which items you'd like to add to your library">
|
||||
<!ENTITY zotero.selectitems.cancel.label "Cancel">
|
||||
|
|
|
@ -1148,6 +1148,9 @@ file.error.cannotAddShortcut = Shortcut files cannot be added directly. Pl
|
|||
lookup.failure.title = Lookup Failed
|
||||
lookup.failure.description = Zotero could not find a record for the specified identifier. Please verify the identifier and try again.
|
||||
lookup.failureToID.description = Zotero could not find any identifiers in your input. Please verify your input and try again.
|
||||
lookup.failureTooMany.description = Too many identifiers. Please enter one identifier and try again.
|
||||
|
||||
createParent.prompt = Enter a DOI, ISBN, PMID, or arXiv ID to identify this file
|
||||
|
||||
locate.online.label = View Online
|
||||
locate.online.tooltip = Go to this item online
|
||||
|
|
|
@ -22,8 +22,10 @@
|
|||
|
||||
@import "components/autosuggest";
|
||||
@import "components/button";
|
||||
@import "components/createParent";
|
||||
@import "components/editable";
|
||||
@import "components/icons";
|
||||
@import "components/progressMeter";
|
||||
@import "components/search";
|
||||
@import "components/tagsBox";
|
||||
@import "components/tagSelector";
|
||||
|
|
35
scss/components/_createParent.scss
Normal file
35
scss/components/_createParent.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
.create-parent-container {
|
||||
.title {
|
||||
font-size: 1.4em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin: 1em 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.downloadProgress {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: -1em;
|
||||
transform: translateY(-100%);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
scss/components/_progressMeter.scss
Normal file
72
scss/components/_progressMeter.scss
Normal file
|
@ -0,0 +1,72 @@
|
|||
// From https://dxr.mozilla.org/mozilla-esr60/source/browser/themes/shared/downloads/progressmeter.inc.css
|
||||
|
||||
/*** Common-styled progressmeter ***/
|
||||
.downloadProgress {
|
||||
height: 8px;
|
||||
border-radius: 1px;
|
||||
margin: 4px 0 0;
|
||||
margin-inline-end: 12px;
|
||||
|
||||
/* for overriding rules in progressmeter.css */
|
||||
-moz-appearance: none;
|
||||
border-style: none;
|
||||
background-color: transparent;
|
||||
min-width: initial;
|
||||
min-height: initial;
|
||||
}
|
||||
|
||||
.downloadProgress[mode="undetermined"] {
|
||||
/* for overriding rules on global.css in Linux. */
|
||||
-moz-binding: url("chrome://global/content/bindings/progressmeter.xml#progressmeter");
|
||||
}
|
||||
|
||||
.downloadProgress > .progress-bar {
|
||||
background-color: Highlight;
|
||||
|
||||
/* for overriding rules in progressmeter.css */
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
.downloadProgress[paused="true"] > .progress-bar {
|
||||
background-color: GrayText;
|
||||
}
|
||||
|
||||
.downloadProgress[mode="undetermined"] > .progress-bar {
|
||||
/* Make a white reflecting animation.
|
||||
Create a gradient with 2 identical pattern, and enlarge the size to 200%.
|
||||
This allows us to animate background-position with percentage. */
|
||||
background-image: linear-gradient(90deg, transparent 0%,
|
||||
rgba(255,255,255,0.5) 25%,
|
||||
transparent 50%,
|
||||
rgba(255,255,255,0.5) 75%,
|
||||
transparent 100%);
|
||||
background-blend-mode: lighten;
|
||||
background-size: 200% 100%;
|
||||
animation: downloadProgressSlideX 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.downloadProgress > .progress-remainder {
|
||||
border: solid ButtonShadow;
|
||||
border-block-start-width: 1px;
|
||||
border-block-end-width: 1px;
|
||||
border-inline-start-width: 0;
|
||||
border-inline-end-width: 1px;
|
||||
background-color: ButtonFace;
|
||||
}
|
||||
|
||||
.downloadProgress[value="0"] > .progress-remainder {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.downloadProgress > .progress-remainder[mode="undetermined"] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@keyframes downloadProgressSlideX {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
}
|
4
scss/linux/_createParent.scss
Normal file
4
scss/linux/_createParent.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
.create-parent-container {
|
||||
// This matches the margin on the buttons in the dialog box, which are platform dependent
|
||||
padding: 0 5px;
|
||||
}
|
5
scss/mac/_createParent.scss
Normal file
5
scss/mac/_createParent.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.create-parent-container {
|
||||
// This matches the margin on the buttons in the dialog box, which are platform dependent
|
||||
padding-left: 8px;
|
||||
padding-right: 6px;
|
||||
}
|
4
scss/win/_createParent.scss
Normal file
4
scss/win/_createParent.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
.create-parent-container {
|
||||
// This matches the margin on the buttons in the dialog box, which are platform dependent
|
||||
padding: 0 5px;
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
// --------------------------------------------------
|
||||
|
||||
@import "mac/button";
|
||||
@import "mac/createParent";
|
||||
@import "mac/editable";
|
||||
@import "mac/search";
|
||||
@import "mac/tag-selector";
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
// Linux specific
|
||||
// --------------------------------------------------
|
||||
|
||||
@import "linux/createParent";
|
||||
@import "linux/editable";
|
||||
@import "linux/search";
|
||||
@import "linux/tagsBox";
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
// Windows specific
|
||||
// --------------------------------------------------
|
||||
|
||||
@import "win/createParent";
|
||||
@import "win/search";
|
||||
@import "win/tag-selector";
|
||||
|
|
Loading…
Reference in a new issue