MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3500

14 Progressive disclosure is half-shipped at ONE workspace — the workspace-settings surface is still exposed, and an org invite lands the invitee in ZERO workspaces with no way in

Done
Description

Type · code (two arms: one service, one surface) Parent · none (root sibling) — out-of-band dogfooding finding, no in-flight card, and the epic that owns the surface (MOTIR-326, Epic 6) is done Discovered in · manual dogfooding, 2026-08-25, reported by Yue Repo · motir-core (all sites; the ADR lives here too)


Root cause / fix

docs/decisions/organization-tier.md decides two rules independently and never reconciles them at count = 1:

  • §6"the UI reveals a tier only when it offers a choice (its count ≥ 2)", and the settings table: "At one workspace the workspace-settings surface is hidden … the single Settings home folds in the workspace-config sections."
  • §5"Org add ⟹ NO workspace (no downward propagation) … an 'org-only' member in zero workspaces is a valid state — e.g. a billing admin."

At two or more workspaces both are right. At one they combine into a dead end: the tier the user must be placed into is the tier the UI has hidden. Only the header half of §6 shipped.

Arm A — the workspace-settings surface is still exposed at one workspace

ShellTierNav.tsx:103 gates the switcher correctly (workspaces.length >= 2). Nothing else is gated. /settings/workspace — titled "Workspace settings", carrying Name, Members and a Delete workspace danger zone — is reachable at count 1 from four places:

sitegate todayshould be
app/(authed)/_components/UserMenu.tsx:83 — the "Workspace settings" rownone (UserMenuProps has no count at all)hidden at ≤ 1
app/(authed)/_components/AppCommandPalette.tsx:305 — "Go to settings"!hasProject onlyhidden at ≤ 1
app/(authed)/_components/SidebarNav.tsx:386 — the settings door's no-project target!hasProject onlyhidden at ≤ 1
app/(authed)/settings/organization/_components/WorkspaceConfigCard.tsx:44 — the "Open workspace settings" buttonrenders only at orgWorkspaces.length <= 1 (settings/organization/page.tsx:131)§6d's fold-IN, not a link-OUT

The last row is the sharp one: the card built for the collapsed state is the thing that advertises the surface the collapsed state is supposed to hide. Its own comment cites §6d while doing the opposite of it — "the config editors are owned by their own design areas and are not redrawn here" is the under-specified half that shipped.

Arm B — an org invite strands the invitee, and their only exit makes it worse

organizationsService.addMemberByEmailaddMember (lib/services/organizationsService.ts:335) creates an OrganizationMembership and nothing else. Verified, not read off the code: the shipped assertion is tests/organizations-service.test.ts:191"adding a user to the ORG creates NO workspace membership" — which asserts workspaceMembership is null and resolveWorkspaceAccess(...) === null.

So in a one-workspace org the invitee signs in and:

  1. has no workspace membership → getWorkspaceContext() is null → no project, no board, no items;
  2. sees no workspace affordance in the header — ShellTierNav.tsx:103 hides the switcher at length < 2, so WorkspaceSwitcher.tsx:78's own zero-workspace "Create workspace" branch is unreachable dead code in exactly the state it was written for;
  3. is shown to the admin as "No workspaces" (OrgMembersClient.tsx:496) with no add-to-workspace action anywhere on the org roster;
  4. has exactly one exit — OrgControl's "New workspace" — which creates a second workspace, flipping the org out of the collapsed state and splitting the team across two workspaces. The one available escape is the wrong outcome.

Fix direction

  • Give §5 a count-1 arm. In addMember, inside the existing withOrgContext transaction: when the org has exactly one workspace, also create that WorkspaceMembership (role member). At ≥ 2 the asymmetry stands unchanged — the billing-admin-in-zero-workspaces carve-out is only meaningful once a choice exists, which is the same predicate §6 already uses. Amend organization-tier.md §5 + §6 to state the arm, and re-point tests/organizations-service.test.ts:191 at a two-workspace org so it keeps asserting the rule it was written for.
  • Hide the surface at ≤ 1 (§6d): drop the three navigation entries, replace WorkspaceConfigCard's link-out with the folded-in Name / Members / Danger-zone sections on /settings/organization, and make /settings/workspace itself notFound() at count 1 so it is not link-reachable either. At ≥ 2 the area returns exactly as it is today.
  • /settings/workspace/github and /settings/workspace/jobs are workspace-scoped but not workspace-named; §6 is about revealing a tier, not relocating every page under it. They stay.

This is one root cause and two subtasks — the service arm (~3 pts) and the surface arm (~5 pts). The sizing here is the whole defect; do not run it as one card.

Acceptance criteria

  1. In an org with one workspace, organizationsService.addMember also creates the invitee's WorkspaceMembership for that sole workspace, in the same transaction, and resolveWorkspaceAccess(invitee, thatWorkspace) is non-null.
  2. In an org with two or more workspaces, addMember still creates no workspace membership (§5 unchanged) — asserted by the re-pointed tests/organizations-service.test.ts case.
  3. docs/decisions/organization-tier.md §5 and §6 both state the count-1 arm, so the two clauses no longer contradict at the boundary.
  4. At workspaceCount <= 1, no rendered markup names /settings/workspace: not in UserMenu, not in AppCommandPalette, not in SidebarNav's settings door, not in WorkspaceConfigCard. (The row is absent, not disabled — the entry-point rule SidebarNav already states.)
  5. At workspaceCount <= 1, /settings/workspace answers 404, and the Name / Members / Delete sections are reachable from /settings/organization instead.
  6. At workspaceCount >= 2, every entry point and /settings/workspace render exactly as they do today.

Context refs

  • docs/decisions/organization-tier.md §5, §6
  • lib/services/organizationsService.ts:335 (addMember), :376 (addMemberByEmail)
  • app/(authed)/_components/ShellTierNav.tsx:103, WorkspaceSwitcher.tsx:78, UserMenu.tsx:83, AppCommandPalette.tsx:305, SidebarNav.tsx:386
  • app/(authed)/settings/organization/page.tsx:131, _components/WorkspaceConfigCard.tsx:44
  • app/(authed)/settings/workspace/page.tsx, _components/DangerZoneCard.tsx
  • app/(authed)/settings/organization/members/_components/OrgMembersClient.tsx:496
  • tests/organizations-service.test.ts:191
  • ⚠️ AC 5 adds a notFound() under app/(authed) — the no-loading.tsx-above-an-existence-deciding-route rule in CLAUDE.md applies.

Resolution

(open)

Status
Done
Type
Bug