MVP 2: added introduced by
This commit is contained in:
@@ -9,6 +9,7 @@ export async function GET() {
|
|||||||
prisma.person.findMany(),
|
prisma.person.findMany(),
|
||||||
prisma.connection.findMany(),
|
prisma.connection.findMany(),
|
||||||
]);
|
]);
|
||||||
|
const nameById = new Map(people.map((p: any) => [p.id, p.name]));
|
||||||
|
|
||||||
const nodes = people.map((p: any) => ({
|
const nodes = people.map((p: any) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
@@ -18,13 +19,21 @@ export async function GET() {
|
|||||||
role: p.role,
|
role: p.role,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edges = connections.map((c: any) => ({
|
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,
|
id: c.id,
|
||||||
source: c.personAId,
|
source: c.personAId,
|
||||||
target: c.personBId,
|
target: c.personBId,
|
||||||
introducedByCount: c.introducedByChain.length,
|
introducedByCount: c.introducedByChain.length,
|
||||||
hasProvenance: c.introducedByChain.length > 0,
|
hasProvenance: c.introducedByChain.length > 0,
|
||||||
}));
|
introducedByNames,
|
||||||
|
eventLabels: Array.isArray(c.eventLabels) ? c.eventLabels : [],
|
||||||
|
notes: c.notes ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({ nodes, edges }, { status: 200 });
|
return NextResponse.json({ nodes, edges }, { status: 200 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -65,10 +65,23 @@ export default function ConnectionForm() {
|
|||||||
return;
|
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 = {
|
const payload = {
|
||||||
personAId: personA.id,
|
personAId: personA.id,
|
||||||
personBId: personB.id,
|
personBId: personB.id,
|
||||||
introducedByChain: introducedBy.filter(Boolean).map((p) => (p as Selected)!.id),
|
introducedByChain: chain,
|
||||||
eventLabels: splitCsv(eventLabelsText),
|
eventLabels: splitCsv(eventLabelsText),
|
||||||
notes: notes.trim() || undefined,
|
notes: notes.trim() || undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ type GraphEdgeDTO = {
|
|||||||
target: string;
|
target: string;
|
||||||
introducedByCount: number;
|
introducedByCount: number;
|
||||||
hasProvenance: boolean;
|
hasProvenance: boolean;
|
||||||
|
introducedByNames?: string[];
|
||||||
|
eventLabels?: string[];
|
||||||
|
notes?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GraphData = {
|
type GraphData = {
|
||||||
@@ -46,15 +49,25 @@ export default function Graph({ data, height = 600 }: Props) {
|
|||||||
sectors: n.sectors,
|
sectors: n.sectors,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
const edges = data.edges.map((e) => ({
|
const edges = data.edges.map((e) => {
|
||||||
|
const label =
|
||||||
|
e.introducedByNames && e.introducedByNames.length
|
||||||
|
? `Introduced by: ${e.introducedByNames.join(" → ")}`
|
||||||
|
: "";
|
||||||
|
return {
|
||||||
data: {
|
data: {
|
||||||
id: e.id,
|
id: e.id,
|
||||||
source: e.source,
|
source: e.source,
|
||||||
target: e.target,
|
target: e.target,
|
||||||
introducedByCount: e.introducedByCount,
|
introducedByCount: e.introducedByCount,
|
||||||
hasProvenance: e.hasProvenance,
|
hasProvenance: e.hasProvenance,
|
||||||
|
introducedByNames: e.introducedByNames ?? [],
|
||||||
|
eventLabels: e.eventLabels ?? [],
|
||||||
|
notes: e.notes ?? null,
|
||||||
|
edgeLabel: label,
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
return { nodes, edges };
|
return { nodes, edges };
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@@ -104,6 +117,23 @@ export default function Graph({ data, height = 600 }: Props) {
|
|||||||
"line-color": "#16a34a", // green-600
|
"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",
|
selector: ":selected",
|
||||||
style: {
|
style: {
|
||||||
@@ -147,6 +177,9 @@ export default function Graph({ data, height = 600 }: Props) {
|
|||||||
target: d.target,
|
target: d.target,
|
||||||
introducedByCount: Number(d.introducedByCount ?? 0),
|
introducedByCount: Number(d.introducedByCount ?? 0),
|
||||||
hasProvenance: Boolean(d.hasProvenance),
|
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}
|
{selectedEdge.introducedByCount}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<span className="text-xs text-zinc-500">Has provenance:</span>{" "}
|
<span className="text-xs text-zinc-500">Introduced by:</span>{" "}
|
||||||
{selectedEdge.hasProvenance ? "Yes" : "No"}
|
{selectedEdge.introducedByNames && selectedEdge.introducedByNames.length > 0
|
||||||
|
? selectedEdge.introducedByNames.join(" → ")
|
||||||
|
: "None"}
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ export const graphEdgeSchema = z.object({
|
|||||||
target: z.string().uuid(),
|
target: z.string().uuid(),
|
||||||
introducedByCount: z.number().int().nonnegative(),
|
introducedByCount: z.number().int().nonnegative(),
|
||||||
hasProvenance: z.boolean(),
|
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({
|
export const graphResponseSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user