Make creating a person obvious; inline "create new" when linking relatives
- Family view gets a prominent "+ Add person" button that creates a person and opens their page to fill in details (previously you could only add a person via the empty-state form or by linking from another person). - The person page's relationship picker (PersonCombobox) now offers "+ Create '<typed name>'" when the person doesn't exist yet: it creates them, links them in the chosen role (parent/child/partner/sibling), and jumps to their new page to edit — no more create-then-go-back-and-link. Frontend only — no migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,6 +126,12 @@ export default function FamilyViewPage() {
|
|||||||
return data?.id ?? null;
|
return data?.id ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new (blank) person and open their page to fill in details.
|
||||||
|
async function newPersonAndGo() {
|
||||||
|
const id = await addPerson("");
|
||||||
|
if (id) router.push(`/trees/${treeId}/persons/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function createFirst(e: React.FormEvent) {
|
async function createFirst(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!firstName.trim()) return;
|
if (!firstName.trim()) return;
|
||||||
@@ -352,12 +358,17 @@ export default function FamilyViewPage() {
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h1 className="text-2xl font-semibold">Family view</h1>
|
<h1 className="text-2xl font-semibold">Family view</h1>
|
||||||
<Link
|
<div className="flex items-center gap-3">
|
||||||
href={`/trees/${treeId}/persons/${focus.id}`}
|
<Button size="sm" onClick={newPersonAndGo}>
|
||||||
className="text-sm text-bronze hover:underline"
|
+ Add person
|
||||||
>
|
</Button>
|
||||||
Open {focus.primary_name ?? "person"} →
|
<Link
|
||||||
</Link>
|
href={`/trees/${treeId}/persons/${focus.id}`}
|
||||||
|
className="text-sm text-bronze hover:underline"
|
||||||
|
>
|
||||||
|
Open {focus.primary_name ?? "person"} →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pedigree: focus → parents → grandparents, with bracket connectors */}
|
{/* Pedigree: focus → parents → grandparents, with bracket connectors */}
|
||||||
|
|||||||
@@ -342,28 +342,47 @@ export default function PersonDetailPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRel(e: React.FormEvent) {
|
async function linkRelative(otherId: string): Promise<boolean> {
|
||||||
e.preventDefault();
|
|
||||||
if (!relOther) return;
|
|
||||||
let body: RelCreate;
|
let body: RelCreate;
|
||||||
if (relKind === "parent") {
|
if (relKind === "parent") {
|
||||||
body = { type: "parent_child", person_from_id: relOther, person_to_id: personId, qualifier: relQual };
|
body = { type: "parent_child", person_from_id: otherId, person_to_id: personId, qualifier: relQual };
|
||||||
} else if (relKind === "child") {
|
} else if (relKind === "child") {
|
||||||
body = { type: "parent_child", person_from_id: personId, person_to_id: relOther, qualifier: relQual };
|
body = { type: "parent_child", person_from_id: personId, person_to_id: otherId, qualifier: relQual };
|
||||||
} else if (relKind === "partner") {
|
} else if (relKind === "partner") {
|
||||||
body = { type: "partnership", person_from_id: personId, person_to_id: relOther };
|
body = { type: "partnership", person_from_id: personId, person_to_id: otherId };
|
||||||
} else {
|
} else {
|
||||||
body = { type: "sibling", person_from_id: personId, person_to_id: relOther };
|
body = { type: "sibling", person_from_id: personId, person_to_id: otherId };
|
||||||
}
|
}
|
||||||
const { error } = await api.POST("/api/v1/trees/{tree_id}/relationships", {
|
const { error } = await api.POST("/api/v1/trees/{tree_id}/relationships", {
|
||||||
params: { path: { tree_id: treeId } },
|
params: { path: { tree_id: treeId } },
|
||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
if (!error) {
|
return !error;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRel(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!relOther) return;
|
||||||
|
if (await linkRelative(relOther)) {
|
||||||
setRelOther("");
|
setRelOther("");
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a brand-new person, link them in the chosen role, then jump to their
|
||||||
|
// page so the user can fill in details immediately.
|
||||||
|
async function createRelativeAndGo(name: string) {
|
||||||
|
const toks = name.trim().split(/\s+/).filter(Boolean);
|
||||||
|
const given = toks.length > 1 ? toks.slice(0, -1).join(" ") : toks[0] ?? name.trim();
|
||||||
|
const surname = toks.length > 1 ? toks[toks.length - 1] : null;
|
||||||
|
const { data } = await api.POST("/api/v1/trees/{tree_id}/persons", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
body: { given, surname },
|
||||||
|
});
|
||||||
|
if (!data) return;
|
||||||
|
await linkRelative(data.id);
|
||||||
|
router.push(`/trees/${treeId}/persons/${data.id}`);
|
||||||
|
}
|
||||||
async function removeRel(id: string) {
|
async function removeRel(id: string) {
|
||||||
await api.DELETE("/api/v1/trees/{tree_id}/relationships/{relationship_id}", {
|
await api.DELETE("/api/v1/trees/{tree_id}/relationships/{relationship_id}", {
|
||||||
params: { path: { tree_id: treeId, relationship_id: id } },
|
params: { path: { tree_id: treeId, relationship_id: id } },
|
||||||
@@ -1096,7 +1115,8 @@ export default function PersonDetailPage() {
|
|||||||
people={others}
|
people={others}
|
||||||
value={relOther}
|
value={relOther}
|
||||||
onChange={setRelOther}
|
onChange={setRelOther}
|
||||||
placeholder="Search for a person…"
|
onCreate={createRelativeAndGo}
|
||||||
|
placeholder="Search, or type a new name…"
|
||||||
/>
|
/>
|
||||||
{(relKind === "parent" || relKind === "child") && (
|
{(relKind === "parent" || relKind === "child") && (
|
||||||
<select className={fieldCls} value={relQual} onChange={(e) => setRelQual(e.target.value as Qualifier)}>
|
<select className={fieldCls} value={relQual} onChange={(e) => setRelQual(e.target.value as Qualifier)}>
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ export function PersonCombobox({
|
|||||||
people,
|
people,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
onCreate,
|
||||||
placeholder = "Search for a person…",
|
placeholder = "Search for a person…",
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
people: Person[];
|
people: Person[];
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (id: string) => void;
|
onChange: (id: string) => void;
|
||||||
|
/** When set, the dropdown offers a "Create '<typed name>'" action. */
|
||||||
|
onCreate?: (name: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
@@ -77,7 +80,7 @@ export function PersonCombobox({
|
|||||||
if (value) onChange(""); // typing invalidates the prior pick
|
if (value) onChange(""); // typing invalidates the prior pick
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{open && matches.length > 0 && (
|
{open && (matches.length > 0 || (onCreate && query.trim())) && (
|
||||||
<ul className="absolute z-30 mt-1 max-h-64 w-72 overflow-auto rounded-lg border border-[var(--border)] bg-[var(--surface)] shadow-lg">
|
<ul className="absolute z-30 mt-1 max-h-64 w-72 overflow-auto rounded-lg border border-[var(--border)] bg-[var(--surface)] shadow-lg">
|
||||||
{matches.map((p) => (
|
{matches.map((p) => (
|
||||||
<li key={p.id}>
|
<li key={p.id}>
|
||||||
@@ -96,6 +99,21 @@ export function PersonCombobox({
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
{onCreate && query.trim() && (
|
||||||
|
<li className={matches.length > 0 ? "border-t border-[var(--border)]" : ""}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const name = query.trim();
|
||||||
|
setOpen(false);
|
||||||
|
onCreate(name);
|
||||||
|
}}
|
||||||
|
className="block w-full px-3 py-2 text-left text-sm text-bronze hover:bg-[var(--muted-bg,rgba(0,0,0,0.04))]"
|
||||||
|
>
|
||||||
|
+ Create “{query.trim()}”
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user