mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 21:59:38 +01:00
fix(onboarding): advance past the organization step when the app adopts the created company
Applies the OnboardingWizard fix from #12667 to the 2026.831.0 stable line. On the first onboarding screen ("What is the name of your organization?"), clicking Continue created the organization but the guard `stillTheSameCompany(null)` treated the surrounding app adopting the just-created company (via the company-list refresh) as a takeover and returned before advancing. The wizard stayed on the name step while the organization was created — and every retry created another duplicate. Recognize the adopt-the-POST-result transition as the same successful company and advance to the first-agent step; a genuine switch to a different organization still stops with a clear error. Includes the regression test, which fails on the unpatched wizard.
This commit is contained in:
1 parent
dbf052577d
commit
65ec059bde
3 files changed
+81
-2
No files matched your search
@@ -0,0 +1,13 @@
|
||||
# Paperclip v2026.831.1
|
||||
|
||||
> Released: 2026-09-02
|
||||
|
||||
A patch on top of [2026.831.0](https://github.com/paperclipai/paperclip/releases/tag/v2026.831.0) that fixes first-run onboarding. It carries a single targeted change; everything else in 2026.831.0 is unchanged.
|
||||
|
||||
## Fixes
|
||||
|
||||
- **Onboarding no longer gets stuck on the organization step.** On the first onboarding screen ("What is the name of your organization?"), clicking **Continue** created the organization on the server but could leave the wizard sitting on the same step — so the page appeared to just refresh, and repeated clicks silently created duplicate organizations. The company-created list refresh can make the surrounding app adopt the just-created organization before the create request's continuation runs; the wizard now recognizes that as the same successful transition and advances to the first-agent step, while a genuine switch to a different organization still stops with a clear message. ([#12667](https://github.com/paperclipai/paperclip/pull/12667))
|
||||
|
||||
## Upgrade Guide
|
||||
|
||||
- No migrations, no configuration changes, no API changes. Upgrade in place.
|
||||
@@ -728,6 +728,50 @@ describe("OnboardingWizard — which step it lands on", () => {
|
||||
// here rather than read as a pass.
|
||||
expect(currentStep()).toBe("agent");
|
||||
expect(companyState.setSelectedCompanyId).not.toHaveBeenCalled();
|
||||
expect(document.body.textContent).toContain(
|
||||
"Organization created, but onboarding switched to another organization.",
|
||||
);
|
||||
});
|
||||
|
||||
it("finishes advancing when the returned company was already adopted", async () => {
|
||||
// The company-created live update can make the surrounding app adopt the
|
||||
// returned company before the POST continuation runs. That is not a
|
||||
// different-company takeover: both signals name the same company, so
|
||||
// dropping the continuation leaves the customer on the name step even
|
||||
// though the organization now exists.
|
||||
let resolveCreate: (company: { id: string; issuePrefix: string }) => void = () => {};
|
||||
mockCompaniesApi.create.mockReturnValue(
|
||||
new Promise<{ id: string; issuePrefix: string }>((resolve) => {
|
||||
resolveCreate = resolve;
|
||||
}),
|
||||
);
|
||||
routerState.pathname = "/onboarding";
|
||||
await render();
|
||||
await settle();
|
||||
|
||||
const nameInput = document.body.querySelector("input")! as HTMLInputElement;
|
||||
setControlledValue(nameInput, "Initech");
|
||||
await settle();
|
||||
const next = [...document.body.querySelectorAll("button")].find(
|
||||
(button) => button.textContent?.trim() === "Continue",
|
||||
)!;
|
||||
await act(async () => {
|
||||
next.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
// Model the surrounding app adopting exactly the company that the pending
|
||||
// request is about, without choosing a new step on the wizard's behalf.
|
||||
dialogState.onboardingOpen = true;
|
||||
dialogState.onboardingOptions = { companyId: "company-created" };
|
||||
await rerender();
|
||||
await settle();
|
||||
|
||||
await act(async () => resolveCreate({ id: "company-created", issuePrefix: "INI" }));
|
||||
await settle();
|
||||
|
||||
expect(currentStep()).toBe("agent");
|
||||
expect(companyState.setSelectedCompanyId).toHaveBeenCalledWith("company-created");
|
||||
expect(mockCompaniesApi.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies the step again when the wizard is re-opened", async () => {
|
||||
|
||||
@@ -903,6 +903,26 @@ function OnboardingWizardInner({
|
||||
return createdCompanyIdRef.current === companyIdAtStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a just-created company can still be committed to this wizard.
|
||||
*
|
||||
* Company-list refreshes can make the surrounding app adopt the POST result
|
||||
* before the continuation runs. That is the same successful transition, not
|
||||
* a takeover. A different id still means navigation moved the wizard to a
|
||||
* different organization while the request was in flight.
|
||||
*/
|
||||
function canCommitCreatedCompany(
|
||||
companyIdAtStart: string | null,
|
||||
returnedCompanyId: string,
|
||||
) {
|
||||
const companyIdNow = createdCompanyIdRef.current;
|
||||
if (companyIdNow === companyIdAtStart || companyIdNow === returnedCompanyId) {
|
||||
return true;
|
||||
}
|
||||
setError("Organization created, but onboarding switched to another organization.");
|
||||
return false;
|
||||
}
|
||||
|
||||
async function handleLaunchToDashboard() {
|
||||
if (!createdCompanyId || !createdAgentId) {
|
||||
setError(INCOMPLETE_ONBOARDING_STATE_MESSAGE);
|
||||
@@ -1173,6 +1193,7 @@ function OnboardingWizardInner({
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const companyIdAtStart = createdCompanyIdRef.current;
|
||||
try {
|
||||
const company = await companiesApi.create({ name: companyName.trim() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
||||
@@ -1181,7 +1202,7 @@ function OnboardingWizardInner({
|
||||
// a company while the request was open has taken over the wizard, and
|
||||
// adopting the company just created would fight it — and would leave the
|
||||
// customer on a company they never navigated to.
|
||||
if (!stillTheSameCompany(null)) return;
|
||||
if (!canCommitCreatedCompany(companyIdAtStart, company.id)) return;
|
||||
setCreatedCompanyId(company.id);
|
||||
// Keep the mirror current here rather than waiting for the next render.
|
||||
// The goal write below asks `stillTheSameCompany(company.id)`, and a ref
|
||||
@@ -1237,6 +1258,7 @@ function OnboardingWizardInner({
|
||||
creatingCompanyRef.current = true;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const companyIdAtStart = createdCompanyIdRef.current;
|
||||
try {
|
||||
const company = await companiesApi.create({ name: companyName.trim() });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
||||
@@ -1245,7 +1267,7 @@ function OnboardingWizardInner({
|
||||
// taken over the wizard, and adopting the company just created would
|
||||
// fight it — and would leave the customer on a company they never
|
||||
// navigated to.
|
||||
if (!stillTheSameCompany(null)) return;
|
||||
if (!canCommitCreatedCompany(companyIdAtStart, company.id)) return;
|
||||
setCreatedCompanyId(company.id);
|
||||
// Keep the mirror current rather than waiting for the next render, for
|
||||
// the same reason the mission path does: anything downstream that asks
|
||||
|
||||
Reference in new issue
Block a user