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
9 changes: 9 additions & 0 deletions .changeset/zod-4-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
"trigger.dev": patch
"@trigger.dev/redis-worker": patch
"@trigger.dev/schema-to-json": patch
---

Add zod v4 compatibility. The `zod` peer dependency is widened to `^3.25.0 || ^4.0.0`, so projects can use zod 3.25+ or zod 4. Internal code was updated for zod v4 API changes (`ZodError.errors` → `.issues`, single-arg `z.record` → keyed, unified `error` option, `z.ZodSchema`/`z.AnyZodObject` → `z.ZodType`/`z.ZodObject`, `z.any()` object fields made `.optional()` to preserve v3 inference). No runtime behavior change for existing zod 3 users.
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.
2 changes: 1 addition & 1 deletion apps/supervisor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"prom-client": "^15.1.0",
"socket.io": "4.7.4",
"std-env": "^3.8.0",
"zod": "3.25.76"
"zod": "4.4.3"
},
"devDependencies": {
"@internal/testcontainers": "workspace:*",
Expand Down
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/v4";
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">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Hidden form fields lose their explicit values, causing feedback submissions to redirect to an empty path

The page path for the feedback form is placed before the spread of auto-generated props (getInputProps(fields.path, { type: "hidden" }) at apps/webapp/app/components/Feedback.tsx:95), so the library's own value silently overrides it.

Impact: Feedback submissions will send an empty or stale path field instead of the current page URL, breaking the post-submit redirect.

Conform v1 getInputProps returns a value prop for hidden inputs that overrides the explicit one

In conform v0.9, conform.input(field, { type: "hidden" }) returned defaultValue, which doesn't conflict with an explicit value prop. In conform v1, getInputProps(field, { type: "hidden" }) returns a value prop (the field's current form-state value).

When the JSX is <input value={location.pathname} {...getInputProps(fields.path, { type: "hidden" })} />, the spread comes second and its value overwrites the explicit value={location.pathname}. Since no defaultValue is set in useForm for the path field, the form submits path="" instead of path="/current/page".

The correct pattern in conform v1 is shown in apps/webapp/app/routes/confirm-basic-details.tsx:315:

<input {...getInputProps(confirmEmail, { type: "hidden", value: false })} value={user.email} />

— either pass { value: false } to disable auto-value, or put the explicit value after the spread.

Suggested change
<InputGroup className="max-w-full">
<input {...getInputProps(fields.path, { type: "hidden", value: false })} value={location.pathname} />
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

{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
29 changes: 15 additions & 14 deletions apps/webapp/app/components/errors/ConfigureErrorAlerts.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 } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod/v4";
import {
EnvelopeIcon,
GlobeAltIcon,
Expand Down Expand Up @@ -104,20 +104,21 @@ export function ConfigureErrorAlerts({
existingWebhooks.length > 0 ? [...existingWebhooks.map((w) => w.url), ""] : [""]
);

const [form, { emails, webhooks, slackChannel, slackIntegrationId }] = useForm({
const [form, fields] = useForm<z.infer<typeof ErrorAlertsFormSchema>>({
id: "configure-error-alerts",
onValidate({ formData }) {
return parse(formData, { schema: ErrorAlertsFormSchema });
return parseWithZod(formData, { schema: ErrorAlertsFormSchema });
},
shouldRevalidate: "onSubmit",
defaultValue: {
emails: emailFieldValues.current,
webhooks: webhookFieldValues.current,
},
});
const { emails, webhooks, slackChannel, slackIntegrationId } = fields;

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

return (
<div className="grid h-full grid-rows-[auto_1fr_auto] overflow-hidden">
Expand All @@ -138,7 +139,7 @@ export function ConfigureErrorAlerts({
<fetcher.Form
method="post"
action={formAction}
{...form.props}
{...getFormProps(form)}
className="contents"
>
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
Expand All @@ -160,7 +161,7 @@ export function ConfigureErrorAlerts({
{emailFields.map((emailField, index) => (
<Fragment key={emailField.key}>
<Input
{...conform.input(emailField, { type: "email" })}
{...getInputProps(emailField, { type: "email" })}
placeholder={index === 0 ? "Enter an email address" : "Add another email"}
icon={EnvelopeIcon}
onChange={(e) => {
Expand All @@ -169,11 +170,11 @@ export function ConfigureErrorAlerts({
emailFields.length === emailFieldValues.current.length &&
emailFieldValues.current.every((v) => v !== "")
) {
requestIntent(form.ref.current ?? undefined, list.append(emails.name));
form.insert({ name: emails.name });
}
}}
/>
<FormError id={emailField.errorId}>{emailField.error}</FormError>
<FormError id={emailField.errorId}>{emailField.errors}</FormError>
</Fragment>
))}
</InputGroup>
Expand Down Expand Up @@ -320,7 +321,7 @@ export function ConfigureErrorAlerts({
{webhookFields.map((webhookField, index) => (
<Fragment key={webhookField.key}>
<Input
{...conform.input(webhookField, { type: "url" })}
{...getInputProps(webhookField, { type: "url" })}
placeholder={
index === 0 ? "https://example.com/webhook" : "Add another webhook URL"
}
Expand All @@ -331,18 +332,18 @@ export function ConfigureErrorAlerts({
webhookFields.length === webhookFieldValues.current.length &&
webhookFieldValues.current.every((v) => v !== "")
) {
requestIntent(form.ref.current ?? undefined, list.append(webhooks.name));
form.insert({ name: webhooks.name });
}
}}
/>
<FormError id={webhookField.errorId}>{webhookField.error}</FormError>
<FormError id={webhookField.errorId}>{webhookField.errors}</FormError>
</Fragment>
))}
<Hint>We'll issue POST requests to these URLs with a JSON payload.</Hint>
</InputGroup>
</div>

<FormError>{form.error}</FormError>
<FormError>{form.errors}</FormError>
</Fieldset>
</div>

Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/metrics/QueryWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const chartConfigOptions = {
sortByColumn: z.string().nullable(),
sortDirection: SortDirection,
aggregation: AggregationType,
seriesColors: z.record(z.string()).optional(),
seriesColors: z.record(z.string(), z.string()).optional(),
};

const ChartConfiguration = z.object({ ...chartConfigOptions });
Expand Down
Loading