Skip to content

Commit b0b86f9

Browse files
committed
Fix onboarding UI jank: no full-page Loading flash, no canonicalize double-hop
Two flashes visible in the onboarding flow: 1. create-org flickered 'Create your organization' → 'Loading' → back. The header was gated on the pending-invitations atom being initial OR waiting, and that atom is keyed on auth — so the create-org submit (which invalidates the auth keys) put it into 'waiting' and flipped the whole page to 'Loading'. Now the create form renders immediately; only the genuine first load shows a skeleton, and only in the invitations slot. A refetch is 'waiting', not 'initial', so it no longer replaces the page. 2. The connect-MCP step flashed the app sidebar. 'Continue to app' / 'Skip' navigated to the BARE /{-$orgSlug}, which mounts the shell at / and then OrgSlugGate fires a SECOND navigation to canonicalize / → /<slug>. That double hop is the window where the shell paints over the still-mounted onboarding page. Now it navigates straight to /<slug> (the page already has the org slug), so the shell mounts once. Both pre-existing; the new auth-routing flow test (#1006) made them visible.
1 parent 1f9bfe0 commit b0b86f9

2 files changed

Lines changed: 21 additions & 16 deletions

File tree

apps/cloud/src/web/pages/create-org.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,13 @@ export const CreateOrgPage = () => {
8484
},
8585
});
8686

87-
const isLoading =
88-
AsyncResult.isInitial(invitationsResult) || AsyncResult.isWaiting(invitationsResult);
87+
// Only the genuine FIRST load (no result yet) gets a placeholder, and only in
88+
// the invitations slot — never a full-page "Loading" header that replaces the
89+
// create form. A refetch (the create-org submit invalidates the auth-keyed
90+
// invitations atom) is `waiting`, NOT `initial`, so it no longer flips the
91+
// page to "Loading" and back. Invitations are empty for ~every new user, so
92+
// the create form is the right thing to show immediately.
93+
const isInitialLoad = AsyncResult.isInitial(invitationsResult);
8994
const invitations = AsyncResult.match(invitationsResult, {
9095
onInitial: () => [] as readonly PendingInvitation[],
9196
onFailure: () => [] as readonly PendingInvitation[],
@@ -103,23 +108,19 @@ export const CreateOrgPage = () => {
103108
Step 1 of 2
104109
</p>
105110
<h1 className="font-serif text-3xl">
106-
{isLoading
107-
? "Loading"
108-
: count === 0
109-
? "Create your organization"
110-
: "You've been invited"}
111+
{count > 0 ? "You've been invited" : "Create your organization"}
111112
</h1>
112-
{!isLoading && count === 0 && (
113+
{count === 0 && (
113114
<p className="text-sm text-muted-foreground">
114115
Organizations group your sources, secrets, and teammates. You can invite others once
115116
it's set up.
116117
</p>
117118
)}
118119
</header>
119120

120-
{isLoading && <InvitationsSkeleton />}
121+
{isInitialLoad && <InvitationsSkeleton />}
121122

122-
{!isLoading && sole && (
123+
{sole && (
123124
<SingleInvitationView
124125
invitation={sole}
125126
accepting={acceptingId === sole.id}
@@ -128,7 +129,7 @@ export const CreateOrgPage = () => {
128129
/>
129130
)}
130131

131-
{!isLoading && count > 1 && (
132+
{count > 1 && (
132133
<MultiInvitationsView
133134
invitations={invitations}
134135
acceptingId={acceptingId}
@@ -137,9 +138,7 @@ export const CreateOrgPage = () => {
137138
/>
138139
)}
139140

140-
{!isLoading && (count === 0 || sole || count > 1) && (
141-
<CreateOrgSection isPrimary={count === 0} form={form} />
142-
)}
141+
<CreateOrgSection isPrimary={count === 0} form={form} />
143142

144143
<footer className="flex items-center justify-center">
145144
{/* oxlint-disable-next-line react/forbid-elements */}

apps/cloud/src/web/pages/setup-mcp.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export const SetupMcpPage = () => {
2323
const auth = useAuth();
2424
const organizationSlug =
2525
auth.status === "authenticated" ? (auth.organization?.slug ?? null) : null;
26+
// Land DIRECTLY on the org's canonical URL. Navigating to the bare
27+
// `/{-$orgSlug}` would mount the shell at `/`, then OrgSlugGate would fire a
28+
// SECOND navigation to canonicalize `/` → `/<slug>` — that double hop is the
29+
// window where the shell paints over this still-mounted onboarding page.
30+
const goToApp = () =>
31+
navigate({ to: "/{-$orgSlug}", params: { orgSlug: organizationSlug ?? undefined } });
2632
const [origin, setOrigin] = useState<string | null>(null);
2733
const [advancedOpen, setAdvancedOpen] = useState(false);
2834
const [elicitationMode, setElicitationMode] = useState<McpElicitationMode>("model");
@@ -150,7 +156,7 @@ export const SetupMcpPage = () => {
150156
type="button"
151157
onClick={() => {
152158
trackEvent("setup_mcp_skipped");
153-
void navigate({ to: "/{-$orgSlug}" });
159+
void goToApp();
154160
}}
155161
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
156162
>
@@ -160,7 +166,7 @@ export const SetupMcpPage = () => {
160166
size="sm"
161167
onClick={() => {
162168
trackEvent("setup_mcp_completed");
163-
void navigate({ to: "/{-$orgSlug}" });
169+
void goToApp();
164170
}}
165171
>
166172
Continue to app

0 commit comments

Comments
 (0)