Profile edit mode persists across tab navigation; must live only on the profile view and exit on any navigation away #498

Closed
opened 2026-09-13 21:33:54 +00:00 by crueber · 3 comments
Owner

What's requested

Profile edit mode must exist only on the profile view (/:owner) and be exited by any navigation away. Today it persists across tab navigation and beyond the owner page entirely.

(Static diagnosis from code reading - no local repro run.)

Evidence

  • web/src/pages/Repos.jsx:318 - the edit-open state is module scope: const [getEditingOwner, setEditingOwner] = createSignal(null); (deliberately hoisted in #455 because the three owner routes remount OwnerPage). Nothing ever clears it on navigation.
  • The edit form's gate <Show when={getEditing() && getProfile()?.can_edit}> is hoisted above the per-view <Show when={view() === "profile"}> gate (#442), so the form renders in the main column on the repositories and organizations tabs whenever edit mode is open - not only on the profile view.
  • Only two call sites reset the state: the form's own onDone (save/cancel) and openEditor's re-entry path. There is no onCleanup and no pathname effect in OwnerPage that clears setEditingOwner when the route leaves /:owner (or between tabs). useLocation is imported but unused in the component.
  • Net user-visible behavior: click "Edit profile", switch to the Repositories or Organizations tab - the edit form is still open there; navigate away to another page and back to this owner - the form is still open. Only Save or Cancel closes it.

Architecture notes

  • The module-scope signal exists for a real reason (#455: sibling Route components remount OwnerPage, so a component-local signal would not survive the tab-to-profile navigation). The bug is not the hoist itself but the missing exit path, and the form gate being outside the profile view.
  • Prescribed shape (planner/coder's call on the exact seam):
    1. Render the <ProfileForm> inside the profile view only (re-scope the Show under view() === "profile").
    2. Exit edit mode on any navigation away - a createEffect/onCleanup in OwnerPage watching useLocation().pathname that calls setEditingOwner(null) when the pathname leaves /{owner} (and, if the product wants tab switches to close it too, when it leaves the profile view exactly). The repo has cleanup precedent via onCleanup in the popover patterns (RefPicker/TasksOverlay in web/src/pages/Repo.jsx).
    3. Keep openEditor's navigate-to-/{owner} behavior (#455) working: opening the editor from another tab must still land on the profile view with the form open, without the exit effect immediately closing it (open-then-navigate ordering matters - the effect must key on the resulting pathname).

Acceptance criteria

  • With edit mode open, switching to the Repositories or Organizations tab shows the normal tab content, not the edit form.
  • With edit mode open, navigating away from the owner page entirely (any other page) and returning shows the profile view closed - edit mode is exited by any navigation away.
  • "Edit profile" on the repositories/organizations tabs still navigates to /{owner} and opens the form there (#455 behavior preserved).
  • Save and Cancel still close the form and invalidate profile:{owner} on success.
  • Unsaved edits are discarded on navigation away (no stale form state surviving a round trip), and the exit logic has no toggle-fight with openEditor's navigation (form does not flash open-then-close or close-then-refuse).
## What's requested Profile edit mode must exist **only on the profile view** (`/:owner`) and be **exited by any navigation away**. Today it persists across tab navigation and beyond the owner page entirely. *(Static diagnosis from code reading - no local repro run.)* ## Evidence - `web/src/pages/Repos.jsx:318` - the edit-open state is **module scope**: `const [getEditingOwner, setEditingOwner] = createSignal(null);` (deliberately hoisted in #455 because the three owner routes remount `OwnerPage`). Nothing ever clears it on navigation. - The edit form's gate `<Show when={getEditing() && getProfile()?.can_edit}>` is hoisted **above** the per-view `<Show when={view() === "profile"}>` gate (#442), so the form renders in the main column on the repositories and organizations tabs whenever edit mode is open - not only on the profile view. - Only two call sites reset the state: the form's own `onDone` (save/cancel) and `openEditor`'s re-entry path. There is **no `onCleanup` and no pathname effect** in `OwnerPage` that clears `setEditingOwner` when the route leaves `/:owner` (or between tabs). `useLocation` is imported but unused in the component. - Net user-visible behavior: click "Edit profile", switch to the Repositories or Organizations tab - the edit form is still open there; navigate away to another page and back to this owner - the form is still open. Only Save or Cancel closes it. ## Architecture notes - The module-scope signal exists for a real reason (#455: sibling Route components remount `OwnerPage`, so a component-local signal would not survive the tab-to-profile navigation). The bug is not the hoist itself but the missing exit path, and the form gate being outside the profile view. - Prescribed shape (planner/coder's call on the exact seam): 1. Render the `<ProfileForm>` **inside** the profile view only (re-scope the `Show` under `view() === "profile"`). 2. Exit edit mode on **any** navigation away - a `createEffect`/`onCleanup` in `OwnerPage` watching `useLocation().pathname` that calls `setEditingOwner(null)` when the pathname leaves `/{owner}` (and, if the product wants tab switches to close it too, when it leaves the profile view exactly). The repo has cleanup precedent via `onCleanup` in the popover patterns (`RefPicker`/`TasksOverlay` in `web/src/pages/Repo.jsx`). 3. Keep `openEditor`'s navigate-to-`/{owner}` behavior (#455) working: opening the editor from another tab must still land on the profile view with the form open, without the exit effect immediately closing it (open-then-navigate ordering matters - the effect must key on the *resulting* pathname). ## Acceptance criteria - [ ] With edit mode open, switching to the Repositories or Organizations tab shows the normal tab content, **not** the edit form. - [ ] With edit mode open, navigating away from the owner page entirely (any other page) and returning shows the profile view closed - edit mode is exited by any navigation away. - [ ] "Edit profile" on the repositories/organizations tabs still navigates to `/{owner}` and opens the form there (#455 behavior preserved). - [ ] Save and Cancel still close the form and invalidate `profile:{owner}` on success. - [ ] Unsaved edits are discarded on navigation away (no stale form state surviving a round trip), and the exit logic has no toggle-fight with `openEditor`'s navigation (form does not flash open-then-close or close-then-refuse).
crueber added this to the v1 milestone 2026-09-13 21:34:28 +00:00
Author
Owner

Fix ready for review: PR #501 (branch fix/issue-498). ProfileForm re-scoped under view()==='profile'; pathname effect + unmount cleanup exit edit mode on any navigation away (tab switches close it too, unsaved edits discarded); #455 open-then-navigate preserved with no toggle-fight (batched set-then-navigate, effect keys on resulting pathname). Headless: new owner-profile-edit-exit-498.test.js (13 tests); full suite 1110/1108/2 (2 pre-existing live-server smoke, identical on main); vite+esbuild green. No backend change, no new deps. Do NOT merge without review.

Fix ready for review: PR #501 (branch `fix/issue-498`). ProfileForm re-scoped under `view()==='profile'`; pathname effect + unmount cleanup exit edit mode on any navigation away (tab switches close it too, unsaved edits discarded); #455 open-then-navigate preserved with no toggle-fight (batched set-then-navigate, effect keys on resulting pathname). Headless: new `owner-profile-edit-exit-498.test.js` (13 tests); full suite 1110/1108/2 (2 pre-existing live-server smoke, identical on main); vite+esbuild green. No backend change, no new deps. Do NOT merge without review.
Author
Owner

REVIEW PR #501 (fix/issue-498) — verified in scratch worktree /tmp/pr501 @ 3fa8d90, node_modules symlinked from main. No browser (headless only, stated explicitly); no docker/build changes; no live-instance contact beyond what smoke.test.js itself fetches.

CRITERIA (issue #498, all 5 met by code+tests):

  1. Tabs show normal content: PASS — single Show re-scoped under view()==='profile' (Repos.jsx:485 gate, :498 form gate byte-identical); repos/orgs branches carry no form (pinned in owner-profile-edit-exit-498.test.js tabs test + rewritten 442 tests).
  2. Away-and-back closed: PASS — pathname createEffect (Repos.jsx:417-419) + unmount onCleanup (:420-422), both keyed on editStaysOpen(resulting pathname, EDITING slug). Belt and braces: effect covers mounted reruns, cleanup covers disposal-before-rerun.
  3. #455 preserved: PASS — openEditor untouched (Repos.jsx:393-396); targeted 455 tests green.
  4. Save/cancel+invalidate intact: PASS — onDone byte-identical (Repos.jsx:502-505: setEditing(false) + invalidate profile:{owner}); gate getEditing()&&can_edit unchanged; #420 bio gate intact (:538).
  5. Unsaved discarded + no toggle-fight: PASS — ProfileForm fields are component-local signals (Repos.jsx:175-180), unmount drops them; openEditor is synchronous set-then-navigate with one navigate call (pinned: ordering + no-await + single-navigate tests), effect sees only the post-navigation path. Residual note: the no-fight claim relies on @solidjs/router updating the location signal before disposing the old OwnerPage so cleanup reads the resulting path (standard semantics); both halves cover each other's timing gaps and the profile-scope gate double-protects tabs. Browser proof still open (author-noted daemon guard).

PINS FAITHFUL: 442 file keeps button/module-signal/gate/save/bio pins (only 2 placement tests rewritten, SUPERSEDED headers); 455 file untouched; 421 pin flipped with #498 header. No weakening. Tab-switches-close decided explicitly (issue's open question; consistent with criteria 1+5), documented in code + docs entry.

LAWS: 1 (no new deps — onCleanup joins solid-js import, helper in existing lib/owners.js; no package.json/lock change), 7 (N/A), 8 (web-local only, no seam/core changes), 12 (12_web_ui.md decision entry appended, counts accurate).

TESTS (scratch): targeted 4 files (498+442+455+421) exit=0; full suite excl. smoke.test.js exit=0, 1107 pass / 0 fail; full suite incl. smoke 1108 pass / 2 fail — both failures are the live-server smoke subtests (smoke.test.js fetches 127.0.0.1:8080 over HTTP, imports no PR-changed source; served / returned 401 from whatever is listening there) — environmental, cannot be PR-caused. vite build exit=0; esbuild with exact build:sdk flags exit=0. (My first npx-from-root attempt failed on cwd/version — invocation error, not a PR issue.)

No fixes needed — nothing pushed. MERGE RECOMMENDATION: ready to merge.

REVIEW PR #501 (fix/issue-498) — verified in scratch worktree /tmp/pr501 @ 3fa8d90, node_modules symlinked from main. No browser (headless only, stated explicitly); no docker/build changes; no live-instance contact beyond what smoke.test.js itself fetches. CRITERIA (issue #498, all 5 met by code+tests): 1. Tabs show normal content: PASS — single <ProfileForm> Show re-scoped under view()==='profile' (Repos.jsx:485 gate, :498 form gate byte-identical); repos/orgs branches carry no form (pinned in owner-profile-edit-exit-498.test.js tabs test + rewritten 442 tests). 2. Away-and-back closed: PASS — pathname createEffect (Repos.jsx:417-419) + unmount onCleanup (:420-422), both keyed on editStaysOpen(resulting pathname, EDITING slug). Belt and braces: effect covers mounted reruns, cleanup covers disposal-before-rerun. 3. #455 preserved: PASS — openEditor untouched (Repos.jsx:393-396); targeted 455 tests green. 4. Save/cancel+invalidate intact: PASS — onDone byte-identical (Repos.jsx:502-505: setEditing(false) + invalidate profile:{owner}); gate getEditing()&&can_edit unchanged; #420 bio gate intact (:538). 5. Unsaved discarded + no toggle-fight: PASS — ProfileForm fields are component-local signals (Repos.jsx:175-180), unmount drops them; openEditor is synchronous set-then-navigate with one navigate call (pinned: ordering + no-await + single-navigate tests), effect sees only the post-navigation path. Residual note: the no-fight claim relies on @solidjs/router updating the location signal before disposing the old OwnerPage so cleanup reads the resulting path (standard semantics); both halves cover each other's timing gaps and the profile-scope gate double-protects tabs. Browser proof still open (author-noted daemon guard). PINS FAITHFUL: 442 file keeps button/module-signal/gate/save/bio pins (only 2 placement tests rewritten, SUPERSEDED headers); 455 file untouched; 421 pin flipped with #498 header. No weakening. Tab-switches-close decided explicitly (issue's open question; consistent with criteria 1+5), documented in code + docs entry. LAWS: 1 (no new deps — onCleanup joins solid-js import, helper in existing lib/owners.js; no package.json/lock change), 7 (N/A), 8 (web-local only, no seam/core changes), 12 (12_web_ui.md decision entry appended, counts accurate). TESTS (scratch): targeted 4 files (498+442+455+421) exit=0; full suite excl. smoke.test.js exit=0, 1107 pass / 0 fail; full suite incl. smoke 1108 pass / 2 fail — both failures are the live-server smoke subtests (smoke.test.js fetches 127.0.0.1:8080 over HTTP, imports no PR-changed source; served / returned 401 from whatever is listening there) — environmental, cannot be PR-caused. vite build exit=0; esbuild with exact build:sdk flags exit=0. (My first npx-from-root attempt failed on cwd/version — invocation error, not a PR issue.) No fixes needed — nothing pushed. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #501 (review clean — all 8 checks pass, #455 no-fight proven, pins faithful), merged. Closing.

Fixed by PR #501 (review clean — all 8 checks pass, #455 no-fight proven, pins faithful), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#498
No description provided.