Add an option to create parent item from identifier (#1901)

This commit is contained in:
fletcherhaz 2020-11-20 14:17:48 -07:00 committed by GitHub
parent f393a233e9
commit 86b77cc45e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 429 additions and 54 deletions

View file

@ -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);
};

View 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();
}

View 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>

View file

@ -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) {

View file

@ -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* () {

View file

@ -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">

View file

@ -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

View file

@ -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";

View 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;
}
}
}
}

View 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;
}
}

View 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;
}

View 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;
}

View 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;
}

View file

@ -5,6 +5,7 @@
// --------------------------------------------------
@import "mac/button";
@import "mac/createParent";
@import "mac/editable";
@import "mac/search";
@import "mac/tag-selector";

View file

@ -4,6 +4,7 @@
// Linux specific
// --------------------------------------------------
@import "linux/createParent";
@import "linux/editable";
@import "linux/search";
@import "linux/tagsBox";

View file

@ -4,5 +4,6 @@
// Windows specific
// --------------------------------------------------
@import "win/createParent";
@import "win/search";
@import "win/tag-selector";