Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 113 additions & 7 deletions src/pages/RecordCrossmatchDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<void>;
}

function ResolutionSelector({
Expand All @@ -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<string | null>(
null,
);
const busy = resolving !== null || addingCandidate;
const matchedPgc =
crossmatch.status === "existing" ? crossmatch.metadata.pgc : null;

async function submitNewCandidate(): Promise<void> {
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 (
<ObjectSummary
Expand Down Expand Up @@ -307,7 +338,7 @@ function ResolutionSelector({
selected === "new"
? "border-accent bg-accent/15"
: "border-border bg-surface hover:bg-surface-2",
resolving !== null && "opacity-50 cursor-wait",
busy && "opacity-50 cursor-wait",
)}
>
<div className="flex items-center gap-3">
Expand All @@ -316,7 +347,7 @@ function ResolutionSelector({
name="crossmatch-resolution"
className="shrink-0"
checked={selected === "new"}
disabled={resolving !== null}
disabled={busy}
onChange={() => onSelect("new")}
/>
<span className="text-sm font-semibold">
Expand All @@ -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",
)}
>
<div className="flex items-center gap-3">
Expand All @@ -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)}
/>
<div className="min-w-0 flex-1">
Expand All @@ -354,7 +385,60 @@ function ResolutionSelector({

<Button
type="button"
disabled={selected === null || resolving !== null}
disabled={busy}
className="w-8 h-8 p-0 justify-center"
onClick={() => {
setShowAddCandidate((current) => !current);
setPgcInput("");
setAddCandidateError(null);
}}
>
{showAddCandidate ? (
<MdClose className="w-5 h-5" />
) : (
<MdAdd className="w-5 h-5" />
)}
</Button>

{showAddCandidate && (
<div className="rounded-lg border border-border bg-surface px-4 py-2">
<input
type="text"
inputMode="numeric"
placeholder="PGC number"
value={pgcInput}
disabled={busy}
autoFocus
onChange={(event) => {
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"
/>
</div>
)}

{addCandidateError && (
<p className="text-danger text-sm" role="alert">
{addCandidateError}
</p>
)}

<Button
type="button"
disabled={selected === null || busy}
onClick={onSubmit}
>
{resolving !== null ? "Saving…" : "Save resolution"}
Expand Down Expand Up @@ -389,6 +473,7 @@ function RecordCrossmatchDetails({
}: RecordCrossmatchDetailsProps): ReactElement {
const [selected, setSelected] = useState<ResolutionChoice | null>(null);
const [resolving, setResolving] = useState<ResolutionChoice | null>(null);
const [addingCandidate, setAddingCandidate] = useState(false);
const [resolveError, setResolveError] = useState<string | null>(null);
const {
crossmatch,
Expand All @@ -397,8 +482,7 @@ function RecordCrossmatchDetails({
table_name: tableName,
original_data: originalData,
} = data;
const showResolveControls =
isLoggedIn() && crossmatch.triage_status === "pending";
const showResolveControls = isLoggedIn();
const backendSchema = convertAdminSchemaToBackendSchema(schema);
const candidateSources = convertCandidatesToAdditionalSources(
candidates,
Expand Down Expand Up @@ -466,6 +550,26 @@ function RecordCrossmatchDetails({
}
}

async function addCandidate(pgc: number): Promise<void> {
setResolveError(null);
setAddingCandidate(true);
try {
const currentPgcs = candidates.map((candidate) => candidate.pgc);
await submitCrossmatchResolution({
collided: {
record_ids: [crossmatch.record_id],
possible_matches: [[...currentPgcs, pgc]],
triage_statuses: ["pending"],
},
});
} catch (err) {
setResolveError(`${err}`);
throw err;
} finally {
setAddingCandidate(false);
}
}

async function submitResolution(): Promise<void> {
if (selected === null) return;
if (selected === "new") {
Expand Down Expand Up @@ -527,9 +631,11 @@ function RecordCrossmatchDetails({
schema={backendSchema}
showResolveControls={showResolveControls}
resolving={resolving}
addingCandidate={addingCandidate}
selected={selected}
onSelect={setSelected}
onSubmit={() => void submitResolution()}
onAddCandidate={addCandidate}
/>

{resolveError && (
Expand Down
Loading