From 52feba2114276e1818d4a9a7ecf0e4a5297e1e61 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 12:51:08 +0000 Subject: [PATCH] fix(webapp): hard-navigate after creating an organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once the organization row was committed, the action returned a client-side redirect and the browser navigated on through a chain of redirects to the new organization's first page. That navigation could be aborted while the destination route's code was still loading, and the browser then went back to /orgs/new — leaving the user sitting on the creation form with what they typed still in place, even though the organization had in fact been created. Clicking Create again made a duplicate. The success redirects now use redirectDocument(), so the browser performs a hard navigation to the destination instead of client-side routing into its chunks, which is the window that broke. Remix still handles the submission itself, so the zod validation and the pending/disabled state on the Create button behave exactly as before. The failure path also returns a conform-shaped result and logs the underlying error, so a genuine failure renders its message with the submitted values preserved instead of resetting silently. Co-Authored-By: Claude --- .../fix-new-organization-form-submit.md | 6 +++ .../webapp/app/routes/_app.orgs.new/route.tsx | 38 +++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 .server-changes/fix-new-organization-form-submit.md diff --git a/.server-changes/fix-new-organization-form-submit.md b/.server-changes/fix-new-organization-form-submit.md new file mode 100644 index 0000000000..52979154be --- /dev/null +++ b/.server-changes/fix-new-organization-form-submit.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Creating an organization sometimes left you back on the creation form even though the organization had already been created, so clicking Create again made a duplicate. Creating an organization now completes and takes you to your new organization. diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index f87d398159..c13c7d1175 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -3,7 +3,12 @@ import { GlobeLinesIcon } from "~/assets/icons/GlobeLinesIcon"; import { parseWithZod } from "@conform-to/zod"; import { BuildingOffice2Icon } from "@heroicons/react/20/solid"; import { RadioGroup } from "@radix-ui/react-radio-group"; -import { json, redirect, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/node"; +import { + json, + redirectDocument, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from "@remix-run/node"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -24,6 +29,7 @@ import { useFaviconUrl } from "~/hooks/useFaviconUrl"; import { useFeatures } from "~/hooks/useFeatures"; import { createOrganization } from "~/models/organization.server"; import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server"; +import { logger } from "~/services/logger.server"; import { requireUser, requireUserId } from "~/services/session.server"; import { extractDomain, faviconUrl } from "~/utils/favicon"; import { organizationPath, rootPath } from "~/utils/pathBuilder"; @@ -47,7 +53,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { }); }; -export const action: ActionFunction = async ({ request }) => { +export const action = async ({ request }: ActionFunctionArgs) => { const user = await requireUser(request); const formData = await request.formData(); const submission = parseWithZod(formData, { schema }); @@ -102,18 +108,32 @@ export const action: ActionFunction = async ({ request }) => { if (next) { params.set("next", next); } - return redirect(`${organizationPath(organization)}/projects/new?${params.toString()}`); + return redirectDocument( + `${organizationPath(organization)}/projects/new?${params.toString()}` + ); } - return redirect(organizationPath(organization)); - } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return redirectDocument(organizationPath(organization)); + } catch (error) { + logger.error("Failed to create organization", { + userId: user.id, + error: error instanceof Error ? error.message : error, + }); + + return json( + submission.reply({ + formErrors: [ + "We couldn't create your organization. Check your organization list before trying again, and if this problem persists please contact support.", + ], + }), + { status: 400 } + ); } }; export default function NewOrganizationPage() { const { hasOrganizations } = useTypedLoaderData(); - const lastSubmission = useActionData(); + const lastSubmission = useActionData(); const { isManagedCloud } = useFeatures(); const navigation = useNavigation(); const [companyUrl, setCompanyUrl] = useState(""); @@ -122,7 +142,7 @@ export default function NewOrganizationPage() { const [form, { orgName }] = useForm({ id: "create-organization", - lastResult: lastSubmission as any, + lastResult: lastSubmission, onValidate({ formData }) { return parseWithZod(formData, { schema }); }, @@ -228,6 +248,8 @@ export default function NewOrganizationPage() { )} + {form.errors} +