fix(graph): address review — reachFrom direct edges + node a11y
- reachFrom now returns the direct outbound edges PLUS the recursive $graphLookup reaches, de-duped by edge id, with maxDepth to bound cycles. It was dropping edges straight off the start node. [review P2] - GraphView SVG nodes are keyboard-accessible: role/tabIndex/aria-label + Enter/Space handler alongside onClick. [review P3] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ export async function reachFrom(podId: string, startNodeId: string): Promise<Rea
|
|||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.collection<GraphEdgeDoc>('graph_edges')
|
.collection<GraphEdgeDoc>('graph_edges')
|
||||||
.aggregate<{ reaches: GraphEdgeDoc[] }>([
|
.aggregate<GraphEdgeDoc & { reaches: GraphEdgeDoc[] }>([
|
||||||
{ $match: { podId, source: startNodeId } },
|
{ $match: { podId, source: startNodeId } },
|
||||||
{
|
{
|
||||||
$graphLookup: {
|
$graphLookup: {
|
||||||
@@ -80,9 +80,25 @@ export async function reachFrom(podId: string, startNodeId: string): Promise<Rea
|
|||||||
connectToField: 'source',
|
connectToField: 'source',
|
||||||
as: 'reaches',
|
as: 'reaches',
|
||||||
restrictSearchWithMatch: { podId },
|
restrictSearchWithMatch: { podId },
|
||||||
|
maxDepth: 6,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.toArray();
|
.toArray();
|
||||||
return { start: startNodeId, reaches: rows.flatMap((r) => r.reaches) };
|
|
||||||
|
// Include the direct outbound edges (the $match rows) plus everything reachable
|
||||||
|
// from them, de-duped by edge id. Without the direct rows, edges straight off
|
||||||
|
// the start node go missing unless a cycle happens to re-discover them.
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const reaches: GraphEdgeDoc[] = [];
|
||||||
|
for (const row of rows) {
|
||||||
|
const { reaches: recursive, ...direct } = row;
|
||||||
|
for (const edge of [direct as GraphEdgeDoc, ...(recursive ?? [])]) {
|
||||||
|
if (edge?.id && !seen.has(edge.id)) {
|
||||||
|
seen.add(edge.id);
|
||||||
|
reaches.push(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { start: startNodeId, reaches };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,7 +245,16 @@ export function GraphView({ podId, onClose }: { podId: string; onClose: () => vo
|
|||||||
<g
|
<g
|
||||||
key={n.id}
|
key={n.id}
|
||||||
className={`pm-node ${dimNode(n.id) ? 'pm-dim' : ''}`}
|
className={`pm-node ${dimNode(n.id) ? 'pm-dim' : ''}`}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`${n.kind}: ${n.label}`}
|
||||||
onClick={() => setSelected((cur) => (cur === n.id ? null : n.id))}
|
onClick={() => setSelected((cur) => (cur === n.id ? null : n.id))}
|
||||||
|
onKeyDown={(ev) => {
|
||||||
|
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||||
|
ev.preventDefault();
|
||||||
|
setSelected((cur) => (cur === n.id ? null : n.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<NodeShape node={n} />
|
<NodeShape node={n} />
|
||||||
<text className="pm-lbl" x={n.x} y={n.y + 33} textAnchor="middle">
|
<text className="pm-lbl" x={n.x} y={n.y + 33} textAnchor="middle">
|
||||||
|
|||||||
Reference in New Issue
Block a user