diff --git a/.server-changes/fix-new-organization-form-error.md b/.server-changes/fix-new-organization-form-error.md new file mode 100644 index 0000000000..628ca4aaf7 --- /dev/null +++ b/.server-changes/fix-new-organization-form-error.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +When creating an organization failed, the form quietly cleared the name you typed and showed nothing, so it was easy to submit again and end up with a duplicate. The name is now kept and an error message explains what happened. diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index f87d398159..cae9dcbde3 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -3,7 +3,7 @@ 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, redirect, 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 +24,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 +48,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 }); @@ -106,14 +107,26 @@ export const action: ActionFunction = async ({ request }) => { } return redirect(organizationPath(organization)); - } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + } 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 +135,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 +241,8 @@ export default function NewOrganizationPage() { )} + {form.errors} +