Skip to content

Commit 3e44329

Browse files
committed
Fix onboarding UI jank: no 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'. Almost no new user has a pending invitation, so we now show NO loading placeholder at all: the create form renders immediately and stays put, and invitations (when they exist) just appear above it once the fetch resolves. Removed the InvitationsSkeleton entirely. 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 3e44329

2 files changed

Lines changed: 18 additions & 37 deletions

File tree

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

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import * as Exit from "effect/Exit";
66
import { authWriteKeys } from "@executor-js/react/api/reactivity-keys";
77
import { trackEvent } from "@executor-js/react/api/analytics";
88
import { Button } from "@executor-js/react/components/button";
9-
import { Skeleton } from "@executor-js/react/components/skeleton";
109

1110
import { AUTH_PATHS } from "../../auth/api";
1211
import { acceptInvitation, pendingInvitationsAtom, useAuth } from "../auth";
@@ -84,8 +83,11 @@ export const CreateOrgPage = () => {
8483
},
8584
});
8685

87-
const isLoading =
88-
AsyncResult.isInitial(invitationsResult) || AsyncResult.isWaiting(invitationsResult);
86+
// Almost no new user has a pending invitation, so we never show a loading
87+
// placeholder for them — the create form renders immediately and stays put.
88+
// Invitations (when they exist) just appear above it once the fetch resolves;
89+
// a refetch (the create-org submit invalidates the auth-keyed atom) keeps the
90+
// last value, so nothing flickers.
8991
const invitations = AsyncResult.match(invitationsResult, {
9092
onInitial: () => [] as readonly PendingInvitation[],
9193
onFailure: () => [] as readonly PendingInvitation[],
@@ -103,23 +105,17 @@ export const CreateOrgPage = () => {
103105
Step 1 of 2
104106
</p>
105107
<h1 className="font-serif text-3xl">
106-
{isLoading
107-
? "Loading"
108-
: count === 0
109-
? "Create your organization"
110-
: "You've been invited"}
108+
{count > 0 ? "You've been invited" : "Create your organization"}
111109
</h1>
112-
{!isLoading && count === 0 && (
110+
{count === 0 && (
113111
<p className="text-sm text-muted-foreground">
114112
Organizations group your sources, secrets, and teammates. You can invite others once
115113
it's set up.
116114
</p>
117115
)}
118116
</header>
119117

120-
{isLoading && <InvitationsSkeleton />}
121-
122-
{!isLoading && sole && (
118+
{sole && (
123119
<SingleInvitationView
124120
invitation={sole}
125121
accepting={acceptingId === sole.id}
@@ -128,7 +124,7 @@ export const CreateOrgPage = () => {
128124
/>
129125
)}
130126

131-
{!isLoading && count > 1 && (
127+
{count > 1 && (
132128
<MultiInvitationsView
133129
invitations={invitations}
134130
acceptingId={acceptingId}
@@ -137,9 +133,7 @@ export const CreateOrgPage = () => {
137133
/>
138134
)}
139135

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

144138
<footer className="flex items-center justify-center">
145139
{/* oxlint-disable-next-line react/forbid-elements */}
@@ -161,25 +155,6 @@ export const CreateOrgPage = () => {
161155

162156
// ---------------------------------------------------------------------------
163157

164-
const InvitationsSkeleton = () => (
165-
<div className="flex flex-col gap-2.5">
166-
<InvitationRowSkeleton />
167-
<InvitationRowSkeleton />
168-
</div>
169-
);
170-
171-
const InvitationRowSkeleton = () => (
172-
<div className="flex items-center gap-3 rounded-md border border-border px-3 py-2.5">
173-
<div className="flex flex-1 flex-col gap-1.5">
174-
<Skeleton className="h-3.5 w-2/5" />
175-
<Skeleton className="h-3 w-3/5" />
176-
</div>
177-
<Skeleton className="h-8 w-16 rounded-md" />
178-
</div>
179-
);
180-
181-
// ---------------------------------------------------------------------------
182-
183158
const InviterAttribution = ({ invitation }: { invitation: PendingInvitation }) => {
184159
const inviterLabel = invitation.inviter
185160
? invitation.inviter.name && invitation.inviter.name.length > 0

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)