Re-implement "Create Parent Dialog" without using React for 7.0 (#5381)

* Fixed an issue where the window size did not match the content
* Replaced the semi-transparent progress bar with a spinner
* Fixed a problem that allowed triggering a search while one was already in progress
* Reduced code complexity
This commit is contained in:
Tom Najdek 2025-07-15 03:21:07 +02:00 committed by GitHub
parent 36e0c6469c
commit c9efc33d9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 75 additions and 178 deletions

View file

@ -1,91 +0,0 @@
/*
***** 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, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
function CreateParent({ loading, item, toggleAccept }) {
// With React 18, this is required for the window's dialog to be properly sized
const ref = useRef();
useEffect(() => {
// Wait for Fluent to inject translated strings before resizing the dialog (fixes #5365).
const observer = new MutationObserver(() => window.sizeToContent());
observer.observe(ref.current, { childList: true, subtree: true });
return () => {
observer.disconnect();
};
}, []);
// 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 (
<div className="create-parent-container" ref={ ref }>
<div className="title">
{ item.attachmentFilename }
</div>
<p className="intro" data-l10n-id="create-parent-intro"/>
<div className="body">
<input
id="parent-item-identifier"
size="50"
autoFocus={true}
disabled={loading}
onChange={handleInput}
/>
<div
mode="undetermined"
className={cx('downloadProgress', { hidden: !loading })}
>
<div className="progress-bar"></div>
</div>
</div>
</div>
);
}
CreateParent.propTypes = {
loading: PropTypes.bool,
item: PropTypes.object,
toggleAccept: PropTypes.func
};
Zotero.CreateParent = memo(CreateParent);
Zotero.CreateParent.render = (root, props) => {
root.render(<CreateParent { ...props } />);
};

View file

@ -23,68 +23,73 @@
***** END LICENSE BLOCK *****
*/
"use strict";
/* global Zotero_Lookup: false */
import ReactDOM from "react-dom";
const ZoteroCreateParentDialog = { // eslint-disable-line no-unused-vars
init() {
this.io = window.arguments[0];
var io;
let createParent;
let root;
this.inputEl = document.getElementById('parent-item-identifier');
this.progressEl = document.getElementById('progress');
this.acceptBtnEl = document.querySelector('dialog').getButton("accept");
this.manualEntryBtnEl = document.querySelector('dialog').getButton("extra2");
function toggleAccept(enabled) {
document.querySelector('dialog').getButton("accept").disabled = !enabled;
}
// Set font size from pref
Zotero.UIProperties.registerRoot(
document.getElementById('zotero-create-parent-container')
);
function doLoad() {
// Set font size from pref
let sbc = document.getElementById('zotero-create-parent-container');
Zotero.UIProperties.registerRoot(sbc);
this.inputEl.addEventListener('input', this.handleInput.bind(this));
document.addEventListener('dialogaccept', this.handleAcceptClick.bind(this));
document.addEventListener('dialogextra2', this.handleManualEntry.bind(this));
io = window.arguments[0];
document.getElementById('title').textContent = this.io.dataIn.item.attachmentFilename;
this.inputEl.focus();
},
createParent = document.getElementById('create-parent');
root = ReactDOM.createRoot(createParent);
Zotero.CreateParent.render(root, {
loading: false,
item: io.dataIn.item,
toggleAccept
});
async performLookup() {
let newItems = await Zotero_Lookup.addItemsFromIdentifier(
this.inputEl,
this.io.dataIn.item,
this.handleStatusChange.bind(this)
);
document.addEventListener('dialogaccept', (event) => {
doAccept();
event.preventDefault();
});
document.addEventListener('dialogextra2', doManualEntry);
}
function doUnload() {
root.unmount();
}
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(root, {
loading: on,
item: childItem,
toggleAccept
});
// If we successfully created a parent, return it
if (newItems.length) {
this.io.dataOut = { parent: newItems[0] };
window.close();
}
);
},
// If we successfully created a parent, return it
if (newItems.length) {
io.dataOut = { parent: newItems[0] };
handleInput(event) {
const input = event.target.value.trim();
this.acceptBtnEl.disabled = input === '';
},
handleStatusChange(isLookingUp) {
this.inputEl.disabled = isLookingUp;
this.acceptBtnEl.disabled = isLookingUp;
this.manualEntryBtnEl.disabled = isLookingUp;
if (isLookingUp) {
this.progressEl.setAttribute("status", "animate");
}
else {
this.progressEl.removeAttribute("status");
}
},
handleAcceptClick(ev) {
ev.preventDefault();
if (this.inputEl.value.trim() === '') {
return;
}
this.performLookup();
},
handleManualEntry() {
this.io.dataOut = { parent: false };
window.close();
}
}
function doManualEntry() {
io.dataOut = { parent: false };
window.close();
}
};

View file

@ -14,8 +14,8 @@
xmlns:html="http://www.w3.org/1999/xhtml"
title="&zotero.createParent.title;"
drawintitlebar-platforms="mac"
onload="doLoad();"
onunload="doUnload();">
onload="ZoteroCreateParentDialog.init()"
>
<dialog
id="zotero-parent-dialog"
orient="vertical"
@ -31,12 +31,16 @@
<script src="editMenuOverlay.js"/>
<script src="lookup.js"/>
<script src="createParentDialog.js"/>
<script src="components/createParent/createParent.js"/>
<html:link rel="localization" href="zotero.ftl"/>
<vbox id="zotero-create-parent-container" flex="1">
<html:div id="create-parent" />
<vbox id="zotero-create-parent-container">
<html:h1 id="title" class="title" />
<html:label for="parent-item-identifier" class="intro" id="intro" data-l10n-id="create-parent-intro"/>
<html:div class="body">
<html:input type="text" id="parent-item-identifier" aria-labelledby="intro" />
<image id="progress" class="zotero-spinner-16" />
</html:div>
</vbox>
</dialog>
</window>

View file

@ -1,4 +1,4 @@
.create-parent-container {
#zotero-create-parent-container {
.title {
font-size: 1.4em;
font-weight: 700;
@ -7,39 +7,18 @@
}
.intro {
// Text is added asynchonously, so set a height for dialog auto-sizing
min-height: 1em;
margin-bottom: .5em;
max-width: 500px; // Avoid expanding the dialog too wide; instead, in some languages, the intro text will wrap.
max-width: 550px; // Avoid expanding the dialog too wide; instead, in some languages, the intro text will wrap.
}
.body {
position: relative;
align-items: center;
display: flex;
margin-bottom: 1em;
input {
flex: 1;
font-size: 14px;
margin-left: 0;
}
.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

@ -1,4 +1,4 @@
.create-parent-container {
#zotero-create-parent-container {
// This matches the margin on the buttons in the dialog box, which are platform dependent
padding: 0 5px;
}

View file

@ -1,4 +1,4 @@
.create-parent-container {
#zotero-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

@ -1,4 +1,4 @@
.create-parent-container {
#zotero-create-parent-container {
// This matches the margin on the buttons in the dialog box, which are platform dependent
padding: 0 5px;
}

View file

@ -1731,7 +1731,7 @@ describe("ZoteroPane", function() {
let parent;
let dialogPromise = waitForDialog(async (win) => {
parent = await createDataObject('item', { title: 'Book Title' });
win.io.dataOut = { parent };
win.arguments[0].dataOut = { parent };
win.close();
}, false, 'chrome://zotero/content/createParentDialog.xhtml');
let createParentPromise = zp.createParentItemsFromSelected();