From 5041b3978507e402be579443db6bfbcfa97e0412 Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 31 May 2026 10:59:17 +0100 Subject: [PATCH 1/2] Add button to add new candidate to crossmatch --- src/pages/RecordCrossmatchDetails.tsx | 117 ++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 5 deletions(-) diff --git a/src/pages/RecordCrossmatchDetails.tsx b/src/pages/RecordCrossmatchDetails.tsx index 1de0041..f8b1df0 100644 --- a/src/pages/RecordCrossmatchDetails.tsx +++ b/src/pages/RecordCrossmatchDetails.tsx @@ -36,6 +36,7 @@ import { RightAscension, } from "../components/core/Astronomy"; import classNames from "classnames"; +import { MdAdd, MdClose } from "react-icons/md"; function convertAdminSchemaToBackendSchema( adminSchema: AdminSchema, @@ -233,9 +234,11 @@ interface ResolutionSelectorProps { schema: BackendSchema; showResolveControls: boolean; resolving: ResolutionChoice | null; + addingCandidate: boolean; selected: ResolutionChoice | null; onSelect: (choice: ResolutionChoice) => void; onSubmit: () => void; + onAddCandidate: (pgc: number) => Promise; } function ResolutionSelector({ @@ -244,13 +247,41 @@ function ResolutionSelector({ schema, showResolveControls, resolving, + addingCandidate, selected, onSelect, onSubmit, + onAddCandidate, }: ResolutionSelectorProps): ReactElement { + const [showAddCandidate, setShowAddCandidate] = useState(false); + const [pgcInput, setPgcInput] = useState(""); + const [addCandidateError, setAddCandidateError] = useState( + null, + ); + const busy = resolving !== null || addingCandidate; const matchedPgc = crossmatch.status === "existing" ? crossmatch.metadata.pgc : null; + async function submitNewCandidate(): Promise { + const pgc = Number.parseInt(pgcInput.trim(), 10); + if (!Number.isFinite(pgc) || pgc <= 0) { + setAddCandidateError("Enter a valid PGC number"); + return; + } + + if (candidates.some((candidate) => candidate.pgc === pgc)) { + setAddCandidateError("This PGC is already a candidate"); + return; + } + + setAddCandidateError(null); + try { + await onAddCandidate(pgc); + } catch (err) { + setAddCandidateError(`${err}`); + } + } + function renderCandidateSummary(candidate: PgcCandidate): ReactElement { return (
@@ -316,7 +347,7 @@ function ResolutionSelector({ name="crossmatch-resolution" className="shrink-0" checked={selected === "new"} - disabled={resolving !== null} + disabled={busy} onChange={() => onSelect("new")} /> @@ -333,7 +364,7 @@ function ResolutionSelector({ selected === candidate.pgc ? "border-accent bg-accent/15" : "border-border bg-surface hover:bg-surface-2", - resolving !== null && "opacity-50 cursor-wait", + busy && "opacity-50 cursor-wait", )} >
@@ -342,7 +373,7 @@ function ResolutionSelector({ name="crossmatch-resolution" className="shrink-0" checked={selected === candidate.pgc} - disabled={resolving !== null} + disabled={busy} onChange={() => onSelect(candidate.pgc)} />
@@ -354,7 +385,60 @@ function ResolutionSelector({ + + {showAddCandidate && ( +
+ { + setPgcInput(event.target.value); + setAddCandidateError(null); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void submitNewCandidate(); + } + if (event.key === "Escape") { + event.preventDefault(); + setShowAddCandidate(false); + setPgcInput(""); + setAddCandidateError(null); + } + }} + className="bg-surface-2 border border-border rounded px-3 py-2 text-sm text-primary placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent w-full" + /> +
+ )} + + {addCandidateError && ( +

+ {addCandidateError} +

+ )} + +