From 22bc536978f416dea557557c240d8e53cb0df763 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Sat, 6 Jun 2026 22:19:01 -0400 Subject: [PATCH] Rebuild People as a family view (pedigree + family group); add recovery UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The People page is no longer a flat list: it's a focus-person family view with a pedigree of ancestors (parents + grandparents), a spouse/partner panel, and a children panel — with inline 'add parent/child/spouse' (creates the person + the relationship), click-to-refocus, birth–death years, and a searchable people index. Modeled on how real genealogy tools center on a person and let you walk the graph. Adds delete/restore UI: a Delete on the person page, per-tree delete + a 'Recently deleted' restore section on the trees list, and a Recovery page (sidebar) for deleted people. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Justin Paul --- frontend/app/trees/[id]/page.tsx | 347 +++++++++++++++--- .../trees/[id]/persons/[personId]/page.tsx | 14 +- frontend/app/trees/[id]/recovery/page.tsx | 72 ++++ frontend/app/trees/page.tsx | 78 ++-- frontend/components/app-sidebar.tsx | 8 +- frontend/lib/api/schema.d.ts | 243 +++++++++++- frontend/openapi.json | 346 +++++++++++++++-- 7 files changed, 1002 insertions(+), 106 deletions(-) create mode 100644 frontend/app/trees/[id]/recovery/page.tsx diff --git a/frontend/app/trees/[id]/page.tsx b/frontend/app/trees/[id]/page.tsx index 2a46cc2..32a4ad6 100644 --- a/frontend/app/trees/[id]/page.tsx +++ b/frontend/app/trees/[id]/page.tsx @@ -2,35 +2,60 @@ import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { api } from "@/lib/api/client"; import type { components } from "@/lib/api/schema"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; type Person = components["schemas"]["PersonRead"]; +type Relationship = components["schemas"]["RelationshipRead"]; +type Event = components["schemas"]["EventRead"]; -export default function TreeDetailPage() { +function splitName(full: string): { given: string | null; surname: string | null } { + const t = full.trim().split(/\s+/).filter(Boolean); + if (t.length === 0) return { given: null, surname: null }; + if (t.length === 1) return { given: t[0], surname: null }; + return { given: t.slice(0, -1).join(" "), surname: t[t.length - 1] }; +} + +type AddKind = "parent" | "child" | "partner"; + +export default function FamilyViewPage() { const router = useRouter(); const params = useParams<{ id: string }>(); const treeId = params.id; - const [persons, setPersons] = useState([]); - const [given, setGiven] = useState(""); - const [surname, setSurname] = useState(""); + const [people, setPeople] = useState([]); + const [rels, setRels] = useState([]); + const [events, setEvents] = useState([]); const [ready, setReady] = useState(false); + const [focusId, setFocusId] = useState(null); + const [search, setSearch] = useState(""); + const [firstName, setFirstName] = useState(""); + // Inline add-relative form: which anchor + kind is open, and the typed name. + const [adding, setAdding] = useState<{ kind: AddKind; anchor: string } | null>(null); + const [addName, setAddName] = useState(""); const load = useCallback(async () => { - const { data, response } = await api.GET("/api/v1/trees/{tree_id}/persons", { + const p = await api.GET("/api/v1/trees/{tree_id}/persons", { params: { path: { tree_id: treeId } }, }); - if (response.status === 401) { + if (p.response.status === 401) { router.push("/login"); return; } - setPersons(data ?? []); + const [r, e] = await Promise.all([ + api.GET("/api/v1/trees/{tree_id}/relationships", { params: { path: { tree_id: treeId } } }), + api.GET("/api/v1/trees/{tree_id}/events", { params: { path: { tree_id: treeId } } }), + ]); + const ppl = p.data ?? []; + setPeople(ppl); + setRels(r.data ?? []); + setEvents(e.data ?? []); + setFocusId((cur) => cur ?? ppl[0]?.id ?? null); setReady(true); }, [router, treeId]); @@ -38,60 +63,276 @@ export default function TreeDetailPage() { load(); }, [load]); - async function addPerson(e: React.FormEvent) { - e.preventDefault(); - if (!given.trim() && !surname.trim()) return; - const { error } = await api.POST("/api/v1/trees/{tree_id}/persons", { - params: { path: { tree_id: treeId } }, - body: { given: given || null, surname: surname || null }, - }); - if (!error) { - setGiven(""); - setSurname(""); - load(); + const byId = useMemo(() => new Map(people.map((p) => [p.id, p])), [people]); + const parentsOf = (id: string) => + rels.filter((r) => r.type === "parent_child" && r.person_to_id === id).map((r) => r.person_from_id); + const childrenOf = (id: string) => + rels.filter((r) => r.type === "parent_child" && r.person_from_id === id).map((r) => r.person_to_id); + const partnersOf = (id: string) => + rels + .filter((r) => r.type === "partnership" && (r.person_from_id === id || r.person_to_id === id)) + .map((r) => (r.person_from_id === id ? r.person_to_id : r.person_from_id)); + + const years = useMemo(() => { + const m = new Map(); + const yr = (e: Event) => (e.date_start ? e.date_start.slice(0, 4) : e.date_value ?? ""); + for (const p of people) { + const b = events.find((e) => e.person_id === p.id && e.event_type === "birth"); + const d = events.find((e) => e.person_id === p.id && e.event_type === "death"); + const parts = [b ? yr(b) : "", d ? yr(d) : ""]; + if (parts[0] || parts[1]) m.set(p.id, `${parts[0]}–${parts[1]}`.replace(/^–$/, "")); } + return m; + }, [people, events]); + + async function addPerson(name: string): Promise { + const { given, surname } = splitName(name); + const { data } = await api.POST("/api/v1/trees/{tree_id}/persons", { + params: { path: { tree_id: treeId } }, + body: { given, surname }, + }); + return data?.id ?? null; + } + + async function createFirst(e: React.FormEvent) { + e.preventDefault(); + if (!firstName.trim()) return; + const id = await addPerson(firstName); + setFirstName(""); + if (id) setFocusId(id); + load(); + } + + async function submitAdd(e: React.FormEvent) { + e.preventDefault(); + if (!adding || !addName.trim()) return; + const newId = await addPerson(addName); + if (newId) { + const { kind, anchor } = adding; + const body = + kind === "parent" + ? { type: "parent_child" as const, person_from_id: newId, person_to_id: anchor, qualifier: "biological" as const } + : kind === "child" + ? { type: "parent_child" as const, person_from_id: anchor, person_to_id: newId, qualifier: "biological" as const } + : { type: "partnership" as const, person_from_id: anchor, person_to_id: newId }; + await api.POST("/api/v1/trees/{tree_id}/relationships", { + params: { path: { tree_id: treeId } }, + body, + }); + } + setAdding(null); + setAddName(""); + load(); } if (!ready) return

