Fix #410: VisSelect render path #412

Merged
crueber merged 2 commits from fix/issue-410 into main 2026-09-12 20:42:11 +00:00
Owner

Fixes #410 — the profile visibility selector rendered nothing through an opaque render path.

Research (research-first, hypotheses validated with evidence)

Against the pinned solid-js@1.9.15 sources in web/node_modules:

  • H1 CONFIRMED: a <For> children mapper is invoked per item as mapFn(item) (client mapArray's mapper, arity-1 like ours) or fn(item, () => i) (SSR simpleMap) — the index arrives as an accessor function, never a raw number, and the item is the option object, never o.label. A hand-rolled adapter calling children(item, i) / children(o.label) mismatches both real paths.
  • H2 CONFIRMED as an adapter bug, not framework timing: mapArray reads list() || [], so an undefined each renders nothing — the observed Cannot read properties of undefined (reading 'value') comes from invoking the mapper with an undefined ITEM, which the real path never does with a populated each.
  • H3 REFUTED for production: the vite bundle ships the runtime INSIDE /_ui/assets/*.js (verified: bundled, no external runtime import). The 404 was a harness artifact of bypassing the vite build — plain node has no JSX transform, so .jsx cannot even be imported (that unimportability IS the opaque path).
  • #4065 comment finding HELD: visibilityOptions() built fresh arrays/objects per call; <For> diffs by === identity, so every owner-kind refire rebuilt all <option> nodes and the select fell back to public. Now hoisted frozen constants.

Full write-up in docs/features/01_identity_permissions.md Decisions (#410 entry, law 12).

What changed

  • web/src/components/VisSelect.jsx (new): the one shared selector — real <For each={visibilityOptions(props.isOrg)}> path, arity-1 (o) mapper, value={props.value ?? ''} keeps the #394 blank-while-unseeded rule.
  • web/src/pages/Settings.jsx + web/src/pages/Access.jsx: inline selects replaced with <VisSelect> (Access gains aria-label="Visibility").
  • web/src/lib/visibility.js: visibilityOptions() returns hoisted frozen constants (deep-equal shape unchanged).
  • web/src/lib/visSelect.js (new): headless row model (visRows — exactly the current row selected, none for unknown/empty).
  • web/test/unit/vis-select.test.js (new): the live-render rig — the REAL For from solid-js (no adapter stubs) rendering populated rows and observing the selected mark follow public → private → authenticated plus the user→org relabel; H1/H2 shapes pinned; static guards that the component stays on the real path.

Verification

  • New rig: 9/9 pass; full node --test web/test/unit/*.test.js: 816/818 (the 2 fails are smoke.test.js needing a live server — a foreign process answers :8080 in this env; identical failures on pristine main, pre-existing/environmental).
  • vite build clean (593 kB bundle, options verified inside); no new deps (package.json untouched); gofmt clean (no Go changes).
  • Browser: OPEN — shared obscura daemon blocks loopback here, so no in-browser live-render drive; live-render is observed headless through the genuine solid-js For (see rig). A real-Chromium pass on /, a repo settings page, and /setup is still wanted on review hardware.
Fixes #410 — the profile visibility selector rendered nothing through an opaque render path. ## Research (research-first, hypotheses validated with evidence) Against the pinned `solid-js@1.9.15` sources in `web/node_modules`: - **H1 CONFIRMED:** a `<For>` children mapper is invoked per item as `mapFn(item)` (client `mapArray`'s `mapper`, arity-1 like ours) or `fn(item, () => i)` (SSR `simpleMap`) — the index arrives as an **accessor function**, never a raw number, and the item is the option object, never `o.label`. A hand-rolled adapter calling `children(item, i)` / `children(o.label)` mismatches both real paths. - **H2 CONFIRMED as an adapter bug, not framework timing:** `mapArray` reads `list() || []`, so an undefined `each` renders nothing — the observed `Cannot read properties of undefined (reading 'value')` comes from invoking the mapper with an undefined **ITEM**, which the real path never does with a populated `each`. - **H3 REFUTED for production:** the vite bundle ships the runtime **INSIDE** `/_ui/assets/*.js` (verified: bundled, no external runtime import). The 404 was a harness artifact of bypassing the vite build — plain node has no JSX transform, so `.jsx` cannot even be imported (that unimportability IS the opaque path). - **#4065 comment finding HELD:** `visibilityOptions()` built fresh arrays/objects per call; `<For>` diffs by `===` identity, so every owner-kind refire rebuilt all `<option>` nodes and the select fell back to `public`. Now hoisted frozen constants. Full write-up in `docs/features/01_identity_permissions.md` Decisions (#410 entry, law 12). ## What changed - `web/src/components/VisSelect.jsx` (new): the one shared selector — real `<For each={visibilityOptions(props.isOrg)}>` path, arity-1 `(o)` mapper, `value={props.value ?? ''}` keeps the #394 blank-while-unseeded rule. - `web/src/pages/Settings.jsx` + `web/src/pages/Access.jsx`: inline selects replaced with `<VisSelect>` (Access gains `aria-label="Visibility"`). - `web/src/lib/visibility.js`: `visibilityOptions()` returns hoisted frozen constants (deep-equal shape unchanged). - `web/src/lib/visSelect.js` (new): headless row model (`visRows` — exactly the current row `selected`, none for unknown/empty). - `web/test/unit/vis-select.test.js` (new): the **live-render rig** — the REAL `For` from `solid-js` (no adapter stubs) rendering populated rows and observing the selected mark follow `public → private → authenticated` plus the user→org relabel; H1/H2 shapes pinned; static guards that the component stays on the real path. ## Verification - New rig: 9/9 pass; full `node --test web/test/unit/*.test.js`: 816/818 (the 2 fails are `smoke.test.js` needing a live server — a foreign process answers :8080 in this env; identical failures on pristine main, pre-existing/environmental). - `vite build` clean (593 kB bundle, options verified inside); no new deps (`package.json` untouched); `gofmt` clean (no Go changes). - Browser: OPEN — shared obscura daemon blocks loopback here, so no in-browser live-render drive; live-render is observed headless through the genuine solid-js `For` (see rig). A real-Chromium pass on `/`, a repo settings page, and `/setup` is still wanted on review hardware.
Diagnosed the opaque render path against solid-js@1.9.15 sources and
documented the verdicts in docs/features/01_identity_permissions.md
Decisions: <For> children mapper shape (item, index-accessor), populated
each never yields undefined items, jsx-runtime 404 was a harness artifact
(runtime ships inside the vite bundle). visibilityOptions() now returns
hoisted frozen constants so <For> identity-diffing keeps the populated
selector settled; Settings + Access share the new VisSelect component;
web/test/unit/vis-select.test.js observes live-render through the real
For with no adapter stubs.
Sign in to join this conversation.
No description provided.