"use client"; import { useMemo, useState } from "react"; import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects"; import type { WikiSnapshot } from "@/uhm/types/wiki"; import { useShallow } from "zustand/react/shallow"; import NewBadge from "@/uhm/components/editor/NewBadge"; import { useEditorStore } from "@/uhm/store/editorStore"; type EntityChoice = { id: string; name: string; isNew?: boolean }; type WikiChoice = { id: string; title: string; isNew?: boolean }; type BindingRow = { entityId: string; entityName: string; entityIsNew: boolean; wikiId: string; wikiTitle: string; wikiIsNew: boolean; linkIsNew: boolean; }; type Props = { setLinks: React.Dispatch>; }; function wikiTitle(w: WikiSnapshot): string { const t = String(w.title || "").trim(); return t.length ? t : "Untitled wiki"; } export default function EntityWikiBindingsPanel({ setLinks }: Props) { const { entityCatalog, snapshotEntities, wikis, links, } = useEditorStore( useShallow((state) => ({ entityCatalog: state.entityCatalog, snapshotEntities: state.snapshotEntities, wikis: state.snapshotWikis, links: state.snapshotEntityWikiLinks, })) ); const [activeEntityId, setActiveEntityId] = useState(""); const [activeWikiId, setActiveWikiId] = useState(""); const [collapsed, setCollapsed] = useState(false); const wikiChoices: WikiChoice[] = useMemo( () => (wikis || []) .filter((w) => w && typeof w.id === "string" && w.id.trim().length > 0) .map((w) => ({ id: w.id, title: wikiTitle(w), isNew: w.source === "inline" && w.operation === "create", })), [wikis] ); const entityChoices = useMemo(() => { const visibleSnapshotEntities = new globalThis.Map(); for (const ref of snapshotEntities || []) { const id = String(ref?.id || "").trim(); if (!id || ref?.operation === "delete" || visibleSnapshotEntities.has(id)) continue; visibleSnapshotEntities.set(id, { id, name: String(ref?.name || id), isNew: ref?.source === "inline" && ref?.operation === "create", }); } const rows = Array.from(visibleSnapshotEntities.values()).map((entity) => { const found = entityCatalog.find((item) => String(item.id) === entity.id) || null; return { id: entity.id, name: String(found?.name || entity.name || entity.id), isNew: entity.isNew, }; }); rows.sort((a, b) => a.name.localeCompare(b.name)); return rows; }, [entityCatalog, snapshotEntities]); const activeLinks = useMemo(() => { const set = new Set(); for (const l of links || []) { if (!l || l.entity_id !== activeEntityId) continue; if (l.operation === "delete") continue; set.add(l.wiki_id); } return set; }, [activeEntityId, links]); const activeBindingRows = useMemo(() => { const byKey = new Map(); for (const link of links || []) { const entityId = String(link?.entity_id || "").trim(); const wikiId = String(link?.wiki_id || "").trim(); if (!entityId || !wikiId) continue; if (link.operation === "delete") continue; byKey.set(`${entityId}::${wikiId}`, link); } const rows = Array.from(byKey.values()).map((link) => { const entityId = String(link.entity_id); const wikiId = String(link.wiki_id); const entity = entityChoices.find((item) => item.id === entityId) || null; const wiki = wikiChoices.find((item) => item.id === wikiId) || null; return { entityId, entityName: entity?.name || entityId, entityIsNew: Boolean(entity?.isNew), wikiId, wikiTitle: wiki?.title || wikiId, wikiIsNew: Boolean(wiki?.isNew), linkIsNew: link.operation === "binding", }; }); rows.sort((a, b) => { if (a.linkIsNew !== b.linkIsNew) return a.linkIsNew ? -1 : 1; const entityCompare = a.entityName.localeCompare(b.entityName); if (entityCompare !== 0) return entityCompare; return a.wikiTitle.localeCompare(b.wikiTitle); }); return rows; }, [entityChoices, links, wikiChoices]); const toggle = (wikiId: string) => { if (!activeEntityId) return; const id = String(wikiId || "").trim(); if (!id) return; setLinks((prev) => { const idx = prev.findIndex((l) => l.entity_id === activeEntityId && l.wiki_id === id); // If link exists (reference/binding), unlink by removing the row entirely. if (idx >= 0 && prev[idx]?.operation !== "delete") { return prev.filter((_, i) => i !== idx); } // If link doesn't exist, add as a new binding (create for relation). return [ ...prev.filter((l) => !(l.entity_id === activeEntityId && l.wiki_id === id)), { entity_id: activeEntityId, wiki_id: id, operation: "binding" }, ]; }); }; const activeWikiLinked = activeEntityId && activeWikiId ? activeLinks.has(activeWikiId) : false; const activeWikiChoice = activeWikiId ? wikiChoices.find((w) => w.id === activeWikiId) || null : null; const activeEntityChoice = activeEntityId ? entityChoices.find((e) => e.id === activeEntityId) || null : null; return (
Entity ↔ Wiki
{activeBindingRows.length}
{collapsed ? null : (
Entity
{activeEntityId ? ( ) : null}
Wikis
{activeWikiChoice ? ( ) : null} {wikiChoices.length === 0 ? (
No wiki in project yet.
) : ( <> {activeWikiChoice ? (
{activeWikiChoice.id}
) : null} {!activeEntityId ? (
Pick an entity to see/link wikis.
) : activeLinks.size ? (
Linked wikis ({activeLinks.size})
{Array.from(activeLinks).map((id) => { const w = wikiChoices.find((x) => x.id === id) || null; return (
{w?.title || "Untitled wiki"}
{id}
); })}
) : (
No wiki linked yet.
)} )}
All bindings ({activeBindingRows.length})
{activeBindingRows.length ? (
{activeBindingRows.map((row) => (
{row.entityName} {row.entityIsNew ? : null} {row.linkIsNew ? : null}
Wiki {row.wikiTitle} {row.wikiIsNew ? : null}
{row.entityId} ↔ {row.wikiId}
))}
) : (
No entity-wiki binding yet.
)}
)}
); } function ActiveSelectionLabel({ label, id, isNew, }: { label: string; id: string; isNew?: boolean; }) { return (
{label} {id} {isNew ? : null}
); } function PlusIcon() { return ( ); } function MinusIcon() { return ( ); }