Loading…

; - return ( -
-

People

+ if (people.length === 0) { + return ( +
+

Start your tree

+ + +
+ setFirstName(e.target.value)} + /> + +
+
+
+
+ ); + } + const focus = focusId ? byId.get(focusId) : undefined; + if (!focus) { + setFocusId(people[0].id); + return null; + } + + const PersonBox = ({ + id, + muted, + }: { + id: string; + muted?: boolean; + }) => { + const p = byId.get(id); + if (!p) return null; + const isFocus = id === focusId; + return ( + + ); + }; + + const AddSlot = ({ kind, anchor, label }: { kind: AddKind; anchor: string; label: string }) => + adding && adding.kind === kind && adding.anchor === anchor ? ( +
+ setAddName(e.target.value)} + /> +
+ + +
+
+ ) : ( + + ); + + const parents = parentsOf(focus.id); + const partners = partnersOf(focus.id); + const children = childrenOf(focus.id); + + const sorted = [...people].sort((a, b) => + (a.primary_name ?? "").localeCompare(b.primary_name ?? ""), + ); + const matches = search + ? sorted.filter((p) => (p.primary_name ?? "").toLowerCase().includes(search.toLowerCase())) + : sorted; + + return ( +
+
+

Family view

+ + Open {focus.primary_name ?? "person"} → + +
+ + {/* Pedigree: focus → parents → grandparents */} - - Add a person - - -
- setGiven(e.target.value)} /> - setSurname(e.target.value)} /> - -
+ +
+
+
+ Focus +
+ +
+ +
+
+ Parents +
+ {parents.map((pid) => ( + + ))} + {parents.length < 2 && } +
+ +
+
+ Grandparents +
+ {parents.length === 0 && ( +
Add parents first.
+ )} + {parents.map((pid) => ( +
+ {parentsOf(pid).map((gp) => ( + + ))} + {parentsOf(pid).length < 2 && ( + + )} +
+ ))} +
+
-
-

