MVP 2: added introduced by
This commit is contained in:
@@ -9,6 +9,7 @@ export async function GET() {
|
||||
prisma.person.findMany(),
|
||||
prisma.connection.findMany(),
|
||||
]);
|
||||
const nameById = new Map(people.map((p: any) => [p.id, p.name]));
|
||||
|
||||
const nodes = people.map((p: any) => ({
|
||||
id: p.id,
|
||||
@@ -18,13 +19,21 @@ export async function GET() {
|
||||
role: p.role,
|
||||
}));
|
||||
|
||||
const edges = connections.map((c: any) => ({
|
||||
id: c.id,
|
||||
source: c.personAId,
|
||||
target: c.personBId,
|
||||
introducedByCount: c.introducedByChain.length,
|
||||
hasProvenance: c.introducedByChain.length > 0,
|
||||
}));
|
||||
const edges = connections.map((c: any) => {
|
||||
const introducedByNames: string[] = Array.isArray(c.introducedByChain)
|
||||
? c.introducedByChain.map((pid: string) => nameById.get(pid)).filter(Boolean)
|
||||
: [];
|
||||
return {
|
||||
id: c.id,
|
||||
source: c.personAId,
|
||||
target: c.personBId,
|
||||
introducedByCount: c.introducedByChain.length,
|
||||
hasProvenance: c.introducedByChain.length > 0,
|
||||
introducedByNames,
|
||||
eventLabels: Array.isArray(c.eventLabels) ? c.eventLabels : [],
|
||||
notes: c.notes ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ nodes, edges }, { status: 200 });
|
||||
} catch (err) {
|
||||
|
||||
@@ -65,10 +65,23 @@ export default function ConnectionForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build introduced-by chain preserving order. If the user selected an introducer
|
||||
// but forgot to click “Add”, include it automatically on save (last position).
|
||||
const baseChain = introducedBy.filter(Boolean).map((p) => (p as Selected)!.id);
|
||||
let chain = [...baseChain];
|
||||
if (
|
||||
introducerPick &&
|
||||
introducerPick.id !== personA.id &&
|
||||
introducerPick.id !== personB.id &&
|
||||
!chain.includes(introducerPick.id)
|
||||
) {
|
||||
chain.push(introducerPick.id);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
personAId: personA.id,
|
||||
personBId: personB.id,
|
||||
introducedByChain: introducedBy.filter(Boolean).map((p) => (p as Selected)!.id),
|
||||
introducedByChain: chain,
|
||||
eventLabels: splitCsv(eventLabelsText),
|
||||
notes: notes.trim() || undefined,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,9 @@ type GraphEdgeDTO = {
|
||||
target: string;
|
||||
introducedByCount: number;
|
||||
hasProvenance: boolean;
|
||||
introducedByNames?: string[];
|
||||
eventLabels?: string[];
|
||||
notes?: string | null;
|
||||
};
|
||||
|
||||
type GraphData = {
|
||||
@@ -46,15 +49,25 @@ export default function Graph({ data, height = 600 }: Props) {
|
||||
sectors: n.sectors,
|
||||
},
|
||||
}));
|
||||
const edges = data.edges.map((e) => ({
|
||||
data: {
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
introducedByCount: e.introducedByCount,
|
||||
hasProvenance: e.hasProvenance,
|
||||
},
|
||||
}));
|
||||
const edges = data.edges.map((e) => {
|
||||
const label =
|
||||
e.introducedByNames && e.introducedByNames.length
|
||||
? `Introduced by: ${e.introducedByNames.join(" → ")}`
|
||||
: "";
|
||||
return {
|
||||
data: {
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
introducedByCount: e.introducedByCount,
|
||||
hasProvenance: e.hasProvenance,
|
||||
introducedByNames: e.introducedByNames ?? [],
|
||||
eventLabels: e.eventLabels ?? [],
|
||||
notes: e.notes ?? null,
|
||||
edgeLabel: label,
|
||||
},
|
||||
};
|
||||
});
|
||||
return { nodes, edges };
|
||||
}, [data]);
|
||||
|
||||
@@ -104,6 +117,23 @@ export default function Graph({ data, height = 600 }: Props) {
|
||||
"line-color": "#16a34a", // green-600
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "edge:selected",
|
||||
style: {
|
||||
label: "data(edgeLabel)",
|
||||
"font-size": 10,
|
||||
color: "#111827",
|
||||
"text-background-color": "#ffffff",
|
||||
"text-background-opacity": 1,
|
||||
"text-background-padding": "2px",
|
||||
"text-border-color": "#e5e7eb",
|
||||
"text-border-width": 1,
|
||||
"text-border-opacity": 1,
|
||||
"text-wrap": "wrap",
|
||||
"text-max-width": "120px",
|
||||
"text-rotation": "autorotate",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ":selected",
|
||||
style: {
|
||||
@@ -147,6 +177,9 @@ export default function Graph({ data, height = 600 }: Props) {
|
||||
target: d.target,
|
||||
introducedByCount: Number(d.introducedByCount ?? 0),
|
||||
hasProvenance: Boolean(d.hasProvenance),
|
||||
introducedByNames: Array.isArray(d.introducedByNames) ? (d.introducedByNames as string[]) : [],
|
||||
eventLabels: Array.isArray(d.eventLabels) ? (d.eventLabels as string[]) : [],
|
||||
notes: (d.notes ?? null) as string | null,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -249,9 +282,22 @@ export default function Graph({ data, height = 600 }: Props) {
|
||||
{selectedEdge.introducedByCount}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-xs text-zinc-500">Has provenance:</span>{" "}
|
||||
{selectedEdge.hasProvenance ? "Yes" : "No"}
|
||||
<span className="text-xs text-zinc-500">Introduced by:</span>{" "}
|
||||
{selectedEdge.introducedByNames && selectedEdge.introducedByNames.length > 0
|
||||
? selectedEdge.introducedByNames.join(" → ")
|
||||
: "None"}
|
||||
</div>
|
||||
{selectedEdge.eventLabels && selectedEdge.eventLabels.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<span className="text-xs text-zinc-500">Event labels:</span>{" "}
|
||||
{selectedEdge.eventLabels.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
{selectedEdge.notes ? (
|
||||
<div className="text-sm">
|
||||
<span className="text-xs text-zinc-500">Notes:</span> {selectedEdge.notes}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,9 @@ export const graphEdgeSchema = z.object({
|
||||
target: z.string().uuid(),
|
||||
introducedByCount: z.number().int().nonnegative(),
|
||||
hasProvenance: z.boolean(),
|
||||
introducedByNames: z.array(z.string()).default([]).optional(),
|
||||
eventLabels: z.array(z.string()).default([]).optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const graphResponseSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user