History

Fix onboarding UI jank: no Loading flash, no canonicalize double-hop (#1007)

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.

Rhys Sullivan committed 5 months ago

Commit 84ad4011 parent 1f9bfe02 files · +18 −37Browse files

Unified · Split

2 files
  1. apps/cloud/src/web/pages/create-org.tsx +10 −35
  2. apps/cloud/src/web/pages/setup-mcp.tsx +8 −2
apps/cloud/src/web/pages/create-org.tsx+10−35
@@ -6,7 +6,6 @@ import * as Exit from "effect/Exit"; · 5 unmodified lines
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 = () => { · 71 unmodified lines
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,13 +105,9 @@ export const CreateOrgPage = () => { · 11 unmodified lines
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.
@@ -117,9 +115,7 @@ export const CreateOrgPage = () => { · 1 unmodified line
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 = () => { · 2 unmodified lines
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 = () => { · 2 unmodified lines
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 = () => { · 15 unmodified lines
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+8−2
@@ -23,6 +23,12 @@ export const SetupMcpPage = () => { · 22 unmodified lines
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 = () => { · 121 unmodified lines
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 = () => { · 3 unmodified lines
160166 size="sm"
161167 onClick={() => {
162168 trackEvent("setup_mcp_completed");
163− void navigate({ to: "/{-$orgSlug}" });
169+ void goToApp();
164170 }}
165171 >
166172 Continue to app

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t