People

- {persons.length === 0 ? ( -

No people yet.

- ) : ( -
    - {persons.map((person) => ( -
  • - - - - {person.primary_name ?? ( - Unnamed - )} - - - -
  • - ))} -
- )} + {/* Family group: partners + children of the focus */} +
+ + +

Spouses & partners

+
+ {partners.map((id) => ( + + ))} + +
+
+
+ + + +

Children

+
+ {children.map((id) => ( + + ))} + +
+
+
+
+ + {/* Searchable index of everyone in the tree */} +
+
+

All people ({people.length})

+ setSearch(e.target.value)} + /> +
+
+ {matches.map((p) => ( + + ))} +
); diff --git a/frontend/app/trees/[id]/persons/[personId]/page.tsx b/frontend/app/trees/[id]/persons/[personId]/page.tsx index b615063..86722e7 100644 --- a/frontend/app/trees/[id]/persons/[personId]/page.tsx +++ b/frontend/app/trees/[id]/persons/[personId]/page.tsx @@ -206,6 +206,13 @@ export default function PersonDetailPage() { load(); } + async function removePerson() { + await api.DELETE("/api/v1/trees/{tree_id}/persons/{person_id}", { + params: { path: { tree_id: treeId, person_id: personId } }, + }); + router.push(`/trees/${treeId}`); + } + if (!ready) return

Loading…

; if (!person) return

Not found.

; @@ -311,7 +318,12 @@ export default function PersonDetailPage() {

{person.primary_name ?? "Unnamed person"}

- {citeControl("p", { person_id: personId }, personCites)} +
+ {citeControl("p", { person_id: personId }, personCites)} + +
diff --git a/frontend/app/trees/[id]/recovery/page.tsx b/frontend/app/trees/[id]/recovery/page.tsx new file mode 100644 index 0000000..4b6d5e6 --- /dev/null +++ b/frontend/app/trees/[id]/recovery/page.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useParams, useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; + +import { api } from "@/lib/api/client"; +import type { components } from "@/lib/api/schema"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; + +type Person = components["schemas"]["PersonRead"]; + +export default function RecoveryPage() { + const router = useRouter(); + const params = useParams<{ id: string }>(); + const treeId = params.id; + + const [people, setPeople] = useState([]); + const [ready, setReady] = useState(false); + + const load = useCallback(async () => { + const { data, response } = await api.GET("/api/v1/trees/{tree_id}/persons", { + params: { path: { tree_id: treeId }, query: { deleted: true } }, + }); + if (response.status === 401) { + router.push("/login"); + return; + } + setPeople(data ?? []); + setReady(true); + }, [router, treeId]); + + useEffect(() => { + load(); + }, [load]); + + async function restore(id: string) { + await api.POST("/api/v1/trees/{tree_id}/persons/{person_id}/restore", { + params: { path: { tree_id: treeId, person_id: id } }, + }); + load(); + } + + if (!ready) return

Loading…

; + + return ( +
+

Recently deleted

+

+ Deleted people are recoverable for 30 days, then permanently purged. +

+ {people.length === 0 ? ( +

Nothing here.

+ ) : ( +
    + {people.map((p) => ( +
  • + + + {p.primary_name ?? "Unnamed"} + + + +
  • + ))} +
+ )} +
+ ); +} diff --git a/frontend/app/trees/page.tsx b/frontend/app/trees/page.tsx index b2d9e52..ab401b2 100644 --- a/frontend/app/trees/page.tsx +++ b/frontend/app/trees/page.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from "react"; import { api } from "@/lib/api/client"; import type { components } from "@/lib/api/schema"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; type Tree = components["schemas"]["TreeRead"]; @@ -15,6 +15,7 @@ type Tree = components["schemas"]["TreeRead"]; export default function TreesPage() { const router = useRouter(); const [trees, setTrees] = useState([]); + const [deleted, setDeleted] = useState([]); const [name, setName] = useState(""); const [ready, setReady] = useState(false); @@ -25,6 +26,8 @@ export default function TreesPage() { return; } setTrees(data ?? []); + const del = await api.GET("/api/v1/trees", { params: { query: { deleted: true } } }); + setDeleted(del.data ?? []); setReady(true); }, [router]); @@ -42,24 +45,26 @@ export default function TreesPage() { } } + async function remove(id: string) { + await api.DELETE("/api/v1/trees/{tree_id}", { params: { path: { tree_id: id } } }); + load(); + } + async function restore(id: string) { + await api.POST("/api/v1/trees/{tree_id}/restore", { params: { path: { tree_id: id } } }); + load(); + } + if (!ready) return

