@@ -421,7 +484,7 @@ function KnowledgeGraph2DImpl({
aria-label="Knowledge graph"
style={{
display: "block",
- cursor: dragRef.current?.kind === "pan" ? "grabbing" : "grab",
+ cursor: "grab",
touchAction: "none",
}}
onPointerDown={onSvgPointerDown}
@@ -440,13 +503,17 @@ function KnowledgeGraph2DImpl({
{/* Edges */}
{simLinks.map((l, i) => {
- const s = typeof l.source === "object" ? (l.source as SimNode) : simNodes.find((n) => n.id === l.source);
- const t = typeof l.target === "object" ? (l.target as SimNode) : simNodes.find((n) => n.id === l.target);
+ const s = typeof l.source === "object" ? (l.source as SimNode) : nodeById.get(String(l.source));
+ const t = typeof l.target === "object" ? (l.target as SimNode) : nodeById.get(String(l.target));
if (!s || !t || s.x == null || t.x == null) return null;
const op = variant === "constellation" ? 0.35 : 0.2;
return (
{
+ if (el) edgeElsRef.current.set(i, el);
+ else edgeElsRef.current.delete(i);
+ }}
data-testid="graph-edge"
x1={s.x}
y1={s.y}
@@ -474,11 +541,21 @@ function KnowledgeGraph2DImpl({
return (
{
+ if (el) nodeElsRef.current.set(n.id, el);
+ else nodeElsRef.current.delete(n.id);
+ }}
data-testid="graph-node"
data-node-id={n.id}
+ transform={`translate(${n.x}, ${n.y})`}
style={{ cursor: "grab" }}
onPointerDown={(ev) => onNodePointerDown(ev, n)}
- onPointerEnter={() => setHovered(n)}
+ onPointerEnter={(ev) => {
+ // Seed the tooltip position as it mounts so it opens
+ // under the cursor, not at a stale spot.
+ setTooltipPos({ x: ev.clientX, y: ev.clientY });
+ setHovered(n);
+ }}
onPointerLeave={() => setHovered((h) => (h?.id === n.id ? null : h))}
onClick={(ev) => {
ev.stopPropagation();
@@ -487,10 +564,10 @@ function KnowledgeGraph2DImpl({
}}
>
{variant === "organism" && (
-
+
)}
{isHl && (
-
+
-
-
+
+
>
) : (
({
@@ -102,9 +102,20 @@ const pasteTextarea = () =>
screen.getByPlaceholderText(/paste your cards here/i);
afterEach(() => {
- // Dialog and ToastProvider both portal into document.body; RTL cleanup
- // alone leaves portal siblings behind (same pattern as
- // DocumentUploadModal.test.tsx).
+ // UNMOUNT first, then sweep. Order matters and both steps are needed:
+ //
+ // - vitest.config.ts sets `globals: false`, so @testing-library/react does
+ // NOT auto-register its cleanup. Without an explicit call nothing here
+ // ever unmounted: React kept the tree — and its pending scheduler work —
+ // alive past the end of the file, and when jsdom was torn down that work
+ // landed on a `window` that no longer existed. That surfaced as three
+ // "ReferenceError: window is not defined" unhandled errors, which vitest
+ // reports as `Errors 3` and exits non-zero on even with every test
+ // passing — intermittently red-flagging unrelated PRs (#492 most recently).
+ // - Dialog and ToastProvider both portal into document.body, and cleanup()
+ // alone leaves those portal siblings behind (same pattern as
+ // DocumentUploadModal.test.tsx) — hence the body sweep afterwards.
+ cleanup();
cleanupBody();
vi.clearAllMocks();
});
diff --git a/frontend/src/components/screens/Admin.tsx b/frontend/src/components/screens/Admin.tsx
index 7fbb43c6..eb82f73c 100644
--- a/frontend/src/components/screens/Admin.tsx
+++ b/frontend/src/components/screens/Admin.tsx
@@ -936,7 +936,7 @@ function CosmeticsTab() {
/>
{form.asset_url && (
-