Repository navigation
Browse relationships in two panes instead of stacked tables (closes #2432) - #2446
Conversation
Review TypePragmatic code review of phase 2 only — commit Scope: 9 files, +618 / −2075. Requirements Check — #2432 Phase 2
Code Review Findings🟡 1. The deletion left
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Address Review — all findings addressed✅ 🟡 1. Ad-hoc edge keys had become invisible — a real regression, not just dead codeYou were right that this was the worst of the three possible states, and the diagnosis was exact. Row expansion keyed off Expansion now runs over A second bug fell out of fixing this. The read-only hint keys off
Verified in the browser with a purpose-built schema declaring no edge fields, given an edge storing one anyway: the row expands, shows ✅ 🟡 1b.
|
Re-Review — commit
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Address Re-Review — 3 of 4 nits takenYou marked all four as optional. Three were worth taking; one I'm declining with reasoning. ✅
|
…2432) The panel shipped in #2426 stopped scaling with the SCHEMA — declared-but- empty relationships no longer each got a section. It still scaled with the node's DATA: every populated relationship rendered its own full-width table, stacked vertically, so a node with six of them was six tables and a scrollbar. Now a rail on the left lists the relationships the node actually HAS, one row each with its edge count, and the right pane shows every edge of the selected one. The rail is the same size whatever the node's connectivity. Two panes rather than three because expansion does the third pane's job in place: a row expands to its own edge properties, so properties stay one click away and a single-target relationship needs no special case — it simply shows its one edge. That also retires edge-properties-modal.svelte, which existed because a table cell could not hold a variable-length form; a nested dialog over the panel is not something to keep once the row can hold the form itself. ## The rail encodes the ownership boundary Order is: relationships this node owns, then `+ Add`, then a divider, then INCOMING · READ-ONLY. The control's POSITION says what the user may create, so no per-item badge has to, and the detail pane carries no "read-only" badge either — the rail heading above the selection already said it, and the absent controls are what actually hold the rule. Inbound rows show their edge properties with the same values, read-only, and offer the owning node instead of an editor. Verified in the browser that an edit made from the conference side reads back identically from the speaker's side, which is the point: it is one physical edge, not a copy. ## `+ Add` is the single creation path The old split — inline `+ Add` on populated relationships, a separate chooser for empty ones — collapses into the rail's one control, listing only what is still addable. `partitionGroups`' `addable` filter already computed that set; the chooser popover it fed is gone. ## What was deleted, and why that was a decision The per-group view-settings subsystem from #1918 — selectable columns (edge fields and target-schema fields), sort, filter, persisted per (nodeType, relationshipName, direction) — is removed: two services, three test files, and the two service functions that existed only to feed it (`fetchTargetSchemaFields`, `fetchNodesProperties`). The two-pane design's answer to "see edge data" is expanding a row, not configuring columns, and a right pane offering both would be two mechanisms fighting over the same job. Left in place they would have been unreachable code. Net -1457 lines. ## One bug found in the browser, and its test Removing a relationship's last edge left the detail pane showing a stale header with no rows. The re-selection effect asked whether the selected KEY still resolved — and it does: removing the last edge does not remove the group, it stays declared, which is exactly what makes it addable again. The rail is built from `populated`, so the check has to be against that list, not against resolvability. Fixed, with a unit test that pins the distinction rather than the symptom. Tests: 230 files / 4877 passed. The drop from 4916 is the 40 view-settings tests deleted above. Verified in `dev:browser` against an isolated NODESPACE_HOME with purpose- built conference/speaker/venue schemas: rail selection, expansion (one row at a time, independent not accordion), enum labels rendering as labels, auto-save persisting across a close/reopen, inbound read-only with values visible, target links opening via focusOrOpenNode, removal, and the full `+ Add` flow including cardinality filtering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
… concept
The review found a regression I introduced, not just dead code. Both halves
of finding 1 are fixed, and fixing the first one is what makes the second
one safe.
**Ad-hoc edge keys had become invisible.** The panel keyed row expansion
off `group.edgeFields` — the DECLARED fields — so an edge carrying only
properties the schema never declared rendered as unexpandable, with its
values unreachable in the UI. The old stacked-table modal surfaced those
keys as read-only columns via `groupEdgeColumns`, and
`groupSupportsEdgeEditing`'s docstring still promised exactly that while
the code no longer delivered it.
Expansion now runs over `edgeColumns` — the union of declared fields and
keys actually present on stored edges. Editing still keys off the
declaration: an undeclared key has no `type`, and the update path replaces
edge properties wholesale, so an editor could only guess a free-text input
and would coerce a stored number or boolean to a string on first save.
Showing the value and declining to edit it is the honest option, which is
what the docstring always said. Corrected it to describe the mechanism
that now delivers it.
That also gave the read-only hint a second case to distinguish. It keyed
off `editable`, which is false for two unrelated reasons, and told the user
to "edit these from the node that owns this relationship" — actively
misleading for an OUTBOUND relationship this node already owns, where the
real answer is to declare the field on the schema. Split into two messages.
**`layout` / `groupLayout` / `RelationshipLayout` deleted.** The
table-vs-chips distinction existed only to choose between the old modal's
two renderings. With `edgeColumns` back in production use, `groupLayout`
had one remaining caller: a guard inside `groupEdgeColumns` that returned
`[]` for a "chips" group — redundant, since the union is empty in exactly
that case anyway. Removed the guard, then the function, the type and the
view-model member. Its three unit tests are replaced by three that pin the
union rule instead, including the undeclared-key case that was broken.
**Also from the review:** `disabled={busy}` on the enum `Select.Root`,
which its checkbox and text siblings had and it did not — a concurrent-write
window against a wholesale-replace API. Plus `aria-controls` linking each
expander to the panel it toggles, and `role="alert"` on both error banners.
Not changed: the reviewer asked the author to confirm the wording of the
"empty rail with + Add" criterion. The behaviour is right — a node with no
edges shows `+ Add` and nothing else — so this is a question about the
issue's phrasing, not the code.
Verified in the browser with a purpose-built schema declaring NO edge
fields, given an edge storing one anyway: the row now expands, shows
`FITTING: snug`, renders zero inputs, and explains that declaring the field
is what makes it editable.
Tests: 230 files / 4877 passed. svelte-check + eslint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
Four nits from the re-review. Three taken, one declined. **`(issue #1918)` in a doc comment** violates CLAUDE.md's rule against embedding issue numbers in code — the constraint is described directly or referenced by ADR, not by issue. Removed; grepped the rest of the relationship files and found no others. **`openTarget` was dropping a label it already had.** It called `focusOrOpenNode` with only `nodeType`, so every target opened as "Loading..." until its viewer mounted — even though the row holds a resolved label. `title` is exactly the option phase 1 added for a caller in this position, and the collection viewer already uses it the same way. **The group-wide union was undocumented.** `expandableFields`' comment explained why `edgeColumns` beats `edgeFields` but not that the union spans the whole group, which is the surprising half: a row whose own edge lacks a property still shows it, empty. That is deliberate — per-row columns would make the expander flicker down a list of near-identical rows and would hide the difference between "this edge has no note" and "no edge here has a note", which in a group where three of four carry one is information. Declared fields already behave this way. Written down so the next reader files it as intent rather than a bug. **`{@const open}` shadowing the `$bindable` prop** — renamed to `rowOpen`. Checked first whether it was a live bug: it is not. `open = false` in `openTarget` sits outside the each-block and correctly assigns the prop, and the shadowed name is only read within the block. Renamed anyway because a collision with a bindable prop is a trap for whoever edits this next, and it costs nothing. **Declined: renaming `expandableFields`.** It returns `EdgeRow[]` rather than fields, so the name is imprecise — but "field" is the domain's word here, shared by the docstring, `edgeFields`, `RawEdgeField` and `edge-field-input.ts`. A more literally accurate name would read less like the code around it. Worth revisiting only if `EdgeRow` grows beyond a name and an optional declaration. Tests: 230 files / 4877 passed. svelte-check + eslint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
6972d18 to
ef2161a
Compare
Checked the panel against nodespace-docs/DESIGN.md rather than against the tokens alone. Colors were already tokenized — no hex, rgb, or Tailwind palette literals anywhere — but four things diverged from the documented system. **The rail's selected row used `bg-accent`.** In this palette `--accent` is `176 100% 24%`, a saturated dark teal, paired with a near-black `--accent-foreground`: a brand color and a legibility problem, not a selection surface. DESIGN.md's Sidebar spec is explicit — "navigation items use `active-nav-background` when active, `muted` on hover" — and the rail is a navigation list. Confirmed in the browser: the selected row now computes to `rgb(234,234,236)` in light and `rgb(34,34,37)` in dark, the cool-neutral hue-240 surface the sidebar uses, instead of teal. Worth naming why `bg-accent` was not simply wrong: nine other components use it — dropdown focus, autocomplete selection, calendar cells. The distinction is that those are all TRANSIENT highlight. A rail selection is persistent structural state, which is what `active-nav-background` is for. **Hover was hardcoded as `bg-muted/60`.** DESIGN.md: "derive hover states from `--hover-background` / `--hover-foreground` — never hardcode hover colors." Now reads those variables. Both tokens are consumed through `hsl(var(...))` in a scoped style block rather than Tailwind classes, because neither is mapped in the Tailwind config. The navigation sidebar reads them the same way, so this follows the one existing consumer instead of inventing a second access path. **The "Back" button used `variant="ghost"`.** DESIGN.md: "no outline/ghost variants in the current system — use secondary for low-emphasis actions." Switched. Note the ghost variant is defined as `hover:bg-accent`, so it was a second route to the same teal the first fix removed. **`text-[0.6875rem]` (5 uses) was an arbitrary value** where the type scale has `text-xs` (0.75rem, the documented `body-xs` role). It was the only file in the codebase using that size; other components style the same uppercase micro-label with `text-xs`. Inherited from the modal this replaced rather than introduced here, but no reason to carry it forward. Verified in the browser in both light and dark, with computed colors read back rather than eyeballed. Tests: 230 files / 4877 passed. svelte-check + eslint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
Post-approval commit: DESIGN.md conformanceOne more commit after the re-review approval ( Colors were already fully tokenized — no hex, 1. The rail's selected row used Verified by reading computed styles back in the browser: the selected row is now Worth naming why 2. Hover was hardcoded as Both tokens are read via 3. The "Back" button used 4. Verified in both light and dark mode with computed colors read back rather than eyeballed. Tests: 230 files / 4877 passed; eslint + svelte-check clean; full pre-push gate green. Separately filed: #2454While comparing against DESIGN.md I noticed the dialog close button renders two focus indicators — the browser's default blue outline nested inside the component's grey ring. Cause: Not caused by this PR — 🤖 Generated with Claude Code |
Phase 2 of #2432 — the panel itself. Stacked on #2441 (phase 1), which it depends on for
focusOrOpenNode. Review #2441 first; this PR's diff will shrink to just the panel once that merges.Closes #2432.
The problem
#2426 stopped the panel scaling with the schema — declared-but-empty relationships no longer each got a section. It still scaled with the node's data: every populated relationship rendered its own full-width table, stacked vertically. Six populated relationships meant six tables and a scrollbar.
Two panes
Left — a rail of the relationships the node actually has, one row each with its edge count. Right — every edge of the selected relationship, each row expanding in place to that edge's properties.
Two panes rather than three because expansion does the third pane's job: the rail stays short whatever the node's connectivity, properties stay one click away, and a single-target relationship needs no special case — it shows its one edge.
That also retires
edge-properties-modal.svelte. It existed because a table cell couldn't hold a variable-length form; a nested dialog over the panel isn't worth keeping once the row itself can.The rail encodes the ownership boundary
Order is: relationships this node owns →
+ Add→ divider →INCOMING · READ-ONLY. The control's position says what the user may create, so no per-item badge has to. The detail pane carries no "read-only" badge either — the rail heading above the selection already said it, and the absent controls are what actually hold the rule.+ Addis the single creation path, listing only what's still addable (outbound, and not acardinality: onethat's already filled).What was deleted, and why that was a decision
The per-group view-settings subsystem from #1918 — selectable columns, sort, filter, persisted per
(nodeType, relationshipName, direction)— is removed: two services, three test files, and the two service functions that existed only to feed it (fetchTargetSchemaFields,fetchNodesProperties).The two-pane design's answer to "see edge data" is expanding a row, not configuring columns; a right pane offering both would be two mechanisms fighting over one job. Left in place they'd have been unreachable code. This was an explicit product call, not incidental cleanup — flagging it because it removes shipped functionality, and reversing it later means rebuilding the popover against the new layout.
Net −1457 lines.
One bug found in the browser, and its test
Removing a relationship's last edge left the detail pane showing a stale header with no rows.
The re-selection effect asked whether the selected key still resolved — and it does. Removing the last edge doesn't remove the group; it stays declared, which is exactly what makes it addable again. The rail is built from
populated, so that's what the check has to be against. Fixed, with a unit test that pins the distinction rather than the symptom.Verification
Tests: 230 files / 4877 passed. The drop from 4916 is the 40 view-settings tests deleted above.
eslint+svelte-check: 0 errors, 0 warnings. Full pre-push gate green (frontend + skill + Rust + e2e).Browser (
dev:browser, isolatedNODESPACE_HOME, real DB untouched) against purpose-builtconference/speaker/venueschemas — one relationship with enum + string edge fields, onecardinality: onewith none:ROLE: Keynote / TOPIC: Knowledge Graphs— the value edited from the conference side, confirming it's one physical edge, not a copy — with zero inputs and zero remove buttons, and the hint "Edit these from the node that owns this relationship"focusOrOpenNode, reusing a tab+ Add→ chooser → search → add flow, with the chooser correctly empty when every relationship is populated🤖 Generated with Claude Code
https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK