carbon-tokenization / frontend /src /editor /frontmatter /AuthorInlineForm.tsx
tfrere's picture
tfrere HF Staff
feat(editor): authors & affiliations manager modal
1f88239
Raw
History Blame Contribute Delete
4.46 kB
import { useState, useRef, useEffect } from "react";
import type { Author, Affiliation } from "./frontmatter-store";
/**
* Add/Edit form for a single author, rendered as a native modal dialog.
*
* Affiliations are selected by their 1-based index (matching the store's
* `author.affiliations` model). A free-text "new affiliation" field lets the
* user create + attach an affiliation in one go; the caller persists it via
* `onSubmit(author, newAffiliation)`.
*
* Extracted from FrontmatterHero so both the hero shortcut and the
* AuthorsManager modal share one tested form.
*/
export function AuthorInlineForm({
open,
initial,
affiliations,
onSubmit,
onCancel,
}: {
open: boolean;
initial?: Author;
affiliations: Affiliation[];
onSubmit: (author: Author, newAffiliation?: Affiliation) => void;
onCancel: () => void;
}) {
const [name, setName] = useState(initial?.name || "");
const [url, setUrl] = useState(initial?.url || "");
const [affIndices, setAffIndices] = useState<number[]>(initial?.affiliations || []);
const [newAffName, setNewAffName] = useState("");
const nameRef = useRef<HTMLInputElement>(null);
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (open && !dialog.open) {
setName(initial?.name || "");
setUrl(initial?.url || "");
setAffIndices(initial?.affiliations || []);
setNewAffName("");
dialog.showModal();
setTimeout(() => nameRef.current?.focus(), 50);
} else if (!open && dialog.open) {
dialog.close();
}
}, [open, initial]);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
const onClose = () => onCancel();
dialog.addEventListener("close", onClose);
return () => dialog.removeEventListener("close", onClose);
}, [onCancel]);
const handleSubmit = () => {
if (!name.trim()) return;
const newAff = newAffName.trim() ? { name: newAffName.trim() } : undefined;
onSubmit(
{ name: name.trim(), url: url.trim() || undefined, affiliations: affIndices },
newAff,
);
};
const handleCancel = () => {
dialogRef.current?.close();
onCancel();
};
const toggleAff = (idx: number) => {
setAffIndices((prev) =>
prev.includes(idx) ? prev.filter((i) => i !== idx) : [...prev, idx],
);
};
return (
<dialog ref={dialogRef} className="ed-dialog ed-dialog--author">
<h3 className="ed-dialog__title">
{initial ? "Edit author" : "Add author"}
</h3>
<div className="ed-dialog__body">
<div className="author-form__row">
<input
ref={nameRef}
className="form-input"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
/>
<input
className="form-input"
placeholder="URL (optional)"
value={url}
onChange={(e) => setUrl(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
/>
</div>
{affiliations.length > 0 && (
<div className="author-form__chips" style={{ marginTop: 12 }}>
{affiliations.map((aff, i) => (
<span
key={`aff-select-${i}`}
className={`chip chip--clickable ${affIndices.includes(i + 1) ? "" : "chip--outlined"}`}
style={affIndices.includes(i + 1) ? { background: "var(--primary-color)", color: "#000" } : {}}
onClick={() => toggleAff(i + 1)}
>
{i + 1}. {aff.name}
</span>
))}
</div>
)}
<input
className="form-input"
placeholder="New affiliation (optional), e.g. Hugging Face"
value={newAffName}
onChange={(e) => setNewAffName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
style={{ marginTop: 12 }}
/>
</div>
<div className="ed-dialog__actions">
<button className="btn" onClick={handleCancel}>Cancel</button>
<button
className="btn btn--primary"
onClick={handleSubmit}
disabled={!name.trim()}
>
{initial ? "Save" : "Add"}
</button>
</div>
</dialog>
);
}