Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/conform-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Upgrade the dashboard form layer from `@conform-to` 0.9 to 1.x. conform 1.x supports both zod 3 and zod 4, which unblocks the upcoming zod 4 upgrade.
27 changes: 13 additions & 14 deletions apps/webapp/app/components/Feedback.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { getFormProps, getSelectProps, getInputProps, getTextareaProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { InformationCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid";
import { EnvelopeIcon, ShieldCheckIcon } from "@heroicons/react/24/solid";
import { Form, useActionData, useLocation, useNavigation, useSearchParams } from "@remix-run/react";
Expand Down Expand Up @@ -34,11 +34,11 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac
const navigation = useNavigation();
const [type, setType] = useState<FeedbackType>(defaultValue);

const [form, { path, feedbackType, message }] = useForm({
const [form, fields] = useForm({
id: "accept-invite",
lastSubmission: lastSubmission as any,
lastResult: lastSubmission as any,
onValidate({ formData }) {
return parse(formData, { schema });
return parseWithZod(formData, { schema });
},
shouldRevalidate: "onInput",
});
Expand All @@ -47,8 +47,7 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac
if (
navigation.formAction === "/resources/feedback" &&
navigation.state === "loading" &&
form.error === undefined &&
form.errors.length === 0
(form.errors === undefined || form.errors.length === 0)
) {
setOpen(false);
}
Expand Down Expand Up @@ -90,9 +89,9 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac
type === "concurrency" ||
type === "hipaa"
) && <hr className="border-grid-dimmed" />}
<Form method="post" action="/resources/feedback" {...form.props} className="w-full">
<Form method="post" action="/resources/feedback" {...getFormProps(form)} className="w-full">
<Fieldset className="max-w-full gap-y-3">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<input value={location.pathname} {...getInputProps(fields.path, { type: "hidden" })} />
<InputGroup className="max-w-full">
Comment thread
carderne marked this conversation as resolved.
{type === "feature" && (
<InfoPanel
Expand Down Expand Up @@ -149,7 +148,7 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac
</InfoPanel>
)}
<Select
{...conform.select(feedbackType)}
{...getSelectProps(fields.feedbackType)}
variant="tertiary/medium"
value={type}
defaultValue={type}
Expand All @@ -164,14 +163,14 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac
</SelectItem>
))}
</Select>
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
<FormError id={fields.feedbackType.errorId}>{fields.feedbackType.errors}</FormError>
</InputGroup>
<InputGroup className="max-w-full">
<Label>Message</Label>
<TextArea {...conform.textarea(message)} />
<FormError id={message.errorId}>{message.error}</FormError>
<TextArea {...getTextareaProps(fields.message)} />
<FormError id={fields.message.errorId}>{fields.message.errors}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<FormError>{form.errors}</FormError>
<FormButtons
confirmButton={
<Button type="submit" variant="primary/medium">
Expand Down
24 changes: 12 additions & 12 deletions apps/webapp/app/components/billing/BillingAlertsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { conform, list, requestIntent, useFieldList, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { getFormProps, getInputProps, useForm, type SubmissionResult } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { PlusIcon, TrashIcon } from "@heroicons/react/20/solid";
import { Form, useActionData, useSearchParams } from "@remix-run/react";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
Expand Down Expand Up @@ -62,7 +62,7 @@ export type { BillingAlertsFormData } from "~/components/billing/billingAlertsFo

type BillingAlertsActionData = {
formIntent: "billing-alerts";
submission: ReturnType<typeof parse<typeof billingAlertsSchema>>;
submission: SubmissionResult;
};

type BillingAlertsSectionProps = {
Expand Down Expand Up @@ -168,20 +168,20 @@ export function BillingAlertsSection({
!emailsMatchSaved(emailValues, savedEmails);
const lastSubmission = isDirty ? alertsSubmission : undefined;

const [form, { emails, alertLevels }] = useForm({
const [form, { emails, alertLevels }] = useForm<z.infer<typeof billingAlertsSchema>>({
id: "billing-alerts",
lastSubmission: lastSubmission as any,
lastResult: lastSubmission as any,
shouldRevalidate: "onInput",
onValidate({ formData }) {
return parse(formData, { schema: billingAlertsSchema });
return parseWithZod(formData, { schema: billingAlertsSchema });
},
defaultValue: {
emails: emailValues,
alertLevels: savedThresholds.map(String),
},
});

const emailFields = useFieldList(form.ref, emails);
const emailFields = emails.getFieldList();

useEffect(() => {
nextThresholdIdRef.current = savedThresholds.length;
Expand Down Expand Up @@ -222,7 +222,7 @@ export function BillingAlertsSection({
<TextLink to={docsPath("how-to-reduce-your-spend")}>reduce your compute spend</TextLink>.
</Paragraph>
</div>
<Form method="post" {...form.props}>
<Form method="post" {...getFormProps(form)}>
<input type="hidden" name="intent" value="billing-alerts" />
<Fieldset>
<AnimatedCallout
Expand Down Expand Up @@ -322,7 +322,7 @@ export function BillingAlertsSection({
})}
</div>

<FormError id={alertLevels.errorId}>{alertLevels.error}</FormError>
<FormError id={alertLevels.errorId}>{alertLevels.errors}</FormError>

{canAddThreshold && (
<Button
Expand All @@ -340,7 +340,7 @@ export function BillingAlertsSection({
<InputGroup fullWidth className="mt-4">
<Label htmlFor={emails.id}>Email addresses</Label>
{emailFields.map((email, index) => {
const { defaultValue: _emailDefaultValue, ...emailInputProps } = conform.input(
const { defaultValue: _emailDefaultValue, ...emailInputProps } = getInputProps(
email,
{ type: "email" }
);
Expand All @@ -360,15 +360,15 @@ export function BillingAlertsSection({
emailFields.length === next.length &&
next.every((value) => value !== "")
) {
requestIntent(form.ref.current ?? undefined, list.append(emails.name));
form.insert({ name: emails.name });
return [...next, ""];
}
return next;
});
}}
fullWidth
/>
<FormError id={email.errorId}>{email.error}</FormError>
<FormError id={email.errorId}>{email.errors}</FormError>
</Fragment>
);
})}
Expand Down
19 changes: 10 additions & 9 deletions apps/webapp/app/components/billing/BillingLimitConfigSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { conform, useForm, type Submission } from "@conform-to/react";
import { getFormProps, useForm, type SubmissionResult } from "@conform-to/react";

import { parse } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod";
import { Form, useActionData } from "@remix-run/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { z } from "zod";
Expand Down Expand Up @@ -49,7 +49,7 @@ type BillingLimitFormValue = z.infer<typeof billingLimitFormSchema>;

type BillingLimitActionData = {
formIntent: "billing-limit";
submission: Submission<BillingLimitFormValue>;
submission: SubmissionResult;
};

export function isBillingLimitFormDirty(input: {
Expand Down Expand Up @@ -118,6 +118,7 @@ export function BillingLimitConfigSection({
const [customAmount, setCustomAmount] = useState(savedCustomAmount);
const [cancelInProgressRuns, setCancelInProgressRuns] = useState(savedCancelInProgressRuns);
const customAmountInputRef = useRef<HTMLInputElement>(null);
const formRef = useRef<HTMLFormElement>(null);

useEffect(() => {
setMode(savedMode);
Expand Down Expand Up @@ -156,19 +157,19 @@ export function BillingLimitConfigSection({

const [form, fields] = useForm({
id: "billing-limit",
lastSubmission: lastSubmission as any,
lastResult: lastSubmission as any,
shouldRevalidate: "onInput",
onValidate({ formData }) {
return parse(formData, { schema: billingLimitFormSchema });
return parseWithZod(formData, { schema: billingLimitFormSchema });
},
defaultValue: {
mode: savedMode,
},
});

useEffect(() => {
form.ref.current?.dispatchEvent(new Event("input", { bubbles: true }));
}, [customAmount, form.ref, mode]);
formRef.current?.dispatchEvent(new Event("input", { bubbles: true }));
}, [customAmount, mode]);

const planLimitLabel = formatCurrency(planLimitCents / 100, false);
const showPlanInfoCallout = mode === "plan";
Expand All @@ -185,7 +186,7 @@ export function BillingLimitConfigSection({
</Paragraph>
</div>

<Form method="post" {...form.props}>
<Form method="post" {...getFormProps(form)} ref={formRef}>
<input type="hidden" name="intent" value="billing-limit" />
<Fieldset>
<input type="hidden" name="mode" value={mode} />
Expand Down Expand Up @@ -245,7 +246,7 @@ export function BillingLimitConfigSection({
fullWidth
/>
{fields.amount && (
<FormError id={fields.amount.errorId}>{fields.amount.error}</FormError>
<FormError id={fields.amount.errorId}>{fields.amount.errors}</FormError>
)}
</InputGroup>
)}
Expand Down
21 changes: 11 additions & 10 deletions apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { getFormProps, useForm, type SubmissionResult } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { z } from "zod";
Expand Down Expand Up @@ -38,7 +38,7 @@ export const billingLimitRecoveryFormSchema = z

type BillingLimitRecoveryActionData = {
formIntent: "billing-limit-resolve";
submission: ReturnType<typeof parse<typeof billingLimitRecoveryFormSchema>>;
submission: SubmissionResult;
};

export function BillingLimitRecoveryPanel({
Expand All @@ -60,6 +60,7 @@ export function BillingLimitRecoveryPanel({
const [newAmount, setNewAmount] = useState(String(suggestedNewLimitDollars));
const [resumeMode, setResumeMode] = useState<"queue" | "new_only">("queue");
const newAmountInputRef = useRef<HTMLInputElement>(null);
const formRef = useRef<HTMLFormElement>(null);

useEffect(() => {
setNewAmount(String(suggestedNewLimitDollars));
Expand All @@ -79,10 +80,10 @@ export function BillingLimitRecoveryPanel({

const [form, fields] = useForm({
id: "billing-limit-resolve",
lastSubmission: recoverySubmission as any,
lastResult: recoverySubmission as any,
shouldRevalidate: "onInput",
onValidate({ formData }) {
return parse(formData, { schema: billingLimitRecoveryFormSchema });
return parseWithZod(formData, { schema: billingLimitRecoveryFormSchema });
},
defaultValue: {
action: "increase",
Expand All @@ -92,8 +93,8 @@ export function BillingLimitRecoveryPanel({
});

useEffect(() => {
form.ref.current?.dispatchEvent(new Event("input", { bubbles: true }));
}, [action, form.ref, newAmount, resumeMode]);
formRef.current?.dispatchEvent(new Event("input", { bubbles: true }));
}, [action, newAmount, resumeMode]);

const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
Expand Down Expand Up @@ -142,7 +143,7 @@ export function BillingLimitRecoveryPanel({
)}
</div>

<Form method="post" {...form.props}>
<Form method="post" {...getFormProps(form)} ref={formRef}>
<input type="hidden" name="intent" value="billing-limit-resolve" />
<input type="hidden" name="action" value={action} />
<input type="hidden" name="resumeMode" value={resumeMode} />
Expand Down Expand Up @@ -185,8 +186,8 @@ export function BillingLimitRecoveryPanel({
/>
</InputGroup>
)}
{action === "increase" && fields.newAmount?.error && (
<FormError id={fields.newAmount?.errorId}>{fields.newAmount.error}</FormError>
{action === "increase" && fields.newAmount?.errors && (
<FormError id={fields.newAmount?.errorId}>{fields.newAmount.errors}</FormError>
)}
</div>

Expand Down
Loading
Loading