Skip to content

feat(nav): account-centric dropdown menu#404

Open
alukach wants to merge 31 commits into
mainfrom
worktree-dropdown-submenus
Open

feat(nav): account-centric dropdown menu#404
alukach wants to merge 31 commits into
mainfrom
worktree-dropdown-submenus

Conversation

@alukach

@alukach alukach commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Redesigns the nav + account dropdown to make it simple to:

  1. Navigate to the Products list view
  2. See all orgs that you're a member of ([Proposed Feature] UX - Clearer "My Organizations" List #232)
  3. Quickly link to an org's profile page
  4. See all products that belong to your orgs
  5. Quickly navigate to one of your orgs' products' pages
  6. Create new product
  7. Create new organization
  8. View outstanding invitations
  9. Quickly navigate to Admin pages
  10. Logout

This generally boils down to a UI like this:

[img] Anthony Lukach  you  ▸ | # <- submenu, your account
[img] NOAA                 ▸ | # <- submenu, any other account you're a member of
──────────────────────────── |
+ New product                | # <- link, if permissions allow
+ New organization           | # <- link, if permissions allow
──────────────────────────── |
Invitations(•)             ▸ | # <- submenu, outstanding membership invitations
Admin                      ▸ | # <- submenu, if permissions allow
──────────────────────────── |
Log out                      |
Before After
image image

How

  • Nav (NavigationAuthButtons): AuthButtons (server) owns the right
    side and switches responsively — desktop shows the Products link + the account
    dropdown; mobile collapses to a single hamburger. The Products link
    (ProductsNavLink, Berkeley Mono) is shown to everyone but hidden on the
    marketing homepage.
  • Data (AuthButtons): resolves what the client can't (memberships only
    carry ids): the org accounts you belong to, the products owned by you and by
    each of those orgs (listByAccount(id, 20), filtered by GetRepository so
    restricted products you can't access aren't shown), and any pending
    invitations — all concurrently. Only org-level memberships (repository_id
    undefined) count as orgs you belong to.
  • Desktop dropdown (AccountDropdown): built from DropdownSubmenu, which
    shares a DropdownItems renderer with DropdownSection (items, then
    optional actions after a separator; hidden when empty).
  • Mobile (MobileMenu): a full-screen sheet (Radix Themes Dialog) with
    inline single-open accordions and full-width tap targets — same content as the
    dropdown.

Notes / follow-ups

  • Product creation is a single "New product" link; the create form already lets
    you pick the owner account.
  • The mobile menu is a full-screen sheet with accordion sections; desktop keeps
    the hover dropdown.
  • Not visually verified yet (needs a running app + login) — worth a pass on the
    mobile sheet in particular.
  • DropdownSection.integration.test.tsx fails locally with React.Children.only
    — pre-existing/environmental (react-slot duplication, as that test documents);
    it fails identically on main and is unrelated to this change.

Tests

  • Unit tests for DropdownSection, DropdownSubmenu (visibility + separator
    logic) and accountMenu (invitationLink routing + fallbacks).
  • tsc and ESLint clean; 15/15 unit tests pass. (The nav/mobile components are
    UI-only, covered by manual review rather than unit tests.)

🤖 Generated with Claude Code

closes #232
closes #309

Convert the flat Organizations / Products / Admin sections of the account
dropdown into hover submenus:

- Organizations: first 5 orgs (by name) then "Add Organization" (when permitted)
- Products: first 5 owned products then "All Products" then "Create Product"
  (when permitted)
- Admin: the admin tools (admins only)

Resolve org names and owned products server-side in AuthButtons (memberships
carry only ids) and pass them to the client dropdown. Add a reusable
DropdownSubmenu component sharing a DropdownItems renderer with DropdownSection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
source-cooperative Ready Ready Preview, Comment Jul 1, 2026 11:37pm

Request Review

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 4m 39s —— View job


Claude is reviewing this PR

  • Gather context (diff, CLAUDE.md, changed files)
  • Review changed files for correctness/security/best-practice issues
  • Post findings

