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", value: false })} />
<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
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