Skip to content

Commit ba81b12

Browse files
committed
fix(webapp): make the new-organization form submit work on the first click
The form submitted through the client-side router, which receives the action's redirect as a bodiless response it has to interpret itself. If that redirect was lost on the way, the router committed empty action data and left the user sitting on /orgs/new with the typed name still in the field and no error shown — while the organization had in fact been created, so clicking Create again made a second one. Submit the form as a document POST instead. The server then returns an ordinary redirect and the browser follows it, so completing the form no longer depends on the client interpreting the response. A genuine failure is rendered server-side, keeping the submitted values and showing the error message. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6449a64 commit ba81b12

2 files changed

Lines changed: 29 additions & 7 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Creating an organization sometimes left you on the form with the name still filled in and no error shown, even though the organization had already been created, so clicking Create again made a duplicate. Creating an organization now reliably takes you to it, and if it genuinely fails you get an error message instead of silence.

apps/webapp/app/routes/_app.orgs.new/route.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { GlobeLinesIcon } from "~/assets/icons/GlobeLinesIcon";
33
import { parseWithZod } from "@conform-to/zod";
44
import { BuildingOffice2Icon } from "@heroicons/react/20/solid";
55
import { RadioGroup } from "@radix-ui/react-radio-group";
6-
import { json, redirect, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/node";
6+
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
77
import { Form, useActionData, useNavigation } from "@remix-run/react";
88
import { useState } from "react";
99
import { typedjson, useTypedLoaderData } from "remix-typedjson";
@@ -24,6 +24,7 @@ import { useFaviconUrl } from "~/hooks/useFaviconUrl";
2424
import { useFeatures } from "~/hooks/useFeatures";
2525
import { createOrganization } from "~/models/organization.server";
2626
import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server";
27+
import { logger } from "~/services/logger.server";
2728
import { requireUser, requireUserId } from "~/services/session.server";
2829
import { extractDomain, faviconUrl } from "~/utils/favicon";
2930
import { organizationPath, rootPath } from "~/utils/pathBuilder";
@@ -47,7 +48,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
4748
});
4849
};
4950

50-
export const action: ActionFunction = async ({ request }) => {
51+
export const action = async ({ request }: ActionFunctionArgs) => {
5152
const user = await requireUser(request);
5253
const formData = await request.formData();
5354
const submission = parseWithZod(formData, { schema });
@@ -106,14 +107,26 @@ export const action: ActionFunction = async ({ request }) => {
106107
}
107108

108109
return redirect(organizationPath(organization));
109-
} catch (error: any) {
110-
return json({ errors: { body: error.message } }, { status: 400 });
110+
} catch (error) {
111+
logger.error("Failed to create organization", {
112+
userId: user.id,
113+
error: error instanceof Error ? error.message : error,
114+
});
115+
116+
return json(
117+
submission.reply({
118+
formErrors: [
119+
"We couldn't create your organization. Check your organization list before trying again, and if this problem persists please contact support.",
120+
],
121+
}),
122+
{ status: 400 }
123+
);
111124
}
112125
};
113126

114127
export default function NewOrganizationPage() {
115128
const { hasOrganizations } = useTypedLoaderData<typeof loader>();
116-
const lastSubmission = useActionData();
129+
const lastSubmission = useActionData<typeof action>();
117130
const { isManagedCloud } = useFeatures();
118131
const navigation = useNavigation();
119132
const [companyUrl, setCompanyUrl] = useState("");
@@ -122,7 +135,7 @@ export default function NewOrganizationPage() {
122135

123136
const [form, { orgName }] = useForm({
124137
id: "create-organization",
125-
lastResult: lastSubmission as any,
138+
lastResult: lastSubmission,
126139
onValidate({ formData }) {
127140
return parseWithZod(formData, { schema });
128141
},
@@ -158,7 +171,8 @@ export default function NewOrganizationPage() {
158171
LeadingIcon={<BuildingOffice2Icon className="size-6 text-fuchsia-600" />}
159172
title="Create an Organization"
160173
/>
161-
<Form method="post" {...getFormProps(form)}>
174+
{/* Submit as a document POST so the browser follows the redirect itself */}
175+
<Form method="post" reloadDocument {...getFormProps(form)}>
162176
<Fieldset>
163177
<InputGroup>
164178
<Label htmlFor={orgName.id}>Organization name *</Label>
@@ -228,6 +242,8 @@ export default function NewOrganizationPage() {
228242
</>
229243
)}
230244

245+
<FormError id={form.errorId}>{form.errors}</FormError>
246+
231247
<FormButtons
232248
confirmButton={
233249
<Button type="submit" variant={"primary/small"} isLoading={isLoading}>

0 commit comments

Comments
 (0)