❌ Changes requested — see findings below.

  • src/components/layout/AuthButtons.tsx:52-53 + :72-75 — products are fetched with a hard listByAccount(id, 20) limit before the GetRepository authorization filter is applied, instead of filtering first and then capping. If an org/account has 20+ products and some of the first 20 (in DynamoDB sort-key order) are restricted from the current viewer, the dropdown can show "No products yet" (or a truncated list) even though the viewer actually has access to other products belonging to that account beyond the unfiltered top-20 window. Compare with IndividualProfilePage.tsx:23-31, which fetches up to 1000 products and then filters by Actions.GetRepository — the pattern this PR's comment says it mirrors, but the order of operations doesn't actually match.
    // instead of listByAccount(id, 20) -> filter, over-fetch then filter+cap:
    productsTable.listByAccount(id, 100).then((r) =>
      r.products.filter((p) => isAuthorized(session, p, Actions.GetRepository)).slice(0, 20)
    )
    (Tune the raw limit to whatever's an acceptable worst case for accounts with many restricted products; a full fix would paginate until 20 authorized results are found or the account is exhausted.)

Everything else looks solid:

  • newProductUrl(owner) allowlist-checks owner against potentialOwnerAccounts before use (products/new/page.tsx:75-77) — no open redirect / unvalidated owner injection.
  • logout() extraction to logout.ts is a faithful, behavior-preserving move from the old inline handleLogout.
  • DropdownItems/DropdownSubmenu visibility, separator, and disabled+tooltip logic checked against their new unit tests — matches intent (stable keys via index-then-filter, separator only when both items and actions are non-empty).
  • The per-org "New product" permission check (isAuthorized(session, "*", Actions.CreateRepository)) is a coarse, account-agnostic gate — same pattern already used by the standalone /products/new page and enforced precisely (per target org, via hasRole for Owners/Maintainers) server-side in createProduct. Pre-existing pattern, not a regression.

Not evaluated: visual/UX behavior (dropdown/mobile sheet appearance), since this is a static diff review — @tylere's UI feedback in the thread is worth addressing separately but isn't a correctness/security bug.

Address dropdown redesign feedback:

- Move View/Edit Profile behind a "Profile" submenu
- "Create Organization" / "Create Product" now use a consistent verb and a
  leading + icon
- Always show a divider before Logout (drop UploadsSubmenu's now-redundant
  trailing separator so it isn't doubled when uploads are active)
- Truncate long org/product names so one entry can't blow out the menu width

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
session.memberships includes Invited records (authorized for GetMembership),
so an outstanding invite was surfaced as an org the user already belongs to.
Filter to state === Member, matching the established pattern in
lookups.getManageableAccounts and products/new.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pending invitations:
- Red dot on the account avatar when the user has pending invites
- "Invitations" section (dot-marked) at the top of the dropdown listing each
  invite; clicking navigates to the org/product page whose
  PendingInvitationBanner lets the user accept/decline
- Resolve org names and product titles server-side in AuthButtons; a small
  pure invitationLink() (unit-tested) picks the org- vs product-page route

Products link:
- Add a prominent "Products" link in the nav next to the account dropdown
- Drop the now-redundant "All Products" entry from the Products submenu

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alukach and others added 2 commits June 30, 2026 17:14
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…name

- Org/product/invitation names render at medium weight so they read as data,
  distinct from the menu commands around them
- Nav Products link: match the username's size (3) and drop the bold weight
  for a more prominent, less heavy look

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Show up to 20 orgs/products in their submenus (was 5); fetch 20 owned products
- Render entity names in a monospace face
- Cap submenu height so long lists scroll instead of overflowing the viewport

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ducts)

Replace the separate Profile / Products / Organizations submenus with one list
of accounts — yourself plus each org you belong to. Each account is a submenu
that links to its page and lists that account's products (up to 20), so you can
reach an org's products, not just your own.

- AuthButtons resolves products for you and each member org (parallel, cached)
- Global "New product" / "New organization" actions (the create form already
  picks the owner account), permission-gated
- Drop "Edit Profile" from the menu (edit from the profile page)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ck in menu

- Drop monospace from account/product/invitation names
- Label accounts by display name (not id) with a small avatar to the left
- Grey "No products yet" when an account has no products
- Invitations always shown; grey "No pending invitations" when there are none
  (red dot only when invites exist)
- Move the "Products" (all products) link back to the top of the menu and
  remove it from the nav bar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bound the dropdown and submenu content to a max width and let names shrink
(min-width: 0) and ellipsize within it, so a long org/product name no longer
expands the menu past its default width.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
text-overflow: ellipsis needs a definite width; the min-width:0 approach let
the row clip hard instead. Give names a fixed max-width (180px) so they
ellipsize, and drop the menu/submenu width caps that caused the hard cutoff.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…le menu

- Products link: match the product-list entry headers (var(--gray-a11), weight
  600), no uppercase/underline. Inline styles keep it consistent on the
  marketing page too, where `.landing a` otherwise forces uppercase + underline.
- Account dropdown spans nearly the full viewport on phones (<=640px) for a
  traditional, tap-friendly mobile menu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ray-12 on hover

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On phones the account controls collapse into a single hamburger that opens a
full-screen sheet: Products, your accounts (inline accordions revealing each
account's products), create actions, invitations, admin, and logout. Desktop
keeps the Products link + account dropdown unchanged.

- New MobileMenu (built on the @radix-ui/react-dialog primitive) with a
  single-open accordion and full-width tap targets
- AuthButtons now owns the whole right side and renders desktop vs mobile via
  responsive display; Products link extracted to ProductsNavLink (reused when
  logged out too)
- Shared useLogout hook (was duplicated logic in the dropdown)
- Drop the interim full-width-dropdown CSS; the sheet replaces it

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Switch MobileMenu from the raw @radix-ui/react-dialog primitive to Radix
  Themes' Dialog. The primitive portals outside the Theme, so tokens were
  undefined — no sheet background and the avatars rendered unsized. The themed
  Dialog re-applies the theme inside the portal (fixes both) and supplies the
  backdrop; content is styled full-screen.
- Hide the nav-bar Products link on the marketing homepage (usePathname === '/'),
  and fold its divider into ProductsNavLink so they hide together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alukach and others added 2 commits June 30, 2026 23:17
- useLogout hook (no React hooks inside) → plain logout() function
- DropdownItems: drop index-preservation gymnastics; stateless menu items don't
  need key stability across condition toggles
- reuse the .mobileDot class for the invitations dot (drop single-use const)
- fix stale "All Products" JSDoc example

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Revert an over-eager ponytail cut: filtering before mapping renumbers React
keys, reintroducing the reconcile-wrong-node footgun the original code guarded
against. Shared by DropdownSection + DropdownSubmenu, so keep the guard for
future callers that may toggle an item's condition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the sheet shell (MobileMenuSheet) so logged-in and logged-out variants
share the hamburger/full-screen chrome. When logged out, mobile now shows a
hamburger opening a sheet with the Products link and the Log In / Register
button (desktop keeps them inline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alukach and others added 2 commits July 1, 2026 12:47
Rename ProductsNavLink -> NavLinks; it now renders Products + Docs
(external docs.source.coop), hidden together on the marketing homepage.
Docs row added to both the logged-in and logged-out mobile sheets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New product / New organization now always render in the account dropdown.
When the user lacks permission they're disabled with a Radix tooltip
explaining why, instead of being hidden. DropdownItem gains a `tooltip`
field; disabled+tooltip items wrap a real (padded) disabled menu item in a
pointer-events-live span so the tooltip fires (matching SettingsLayout).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tylere

tylere commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Notes from experimenting with the UI:

  • I would like to see some better indication that the text lines under "View profile" or "View organization" are projects. (Even though I created them, it wasn't immediately clear that they are projects!).
  • The "View organization" label has a verb indicating what the link will do, while the product name links do not. Maybe the load web page icon should be used?
  • I think it is strange to have a submenu that lists the products of an organization (or user), but have the + New product not in the submenu as well. I think the + New product link should be at the bottom of the list of current projects of the user or organization. This would result in the + New organization link occurring under the list of organizations that the user belongs to (if any), which makes sense to me.
  • The user lookup page has a description "Looks up the email in Ory and ...". Suggest removing "in Ory" as an implementation detail (unless Ory is described elsewhere and we want to link to it).
  • Logging out took me back to the home page. I didn't expect this, since viewing the user page that I was on doesn't require being logged in. (I realize this might be old behavior because of the branching parent.)

@alukach

alukach commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Maybe the load web page icon should be used?

@tylere can you clarify what you mean by the "load web page icon"?

alukach and others added 2 commits July 1, 2026 16:35
…to account submenus

Addresses review feedback on the account menu:
- Products under each account now sit under a "Products" label with a page
  icon, so they read as products/links (not ambiguous plain text).
- "+ New product" moves to the bottom of each account's product list; it
  links to /products/new?owner=<account> to preselect that owner.
- "+ New organization" stays after the list of accounts you belong to.
Mobile menu mirrors the same structure (icon + inline New product).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
newProductUrl(ownerAccountId?) appends ?owner=<id>. The new-product page
reads it, validates it against the potential owner accounts, and passes it
to ProductCreationForm as the initial owner (falling back to the first
account otherwise). Opening "New product" from an org menu now defaults the
owner to that org.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tylere

tylere commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

By "load web page icon" I mean't the icon that you pointed out during today's Source Sync. (iirc, this was in response to Michelle's comment about how some links modify the page and some open new pages)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Proposed Feature] Send user email when invited to an organization [Proposed Feature] UX - Clearer "My Organizations" List

2 participants