Loading…

; return ( -
+

Your trees

- - New tree - - +
- setName(e.target.value)} - /> - + setName(e.target.value)} /> +
@@ -67,23 +72,52 @@ export default function TreesPage() { {trees.length === 0 ? (

No trees yet — create your first one above.

) : ( -
    +
      {trees.map((tree) => (
    • - - - - {tree.name} - + + + +
      {tree.name}
      +
      {tree.visibility} - - - - +
      + + +
      +
    • ))}
    )} + + {deleted.length > 0 && ( +
    +

    + Recently deleted +

    +
      + {deleted.map((tree) => ( +
    • + + + {tree.name} + + + +
    • + ))} +
    +
    + )}
); } diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx index 4631227..a9c42f7 100644 --- a/frontend/components/app-sidebar.tsx +++ b/frontend/components/app-sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { BookText, FolderTree, Image as ImageIcon, LogOut, Users } from "lucide-react"; +import { Archive, BookText, FolderTree, Image as ImageIcon, LogOut, Users } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -87,6 +87,12 @@ export function AppSidebar() { icon={ImageIcon} active={pathname.startsWith(`/trees/${treeId}/media`)} /> +
)} diff --git a/frontend/lib/api/schema.d.ts b/frontend/lib/api/schema.d.ts index 22d8765..8f5cfb6 100644 --- a/frontend/lib/api/schema.d.ts +++ b/frontend/lib/api/schema.d.ts @@ -186,6 +186,24 @@ export interface paths { get: operations["get_tree_api_v1_trees__tree_id__get"]; put?: never; post?: never; + /** Delete Tree */ + delete: operations["delete_tree_api_v1_trees__tree_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trees/{tree_id}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Restore Tree */ + post: operations["restore_tree_api_v1_trees__tree_id__restore_post"]; delete?: never; options?: never; head?: never; @@ -221,6 +239,24 @@ export interface paths { get: operations["get_person_api_v1_trees__tree_id__persons__person_id__get"]; put?: never; post?: never; + /** Delete Person */ + delete: operations["delete_person_api_v1_trees__tree_id__persons__person_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trees/{tree_id}/persons/{person_id}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Restore Person */ + post: operations["restore_person_api_v1_trees__tree_id__persons__person_id__restore_post"]; delete?: never; options?: never; head?: never; @@ -234,7 +270,8 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + /** List Tree Events */ + get: operations["list_tree_events_api_v1_trees__tree_id__events_get"]; put?: never; /** Create Event */ post: operations["create_event_api_v1_trees__tree_id__events_post"]; @@ -285,7 +322,8 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + /** List Relationships */ + get: operations["list_relationships_api_v1_trees__tree_id__relationships_get"]; put?: never; /** Create Relationship */ post: operations["create_relationship_api_v1_trees__tree_id__relationships_post"]; @@ -1169,7 +1207,9 @@ export interface operations { }; list_my_trees_api_v1_trees_get: { parameters: { - query?: never; + query?: { + deleted?: boolean; + }; header?: never; path?: never; cookie?: never; @@ -1185,6 +1225,15 @@ export interface operations { "application/json": components["schemas"]["TreeRead"][]; }; }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; }; }; create_tree_api_v1_trees_post: { @@ -1251,7 +1300,7 @@ export interface operations { }; }; }; - list_persons_api_v1_trees__tree_id__persons_get: { + delete_tree_api_v1_trees__tree_id__delete: { parameters: { query?: never; header?: never; @@ -1261,6 +1310,68 @@ export interface operations { cookie?: never; }; requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + restore_tree_api_v1_trees__tree_id__restore_post: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TreeRead"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_persons_api_v1_trees__tree_id__persons_get: { + parameters: { + query?: { + deleted?: boolean; + }; + header?: never; + path: { + tree_id: string; + }; + cookie?: never; + }; + requestBody?: never; responses: { /** @description Successful Response */ 200: { @@ -1349,6 +1460,99 @@ export interface operations { }; }; }; + delete_person_api_v1_trees__tree_id__persons__person_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + person_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + restore_person_api_v1_trees__tree_id__persons__person_id__restore_post: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + person_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PersonRead"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_tree_events_api_v1_trees__tree_id__events_get: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventRead"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; create_event_api_v1_trees__tree_id__events_post: { parameters: { query?: never; @@ -1446,6 +1650,37 @@ export interface operations { }; }; }; + list_relationships_api_v1_trees__tree_id__relationships_get: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RelationshipRead"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; create_relationship_api_v1_trees__tree_id__relationships_post: { parameters: { query?: never; diff --git a/frontend/openapi.json b/frontend/openapi.json index 2cbfbcc..439fce7 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -281,29 +281,6 @@ } }, "/api/v1/trees": { - "get": { - "tags": [ - "trees" - ], - "summary": "List My Trees", - "operationId": "list_my_trees_api_v1_trees_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/TreeRead" - }, - "type": "array", - "title": "Response List My Trees Api V1 Trees Get" - } - } - } - } - } - }, "post": { "tags": [ "trees" @@ -311,14 +288,14 @@ "summary": "Create Tree", "operationId": "create_tree_api_v1_trees_post", "requestBody": { + "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TreeCreate" } } - }, - "required": true + } }, "responses": { "201": { @@ -342,6 +319,51 @@ } } } + }, + "get": { + "tags": [ + "trees" + ], + "summary": "List My Trees", + "operationId": "list_my_trees_api_v1_trees_get", + "parameters": [ + { + "name": "deleted", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Deleted" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TreeRead" + }, + "title": "Response List My Trees Api V1 Trees Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } } }, "/api/v1/trees/{tree_id}": { @@ -385,6 +407,83 @@ } } } + }, + "delete": { + "tags": [ + "trees" + ], + "summary": "Delete Tree", + "operationId": "delete_tree_api_v1_trees__tree_id__delete", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/trees/{tree_id}/restore": { + "post": { + "tags": [ + "trees" + ], + "summary": "Restore Tree", + "operationId": "restore_tree_api_v1_trees__tree_id__restore_post", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TreeRead" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } } }, "/api/v1/trees/{tree_id}/persons": { @@ -455,6 +554,16 @@ "format": "uuid", "title": "Tree Id" } + }, + { + "name": "deleted", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Deleted" + } } ], "responses": { @@ -486,6 +595,50 @@ } }, "/api/v1/trees/{tree_id}/persons/{person_id}": { + "delete": { + "tags": [ + "persons" + ], + "summary": "Delete Person", + "operationId": "delete_person_api_v1_trees__tree_id__persons__person_id__delete", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + }, + { + "name": "person_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Person Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, "get": { "tags": [ "persons" @@ -538,6 +691,59 @@ } } }, + "/api/v1/trees/{tree_id}/persons/{person_id}/restore": { + "post": { + "tags": [ + "persons" + ], + "summary": "Restore Person", + "operationId": "restore_person_api_v1_trees__tree_id__persons__person_id__restore_post", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + }, + { + "name": "person_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Person Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonRead" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/api/v1/trees/{tree_id}/events": { "post": { "tags": [ @@ -589,6 +795,51 @@ } } } + }, + "get": { + "tags": [ + "events" + ], + "summary": "List Tree Events", + "operationId": "list_tree_events_api_v1_trees__tree_id__events_get", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventRead" + }, + "title": "Response List Tree Events Api V1 Trees Tree Id Events Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } } }, "/api/v1/trees/{tree_id}/persons/{person_id}/events": { @@ -745,6 +996,51 @@ } } } + }, + "get": { + "tags": [ + "relationships" + ], + "summary": "List Relationships", + "operationId": "list_relationships_api_v1_trees__tree_id__relationships_get", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RelationshipRead" + }, + "title": "Response List Relationships Api V1 Trees Tree Id Relationships Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } } }, "/api/v1/trees/{tree_id}/persons/{person_id}/relationships": {