Owner profile: Organizations becomes a third tab with route-specific rendering #430

Closed
opened 2026-09-13 01:05:49 +00:00 by crueber · 3 comments
Owner

Owner profile: Organizations becomes a third tab with route-specific rendering

What's requested

The owner surface currently has a two-tab strip (OwnerTabs, Profile | Repositories). Extend it to three tabs — Profile | Repositories | Organizations — and make the pages route-specific:

  • /:owner — the profile view as it renders today (identity header, edit form, membership rail, repo-count teaser).
  • /:owner/repositories — the repositories tab only (toolbar + grid + import footer), as today.
  • /:owner/organizations — a new Organizations tab: the membership list moves out of the profile view into this dedicated tab. Route shows ONLY the org membership list under the tab strip (org name linking to /:org, explicit "No organizations" empty state — the GitHub-parity empty state the rail already has).

Evidence (current tree)

  • web/src/pages/Repos.jsx:88-133 — OwnerTabs renders two <A> links and derives the active tab from loc.pathname === '/{owner}/repositories'. A third tab needs the same pattern for /{owner}/organizations.
  • web/src/pages/Repos.jsx:301-310 — the membership data already loads as a separate cached key memberorgs:{owner} via repos.users.orgs(owner()) (GET /api/v1/users/{principal}/orgs), so the new tab consumes existing machinery — no backend change required.
  • web/src/pages/Repos.jsx:440-465 — the current membership rail renders inside the profile view (view() === "profile" branch). This ticket moves that list into the new tab; decide whether the profile view keeps a compact rail as well or goes rail-free (planner's call — either is acceptable; the tab must always exist for user profiles).
  • web/src/pages/Repos.jsx:512-518 — the tab strip renders on both views outside the isOrg gate. Org profiles (isOrg() branch) deliberately omit the membership section because member principals are email spellings, not routable owner slugs (#370) — keep the Organizations tab user-profiles only (org profiles still render Profile | Repositories).
  • web/src/pages/Repos.jsx:681-695 — route components: Repos → OwnerPage view="profile", OwnerRepositories → view="repos". Add /:owner/organizations in web/src/index.jsx (next to the /:owner/repositories registration) mapped to a new view="orgs" variant; note the same reservation class as /repositories — an org literally named organizations loses its UI page, client-side only.

Architecture notes

  • This absorbs the remainder of #423 (closed): #423's backend membership endpoint (users.orgs, memberorgs:{owner} cache key, explicit empty state) is landed and stays; only its presentation surface moves from the profile rail to a dedicated tab. Reference this ticket in the closed issue with a comment.
  • Server already serves the SPA shell on two-segment owner paths (repoPageGated) — client-only change, same as #422.
  • No cached-response shape changes, so no ETag coverage concern.
  • No non-BMP characters; keep the tab strip's existing classes and active-underline pattern for the third tab.

Acceptance criteria

  • /:owner shows the profile (identity, edit form, teaser) — no repos grid, no full membership list replacing the profile.
  • /:owner/repositories shows only the repos toolbar/grid/import listing under the tab strip.
  • /:owner/organizations shows the membership list (links to /:org) with the explicit "No organizations" empty state; loading state preserved.
  • Tab strip reads Profile | Repositories | Organizations on all three routes, active-tab underline and aria-current correct per route.
  • Org profiles do NOT get the Organizations tab (email-principal rationale, #370).
  • Repositories count badge behavior unchanged.
  • Existing tests (web/test/unit/) updated; a unit test covers the active-tab derivation for all three routes.
  • #423 gets a cross-reference comment pointing here.
# Owner profile: Organizations becomes a third tab with route-specific rendering ## What's requested The owner surface currently has a two-tab strip (`OwnerTabs`, Profile | Repositories). Extend it to three tabs — **Profile | Repositories | Organizations** — and make the pages route-specific: - `/:owner` — the profile view as it renders today (identity header, edit form, membership rail, repo-count teaser). - `/:owner/repositories` — the repositories tab only (toolbar + grid + import footer), as today. - `/:owner/organizations` — a new **Organizations tab**: the membership list moves out of the profile view into this dedicated tab. Route shows ONLY the org membership list under the tab strip (org name linking to `/:org`, explicit "No organizations" empty state — the GitHub-parity empty state the rail already has). ## Evidence (current tree) - `web/src/pages/Repos.jsx:88-133` — `OwnerTabs` renders two `<A>` links and derives the active tab from `loc.pathname === '/{owner}/repositories'`. A third tab needs the same pattern for `/{owner}/organizations`. - `web/src/pages/Repos.jsx:301-310` — the membership data already loads as a separate cached key `memberorgs:{owner}` via `repos.users.orgs(owner())` (GET `/api/v1/users/{principal}/orgs`), so the new tab consumes existing machinery — **no backend change required**. - `web/src/pages/Repos.jsx:440-465` — the current membership rail renders inside the *profile* view (`view() === "profile"` branch). This ticket moves that list into the new tab; decide whether the profile view keeps a compact rail as well or goes rail-free (planner's call — either is acceptable; the tab must always exist for user profiles). - `web/src/pages/Repos.jsx:512-518` — the tab strip renders on both views outside the isOrg gate. Org profiles (`isOrg()` branch) deliberately omit the membership section because member principals are email spellings, not routable owner slugs (#370) — keep the Organizations tab **user-profiles only** (org profiles still render Profile | Repositories). - `web/src/pages/Repos.jsx:681-695` — route components: `Repos` → `OwnerPage view="profile"`, `OwnerRepositories` → `view="repos"`. Add `/:owner/organizations` in `web/src/index.jsx` (next to the `/:owner/repositories` registration) mapped to a new `view="orgs"` variant; note the same reservation class as `/repositories` — an org literally named `organizations` loses its UI page, client-side only. ## Architecture notes - This **absorbs the remainder of #423** (closed): #423's backend membership endpoint (`users.orgs`, `memberorgs:{owner}` cache key, explicit empty state) is landed and stays; only its presentation surface moves from the profile rail to a dedicated tab. Reference this ticket in the closed issue with a comment. - Server already serves the SPA shell on two-segment owner paths (repoPageGated) — client-only change, same as #422. - No cached-response shape changes, so no ETag coverage concern. - No non-BMP characters; keep the tab strip's existing classes and active-underline pattern for the third tab. ## Acceptance criteria - [ ] `/:owner` shows the profile (identity, edit form, teaser) — no repos grid, no full membership list replacing the profile. - [ ] `/:owner/repositories` shows only the repos toolbar/grid/import listing under the tab strip. - [ ] `/:owner/organizations` shows the membership list (links to `/:org`) with the explicit "No organizations" empty state; loading state preserved. - [ ] Tab strip reads Profile | Repositories | Organizations on all three routes, active-tab underline and `aria-current` correct per route. - [ ] Org profiles do NOT get the Organizations tab (email-principal rationale, #370). - [ ] Repositories count badge behavior unchanged. - [ ] Existing tests (`web/test/unit/`) updated; a unit test covers the active-tab derivation for all three routes. - [ ] #423 gets a cross-reference comment pointing here.
crueber added this to the v1 milestone 2026-09-13 01:06:07 +00:00
Author
Owner

Fix PR: #434 (branch fix/issue-430). Client-only: /:owner/organizations → view="orgs" with the membership list moved verbatim from the profile rail; OwnerTabs reads Profile | Repositories | Organizations (same classes/underline/aria-current, three-way derivation); isOrg hides the third tab on org profiles (#370); /:owner goes rail-free (planner's call). node --test 903 total / 901 pass / 2 pre-existing live-server smoke failures (identical pristine); vite build green. No backend change, no new deps.

Fix PR: https://git.packden.us/crueber/walhub/pulls/434 (branch fix/issue-430). Client-only: /:owner/organizations → view="orgs" with the membership list moved verbatim from the profile rail; OwnerTabs reads Profile | Repositories | Organizations (same classes/underline/aria-current, three-way derivation); isOrg hides the third tab on org profiles (#370); /:owner goes rail-free (planner's call). node --test 903 total / 901 pass / 2 pre-existing live-server smoke failures (identical pristine); vite build green. No backend change, no new deps.
Author
Owner

REVIEW PR #434 (fix/issue-430, commit 5001acc) — verified in scratch worktree /tmp/pr434 (main worktree untouched, still clean on main apart from pre-existing untracked .opencode/).

(1) Route — web/src/index.jsx:79-89: /:owner/repositories (79) → /:owner/organizations (85) → /:owner (86) → /:owner/teams/:slug (88) → /:owner/:name (89). Static-before-dynamic holds; reservation comment present (names 'organizations', cites #430, same class as /repositories + /orgs/new). Named import updated (line 18). PASS.
(2) Tab strip — Repos.jsx:103-152: three-way pathname derivation (organizations probed before repositories, fallback profile); all three tabs share one cls (3x class={cls}), same active-underline classList + aria-current pattern on the third tab. Strip call site :519 feeds isOrg. PASS.
(3) Orgs tab content — Repos.jsx:602-644 (view=="orgs" gate): verbatim move — orgs-rail hook, heading, normalizeMemberOrgs, /:org links, 'No organizations' empty state, loading… fallback all kept; same memberorgs:{owner} key + repos.users.orgs fetch; no toolbar/grid/teaser in branch (pinned by test). PASS.
(4) Profile rail-free — Repos.jsx:468-476: rail removed, explanatory comment left; only remaining orgs-rail is the orgs-view one (:603). Teaser intact (View all → deep-links /repositories). Matches the issue's planner's-call option. PASS.
(5) Org profiles excluded — :142 Show when={!props.isOrg} gates only the third tab; Profile/Repositories unconditional. #370 rationale cited in code + docs. (Direct-URL /:org/organizations on an org slug still renders the branch, but the tab never links there — acknowledged in code comment :600; out of scope.) PASS.
(6) Count badge — still rides the Repositories tab only (test pins badge between repos/orgs links), derives from shared repos:{owner} payload, no new fetch. PASS.
(7) #422 views intact — owner-repos-tab-422.test.js updated (2 pins: import line, strip call site with isOrg) and passing; profile/repos gating untouched. PASS.
(8) #423 cross-ref — comment 4329 on #423 verified via API: full body references #430 + PR #434 + endpoint-stays rationale. PASS.
(9) No backend change (6 files: 2 docs + 2 web src + 2 web tests; go.mod/package.json/lock untouched, no new deps); docs accurate (12_web_ui.md §2.3 route table + FIXED #430 entry; features/01_identity_permissions.md membership-move decision — law 12 satisfied). Laws 1/7/8 hold (no deps, N/A long-work, existing OwnerPage(view)+route seam).

VERIFY: node --test web/test/unit/*.test.js → 901 pass / 2 fail; the 2 fails are both smoke.test.js live-server shell-serve tests (fail identically pre/post vite build — environmental, no server in worktree, unrelated to this client-route change). Targeted owner-orgs-tab-430 + owner-repos-tab-422 → 17/17 pass. vite build green (2.15s). No browser drive per review instructions (headless tests + source reasoning only) — browser proof stays open as with #422.

No fixes needed — nothing pushed.

MERGE RECOMMENDATION: ready to merge.

REVIEW PR #434 (fix/issue-430, commit 5001acc) — verified in scratch worktree /tmp/pr434 (main worktree untouched, still clean on main apart from pre-existing untracked .opencode/). (1) Route — web/src/index.jsx:79-89: /:owner/repositories (79) → /:owner/organizations (85) → /:owner (86) → /:owner/teams/:slug (88) → /:owner/:name (89). Static-before-dynamic holds; reservation comment present (names 'organizations', cites #430, same class as /repositories + /orgs/new). Named import updated (line 18). PASS. (2) Tab strip — Repos.jsx:103-152: three-way pathname derivation (organizations probed before repositories, fallback profile); all three tabs share one cls (3x class={cls}), same active-underline classList + aria-current pattern on the third tab. Strip call site :519 feeds isOrg. PASS. (3) Orgs tab content — Repos.jsx:602-644 (view=="orgs" gate): verbatim move — orgs-rail hook, heading, normalizeMemberOrgs, /:org links, 'No organizations' empty state, loading… fallback all kept; same memberorgs:{owner} key + repos.users.orgs fetch; no toolbar/grid/teaser in branch (pinned by test). PASS. (4) Profile rail-free — Repos.jsx:468-476: rail removed, explanatory comment left; only remaining orgs-rail is the orgs-view one (:603). Teaser intact (View all → deep-links /repositories). Matches the issue's planner's-call option. PASS. (5) Org profiles excluded — :142 Show when={!props.isOrg} gates only the third tab; Profile/Repositories unconditional. #370 rationale cited in code + docs. (Direct-URL /:org/organizations on an org slug still renders the branch, but the tab never links there — acknowledged in code comment :600; out of scope.) PASS. (6) Count badge — still rides the Repositories tab only (test pins badge between repos/orgs links), derives from shared repos:{owner} payload, no new fetch. PASS. (7) #422 views intact — owner-repos-tab-422.test.js updated (2 pins: import line, strip call site with isOrg) and passing; profile/repos gating untouched. PASS. (8) #423 cross-ref — comment 4329 on #423 verified via API: full body references #430 + PR #434 + endpoint-stays rationale. PASS. (9) No backend change (6 files: 2 docs + 2 web src + 2 web tests; go.mod/package.json/lock untouched, no new deps); docs accurate (12_web_ui.md §2.3 route table + FIXED #430 entry; features/01_identity_permissions.md membership-move decision — law 12 satisfied). Laws 1/7/8 hold (no deps, N/A long-work, existing OwnerPage(view)+route seam). VERIFY: node --test web/test/unit/*.test.js → 901 pass / 2 fail; the 2 fails are both smoke.test.js live-server shell-serve tests (fail identically pre/post vite build — environmental, no server in worktree, unrelated to this client-route change). Targeted owner-orgs-tab-430 + owner-repos-tab-422 → 17/17 pass. vite build green (2.15s). No browser drive per review instructions (headless tests + source reasoning only) — browser proof stays open as with #422. No fixes needed — nothing pushed. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #434 (review clean — all 9 checks pass, #422 intact, #423 cross-ref posted), merged. Closing.

Fixed by PR #434 (review clean — all 9 checks pass, #422 intact, #423 cross-ref posted), 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#430
No description provided.