From 4b2978207b2198778a98f24deec9f21cc2bdc47a Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 30 Jun 2026 16:08:57 +0100 Subject: [PATCH 1/3] chore: upgrade to zod v4 --- .changeset/zod-4-support.md | 9 + .server-changes/conform-v1.md | 6 - apps/supervisor/package.json | 2 +- apps/webapp/app/components/Feedback.tsx | 26 +- .../errors/ConfigureErrorAlerts.tsx | 9 +- .../app/components/metrics/QueryWidget.tsx | 2 +- .../components/runs/v3/ReplayRunDialog.tsx | 2 +- .../schedules/PurchaseSchedulesModal.tsx | 2 +- .../app/models/orgIntegration.server.ts | 2 +- .../app/models/vercelIntegration.server.ts | 2 +- .../app/models/vercelSdkRecovery.server.ts | 4 +- .../v3/MetricDashboardPresenter.server.ts | 2 +- .../app/presenters/v3/SpanPresenter.server.ts | 3 + .../route.tsx | 2 +- .../route.tsx | 28 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 4 +- .../route.tsx | 43 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 758 +++++++++--------- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 14 +- .../route.tsx | 21 +- .../webapp/app/routes/_app.orgs.new/route.tsx | 2 +- .../app/routes/account._index/route.tsx | 4 +- .../app/routes/account.tokens/route.tsx | 4 +- .../webapp/app/routes/admin.feature-flags.tsx | 2 +- .../app/routes/api.v1.plain.customer-cards.ts | 2 +- .../webapp/app/routes/api.v1.prompts.$slug.ts | 2 +- apps/webapp/app/routes/api.v1.query.ts | 2 +- apps/webapp/app/routes/api.v1.schedules.ts | 1 + .../app/routes/confirm-basic-details.tsx | 9 +- apps/webapp/app/routes/invite-resend.tsx | 2 +- apps/webapp/app/routes/invite-revoke.tsx | 2 +- apps/webapp/app/routes/invites.tsx | 55 +- ...deployments.$deploymentShortCode.cancel.ts | 2 +- ...eployments.$deploymentShortCode.promote.ts | 2 +- ...ployments.$deploymentShortCode.rollback.ts | 2 +- ...urces.batches.$batchId.check-completion.ts | 2 +- .../app/routes/resources.branches.archive.tsx | 8 +- apps/webapp/app/routes/resources.feedback.ts | 5 +- ...cts.$projectParam.env.$envParam.github.tsx | 2 +- ...ectParam.env.$envParam.runs.bulkaction.tsx | 2 +- .../route.tsx | 4 +- ...cts.$projectParam.env.$envParam.vercel.tsx | 2 +- .../route.tsx | 2 +- ....orgs.$organizationSlug.schedules-addon.ts | 2 +- .../resources.sessions.$sessionParam.close.ts | 2 +- .../resources.taskruns.$runParam.cancel.ts | 2 +- .../resources.taskruns.$runParam.replay.ts | 2 +- apps/webapp/app/services/apiAuth.server.ts | 4 +- ...authorizationRateLimitMiddleware.server.ts | 2 +- .../app/services/queryService.server.ts | 4 +- .../routeBuilders/apiBuilder.server.ts | 2 +- .../routeBuilders/dashboardBuilder.server.ts | 2 +- .../routeBuilders/dashboardBuilder.ts | 2 +- apps/webapp/app/utils/json.ts | 4 +- apps/webapp/app/utils/timeGranularity.ts | 16 +- apps/webapp/app/v3/featureFlags.server.ts | 4 +- apps/webapp/app/v3/featureFlags.ts | 33 +- apps/webapp/app/v3/marqs/types.ts | 2 +- apps/webapp/app/v3/schedules.ts | 37 +- .../v3/services/aiRunFilterService.server.ts | 2 +- .../app/v3/services/replayTaskRun.server.ts | 4 +- apps/webapp/app/v3/utils/zodPubSub.server.ts | 2 +- .../vercel/vercelProjectIntegrationSchema.ts | 2 +- apps/webapp/package.json | 4 +- internal-packages/clickhouse/package.json | 2 +- .../clickhouse/src/client/tsql.ts | 4 +- .../clickhouse/src/tsqlFunctions.test.ts | 2 +- internal-packages/compute/package.json | 2 +- internal-packages/compute/src/types.ts | 18 +- internal-packages/emails/package.json | 2 +- internal-packages/run-engine/package.json | 2 +- .../run-engine/src/batch-queue/types.ts | 6 +- .../schedule-engine/package.json | 2 +- internal-packages/tsql/package.json | 2 +- internal-packages/zod-worker/package.json | 2 +- internal-packages/zod-worker/src/index.ts | 8 +- package.json | 4 +- packages/cli-v3/package.json | 9 +- packages/cli-v3/src/cli/common.ts | 4 +- packages/cli-v3/src/commands/analyze.ts | 4 +- packages/cli-v3/src/mcp/tools/agentChat.ts | 2 +- packages/cli-v3/src/utilities/configFiles.ts | 4 +- packages/core/package.json | 11 +- packages/core/src/schemas/eventFilter.ts | 2 +- packages/core/src/schemas/json.ts | 4 +- packages/core/src/v3/apiClient/index.ts | 2 +- .../v3/runEngineWorker/supervisor/schemas.ts | 4 +- packages/core/src/v3/schemas/api.ts | 42 +- packages/core/src/v3/schemas/build.ts | 14 +- packages/core/src/v3/schemas/common.ts | 12 +- packages/core/src/v3/schemas/eventFilter.ts | 2 +- packages/core/src/v3/schemas/messages.ts | 4 +- packages/core/src/v3/schemas/openTelemetry.ts | 2 +- packages/core/src/v3/schemas/query.ts | 2 +- packages/core/src/v3/schemas/resources.ts | 2 +- packages/core/src/v3/schemas/runEngine.ts | 2 +- packages/core/src/v3/schemas/schemas.ts | 14 +- packages/core/src/v3/serverOnly/httpServer.ts | 5 +- packages/core/src/v3/types/tools.ts | 2 +- packages/core/src/v3/zodIpc.ts | 5 +- packages/core/src/v3/zodMessageHandler.ts | 9 +- packages/core/src/v3/zodNamespace.ts | 6 +- packages/core/src/v3/zodSocket.ts | 5 +- packages/redis-worker/package.json | 7 +- .../redis-worker/src/mollifier/schemas.ts | 6 +- packages/redis-worker/src/queue.ts | 5 +- packages/redis-worker/src/worker.ts | 4 +- packages/schema-to-json/package.json | 9 +- packages/trigger-sdk/package.json | 4 +- pnpm-lock.yaml | 325 ++++---- 121 files changed, 891 insertions(+), 916 deletions(-) create mode 100644 .changeset/zod-4-support.md delete mode 100644 .server-changes/conform-v1.md diff --git a/.changeset/zod-4-support.md b/.changeset/zod-4-support.md new file mode 100644 index 00000000000..c0578513828 --- /dev/null +++ b/.changeset/zod-4-support.md @@ -0,0 +1,9 @@ +--- +"@trigger.dev/core": minor +"@trigger.dev/sdk": minor +"trigger.dev": minor +"@trigger.dev/redis-worker": minor +"@trigger.dev/schema-to-json": minor +--- + +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. diff --git a/.server-changes/conform-v1.md b/.server-changes/conform-v1.md deleted file mode 100644 index 5e187829460..00000000000 --- a/.server-changes/conform-v1.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -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. diff --git a/apps/supervisor/package.json b/apps/supervisor/package.json index 2725fe2b729..d4555061708 100644 --- a/apps/supervisor/package.json +++ b/apps/supervisor/package.json @@ -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:*", diff --git a/apps/webapp/app/components/Feedback.tsx b/apps/webapp/app/components/Feedback.tsx index 683f39d34ab..692fc0e293f 100644 --- a/apps/webapp/app/components/Feedback.tsx +++ b/apps/webapp/app/components/Feedback.tsx @@ -1,11 +1,5 @@ -import { - getFormProps, - getSelectProps, - getInputProps, - getTextareaProps, - useForm, -} from "@conform-to/react"; -import { parseWithZod } 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"; @@ -53,11 +47,11 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac if ( navigation.formAction === "/resources/feedback" && navigation.state === "loading" && - Object.keys(form.allErrors).length === 0 + (form.errors === undefined || form.errors.length === 0) ) { setOpen(false); } - }, [navigation.formAction, navigation.state, form.allErrors]); + }, [navigation, form]); // Handle URL param functionality useEffect(() => { @@ -95,17 +89,9 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac type === "concurrency" || type === "hipaa" ) &&
} -
+
- + {type === "feature" && ( - +
diff --git a/apps/webapp/app/components/metrics/QueryWidget.tsx b/apps/webapp/app/components/metrics/QueryWidget.tsx index cde5465721b..0754d365dec 100644 --- a/apps/webapp/app/components/metrics/QueryWidget.tsx +++ b/apps/webapp/app/components/metrics/QueryWidget.tsx @@ -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 }); diff --git a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx index 827452e4d04..aeeb4d25ecb 100644 --- a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useActionData, useNavigation, useParams, useSubmit } from "@remix-run/react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; diff --git a/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx b/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx index 3054a0cdf48..428389e129b 100644 --- a/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx +++ b/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx @@ -1,5 +1,5 @@ import { getFormProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { EnvelopeIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { useFetcher } from "@remix-run/react"; diff --git a/apps/webapp/app/models/orgIntegration.server.ts b/apps/webapp/app/models/orgIntegration.server.ts index 1d04084692c..752a1ba26c3 100644 --- a/apps/webapp/app/models/orgIntegration.server.ts +++ b/apps/webapp/app/models/orgIntegration.server.ts @@ -20,7 +20,7 @@ const SlackSecretSchema = z.object({ refreshToken: z.string().optional(), botScopes: z.array(z.string()).optional(), userScopes: z.array(z.string()).optional(), - raw: z.record(z.any()).optional(), + raw: z.record(z.string(), z.any()).optional(), }); type SlackSecret = z.infer; diff --git a/apps/webapp/app/models/vercelIntegration.server.ts b/apps/webapp/app/models/vercelIntegration.server.ts index b9c3f22da28..2ed8e466cc5 100644 --- a/apps/webapp/app/models/vercelIntegration.server.ts +++ b/apps/webapp/app/models/vercelIntegration.server.ts @@ -143,7 +143,7 @@ export const VercelSecretSchema = z.object({ teamId: z.string().nullable().optional(), userId: z.string().optional(), installationId: z.string().optional(), - raw: z.record(z.any()).optional(), + raw: z.record(z.string(), z.any()).optional(), }); export type VercelSecret = z.infer; diff --git a/apps/webapp/app/models/vercelSdkRecovery.server.ts b/apps/webapp/app/models/vercelSdkRecovery.server.ts index d3e1bfd6961..31253fe430e 100644 --- a/apps/webapp/app/models/vercelSdkRecovery.server.ts +++ b/apps/webapp/app/models/vercelSdkRecovery.server.ts @@ -145,11 +145,11 @@ export const VercelSchemas = { .union([ z .object({ - envs: z.array(z.record(z.unknown())), + envs: z.array(z.record(z.string(), z.unknown())), pagination: z.unknown().optional(), }) .passthrough(), - z.array(z.record(z.unknown())), + z.array(z.record(z.string(), z.unknown())), ]) .transform((val) => (Array.isArray(val) ? { envs: val } : val)), diff --git a/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts b/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts index df43864b53a..8a4dfc9dc68 100644 --- a/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts @@ -45,7 +45,7 @@ export const DashboardLayout = z.discriminatedUnion("version", [ z.object({ version: z.literal("1"), layout: z.array(LayoutItem), - widgets: z.record(Widget), + widgets: z.record(z.string(), Widget), }), ]); diff --git a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts index 5e8a6db9ed3..6bececfdba5 100644 --- a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts @@ -1006,6 +1006,9 @@ export class SpanPresenter extends BasePresenter { filePath: run.lockedBy?.filePath ?? "", }, run: { + // zod v4 types the run-context `context` (z.any) field as required; it was + // never populated here (optional under v3), so undefined preserves behavior. + context: undefined, id: run.friendlyId, createdAt: run.createdAt, tags: run.runTags, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx index e6173da559f..76eb2645b56 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { ArrowUpCircleIcon, EnvelopeIcon, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index c3fbd269fad..6ec787e7505 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -1,11 +1,11 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { HashtagIcon, LockClosedIcon } from "@heroicons/react/20/solid"; import { Form, useActionData, useNavigate, useNavigation } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/router"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { SlackIcon } from "@trigger.dev/companyicons"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; @@ -217,7 +217,6 @@ export default function Page() { const project = useProject(); const environment = useEnvironment(); const [currentAlertChannel, setCurrentAlertChannel] = useState(option ?? "EMAIL"); - const formRef = useRef(null); const [selectedSlackChannelValue, setSelectedSlackChannelValue] = useState(); @@ -230,15 +229,16 @@ export default function Page() { navigation.formMethod === "post" && navigation.formData?.get("action") === "create"; - const [form, { channelValue, alertTypes, environmentTypes, type, integrationId }] = useForm({ - id: "create-alert", - // TODO: type this - lastResult: lastSubmission as any, - onValidate({ formData }) { - return parseWithZod(formData, { schema: FormSchema }); - }, - shouldRevalidate: "onSubmit", - }); + const [form, { channelValue: channelValue, alertTypes, environmentTypes, type, integrationId }] = + useForm({ + id: "create-alert", + // TODO: type this + lastResult: lastSubmission as any, + onValidate({ formData }) { + return parseWithZod(formData, { schema: FormSchema }); + }, + shouldRevalidate: "onSubmit", + }); useEffect(() => { setIsOpen(true); @@ -248,7 +248,7 @@ export default function Page() { if (navigation.state !== "idle") return; if (lastSubmission !== undefined) return; - formRef.current?.reset(); + form.reset(); }, [navigation.state, lastSubmission]); return ( @@ -262,7 +262,7 @@ export default function Page() { > New alert - +
- {formEnvironments.errors} + {formEnvironments.errors}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index adfa701ff0b..97d18c25f34 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -5,7 +5,7 @@ import { getInputProps, useForm, } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { LockClosedIcon, LockOpenIcon, @@ -79,22 +79,19 @@ const schema = z.object({ if (i === "true") return true; return false; }, z.boolean()), - environmentIds: z.preprocess( - (i) => { - if (typeof i === "string") return [i]; - - if (Array.isArray(i)) { - const ids = i.filter((v) => typeof v === "string" && v !== ""); - if (ids.length === 0) { - return; - } - return ids; + environmentIds: z.preprocess((i) => { + if (typeof i === "string") return [i]; + + if (Array.isArray(i)) { + const ids = i.filter((v) => typeof v === "string" && v !== ""); + if (ids.length === 0) { + return; } + return ids; + } - return; - }, - z.array(z.string(), { required_error: "At least one environment is required" }) - ), + return; + }, z.array(z.string(), { error: "At least one environment is required" })), variables: z.preprocess((i) => { if (!Array.isArray(i)) { return []; @@ -227,14 +224,12 @@ export default function Page() { const [selectedEnvironmentIds, setSelectedEnvironmentIds] = useState>(new Set()); const [selectedBranchId, setSelectedBranchId] = useState(undefined); - // TODO for no we only support branch-specific env vars for Preview environments - // Mostly to keep the UX for setting consistent env-vars across Dev/Staging/Prod easier - const previewBranches = environments.filter( - (env) => env.type === "PREVIEW" && env.parentEnvironmentId !== null - ); - const nonBranchEnvironments = environments.filter((env) => env.parentEnvironmentId === null); + const branchEnvironments = environments.filter((env) => env.branchName); + const nonBranchEnvironments = environments.filter((env) => !env.branchName); const selectedEnvironments = environments.filter((env) => selectedEnvironmentIds.has(env.id)); - const previewIsSelected = selectedEnvironments.some((env) => env.type === "PREVIEW"); + const previewIsSelected = selectedEnvironments.some( + (env) => env.branchName !== null || env.type === "PREVIEW" + ); const isLoading = navigation.state !== "idle" && navigation.formMethod === "post"; @@ -416,7 +411,7 @@ export default function Page() { value={selectedBranchId ?? "all"} setValue={handleBranchChange} placeholder="All branches" - items={[{ id: "all", branchName: "All branches" }, ...previewBranches]} + items={[{ id: "all", branchName: "All branches" }, ...branchEnvironments]} className="w-fit min-w-52" filter={{ keys: [ @@ -424,7 +419,7 @@ export default function Page() { ], }} text={(val) => - val ? previewBranches.find((b) => b.id === val)?.branchName : null + val ? branchEnvironments.find((b) => b.id === val)?.branchName : null } dropdownIcon > diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index afea881feb1..a0696b20c9f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { BookOpenIcon, InformationCircleIcon, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx index 3abde6802c3..c7c41a99bec 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { BellAlertIcon } from "@heroicons/react/20/solid"; import { type MetaFunction, useFetcher, useRevalidator } from "@remix-run/react"; import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx index 3ffe35908a5..4eb0751eebd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Outlet, useNavigate } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { useCallback } from "react"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx index 848546a518f..3ca61c53f5d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx @@ -160,7 +160,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (!parsed.success) { return typedjson( { - error: parsed.error.errors.map((e) => e.message).join(", "), + error: parsed.error.issues.map((e) => e.message).join(", "), rows: null, columns: null, stats: null, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx index cd433467c77..468635743b6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { useLocation } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx index 7ca93c2bc50..ea06a5aa082 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { conformZodMessage, parseWithZod } from "@conform-to/zod"; +import { conformZodMessage, parseWithZod } from "@conform-to/zod/v4"; import { ExclamationTriangleIcon, FolderIcon, TrashIcon } from "@heroicons/react/20/solid"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { type ActionFunction, json } from "@remix-run/server-runtime"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx index ffe79fab6ba..90124b3e6f4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { json } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData, useTypedFetcher } from "remix-typedjson"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 582f7f8ef47..5c120193ba7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { CheckCircleIcon, RectangleStackIcon, @@ -430,37 +430,15 @@ function StandardTaskForm({ const [showTemplateCreatedSuccessMessage, setShowTemplateCreatedSuccessMessage] = useState(false); - const [ - form, - { - environmentId, - payload, - metadata, - taskIdentifier, - delaySeconds, - ttlSeconds, - idempotencyKey, - idempotencyKeyTTLSeconds, - queue, - concurrencyKey, - maxAttempts, - maxDurationSeconds, - triggerSource, - tags, - version, - machine, - region, - prioritySeconds, - }, - ] = useForm({ + const [form, fields] = useForm({ id: "test-task", // TODO: type this lastResult: lastSubmission as any, onSubmit(event, { formData }) { event.preventDefault(); - formData.set(payload.name, currentPayloadJson.current); - formData.set(metadata.name, currentMetadataJson.current); + formData.set(fields.payload.name, currentPayloadJson.current); + formData.set(fields.metadata.name, currentMetadataJson.current); submit(formData, { method: "POST" }); }, @@ -468,6 +446,26 @@ function StandardTaskForm({ return parseWithZod(formData, { schema: TestTaskData }); }, }); + const { + environmentId, + payload, + metadata, + taskIdentifier, + delaySeconds, + ttlSeconds, + idempotencyKey, + idempotencyKeyTTLSeconds, + queue, + concurrencyKey, + maxAttempts, + maxDurationSeconds, + triggerSource, + tags, + version, + machine, + region, + prioritySeconds, + } = fields; return ( @@ -1017,379 +1015,387 @@ function ScheduledTaskForm({ method="post" {...getFormProps(form)} > - - - + + + {/* Main area: scrolling form with the toolbar floating on top-right in the same grid cell */}
-
- - {task.taskIdentifier} -
-
- - - - setTimestampValue(val)} - granularity="second" - showNowButton - variant="small" - utc - /> - - This is the timestamp of the CRON, it will come through to your run in the - payload. - - {timestamp.errors} - - - - - setLastTimestampValue(val)} - granularity="second" - showNowButton - showClearButton - variant="small" - utc - /> - - This is the timestamp of the previous run. You can use this in your code to find - new data since the previous run. - - {lastTimestamp.errors} - - - - - - The Timestamp and Last timestamp are in UTC so this just changes the timezone - string that comes through in the payload. - - {timezone.errors} - - - - setExternalIdValue(e.target.value)} - variant="small" - /> - - Optionally, you can specify your own IDs (like a user ID) and then use it inside - the run function of your task.{" "} - Read the docs. - - {externalId.errors} - -
+
+ + {task.taskIdentifier} +
+
+ + + + setTimestampValue(val)} + granularity="second" + showNowButton + variant="small" + utc + /> + + This is the timestamp of the CRON, it will come through to your run in the payload. + + {timestamp.errors} + + + + + setLastTimestampValue(val)} + granularity="second" + showNowButton + showClearButton + variant="small" + utc + /> + + This is the timestamp of the previous run. You can use this in your code to find new + data since the previous run. + + {lastTimestamp.errors} + + + + + + The Timestamp and Last timestamp are in UTC so this just changes the timezone string + that comes through in the payload. + + {timezone.errors} + + + + setExternalIdValue(e.target.value)} + variant="small" + /> - Options enable you to control the execution behavior of your task.{" "} - Read the docs. + Optionally, you can specify your own IDs (like a user ID) and then use it inside the + run function of your task.{" "} + Read the docs. + {externalId.errors} + +
+ + Options enable you to control the execution behavior of your task.{" "} + Read the docs. + + + + + Overrides the machine preset. + {machine.errors} + + + + + {disableVersionSelection ? ( + Only the latest version is available in the development environment. + ) : ( + Runs task on a specific version. + )} + {version.errors} + + {regionItems.length > 1 && ( - - - + )} + + + {allowArbitraryQueues ? ( + setQueueValue(e.target.value)} + /> + ) : ( - {disableVersionSelection ? ( - Only the latest version is available in the development environment. - ) : ( - Runs task on a specific version. - )} - {version.errors} - - {regionItems.length > 1 && ( - - - {/* Our Select primitive uses Ariakit under the hood, which treats - value={undefined} as uncontrolled, keeping stale internal state when - switching environments. The key forces a remount so it reinitializes - with the correct defaultValue. */} - - {isDev ? ( - Region is not available in the development environment. - ) : ( - Overrides the region for this run. - )} - {region.errors} - + )) + } + )} - - - {allowArbitraryQueues ? ( - setQueueValue(e.target.value)} - /> - ) : ( - - )} - Assign run to a specific queue. - {queue.errors} - - - - - Add tags to easily filter runs. - {tags.errors} - - - - - setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined) + Assign run to a specific queue. + {queue.errors} + + + + + Add tags to easily filter runs. + {tags.errors} + + + + + setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined) + } + onKeyDown={(e) => { + // only allow entering integers > 1 + if (["-", "+", ".", "e", "E"].includes(e.key)) { + e.preventDefault(); } - onKeyDown={(e) => { - // only allow entering integers > 1 - if (["-", "+", ".", "e", "E"].includes(e.key)) { - e.preventDefault(); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value); - if (value < 1 && e.target.value !== "") { - e.target.value = "1"; - } - }} - /> - Retries failed runs up to the specified number of attempts. - {maxAttempts.errors} - - - - - Overrides the maximum compute time limit for the run. - {maxDurationSeconds.errors} - - - - - {idempotencyKey.errors} - - Specify an idempotency key to ensure that a task is only triggered once with the - same key. - - - - - - Keys expire after 30 days by default. - - {idempotencyKeyTTLSeconds.errors} - - - - - setConcurrencyKeyValue(e.target.value)} - /> - - Limits concurrency by creating a separate queue for each value of the key. - - {concurrencyKey.errors} - - - - - Sets the priority of the run. Higher values mean higher priority. - {prioritySeconds.errors} - - - - - Expires the run if it hasn't started within the TTL. - {ttlSeconds.errors} - -
+ }} + onBlur={(e) => { + const value = parseInt(e.target.value); + if (value < 1 && e.target.value !== "") { + e.target.value = "1"; + } + }} + /> + Retries failed runs up to the specified number of attempts. + {maxAttempts.errors} + + + + + Overrides the maximum compute time limit for the run. + {maxDurationSeconds.errors} + + + + + {idempotencyKey.errors} + + Specify an idempotency key to ensure that a task is only triggered once with the + same key. + + + + + + Keys expire after 30 days by default. + + {idempotencyKeyTTLSeconds.errors} + + + + + setConcurrencyKeyValue(e.target.value)} + /> + + Limits concurrency by creating a separate queue for each value of the key. + + {concurrencyKey.errors} + + + + + Sets the priority of the run. Higher values mean higher priority. + {prioritySeconds.errors} + + + + + Expires the run if it hasn't started within the TTL. + {ttlSeconds.errors} + +
{/* Toolbar overlay — same grid cell, sits above scrolling form. Outer diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index bcf0483db76..15c8eaf3dfd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -1,6 +1,6 @@ import colorWheelIcon from "../../assets/images/color-wheel.png"; import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { conformZodMessage, parseWithZod } from "@conform-to/zod"; +import { conformZodMessage, parseWithZod } from "@conform-to/zod/v4"; import { CheckIcon, ExclamationTriangleIcon, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx index b3af74e56f7..e0fcb0a330c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Form, useActionData, useParams, type MetaFunction } from "@remix-run/react"; import { json, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core/utils"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx index d4d29f8f37a..b2d17fdb930 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { EnvelopeIcon, NoSymbolIcon, UserPlusIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { @@ -941,11 +941,11 @@ export function PurchaseSeatsModal({ // when the role can't manage billing. The action enforces it independently. const noBillingTooltip = "You don't have permission to manage billing"; const trigger = canManageBilling ? ( - (triggerButton ?? ( + triggerButton ?? ( - )) + ) ) : triggerButton ? ( cloneElement(triggerButton, { disabled: true, tooltip: noBillingTooltip }) ) : ( @@ -963,11 +963,7 @@ export function PurchaseSeatsModal({ {trigger} {title} - +
@@ -983,7 +979,7 @@ export function PurchaseSeatsModal({ Total extra seats - +
} @@ -391,15 +388,9 @@ export default function Page() { {projectName.errors} {canCreateV3Projects ? ( - + ) : ( - + )}
@@ -461,7 +452,9 @@ export default function Page() { shuffledGoals.indexOf(v) + 1))} + value={JSON.stringify( + selectedGoals.map((v) => shuffledGoals.indexOf(v) + 1) + )} /> [], result.columns); return json({ format: "csv", diff --git a/apps/webapp/app/routes/api.v1.schedules.ts b/apps/webapp/app/routes/api.v1.schedules.ts index 56250eaac55..d996af84d5c 100644 --- a/apps/webapp/app/routes/api.v1.schedules.ts +++ b/apps/webapp/app/routes/api.v1.schedules.ts @@ -102,6 +102,7 @@ export async function loader({ request }: LoaderFunctionArgs) { environmentId: authenticationResult.environment.id, page: params.data.page ?? 1, pageSize: params.data.perPage, + tasks: undefined, }); return { diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index c4101d40044..242f72ae9bd 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { conformZodMessage, parseWithZod } from "@conform-to/zod"; +import { conformZodMessage, parseWithZod } from "@conform-to/zod/v4"; import { ArrowRightIcon, EnvelopeIcon, UserIcon } from "@heroicons/react/20/solid"; import { UserGroupIcon } from "~/assets/icons/UserGroupIcon"; import { HandRaisedIcon } from "@heroicons/react/24/solid"; @@ -242,10 +242,7 @@ export default function Page() { return ( - + ) : ( <> - + )} diff --git a/apps/webapp/app/routes/invite-resend.tsx b/apps/webapp/app/routes/invite-resend.tsx index f2938b67a61..cdce4a76a74 100644 --- a/apps/webapp/app/routes/invite-resend.tsx +++ b/apps/webapp/app/routes/invite-resend.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json } from "@remix-run/server-runtime"; import { env } from "process"; import { z } from "zod"; diff --git a/apps/webapp/app/routes/invite-revoke.tsx b/apps/webapp/app/routes/invite-revoke.tsx index fc1e4d0b79e..d4ab69f9c83 100644 --- a/apps/webapp/app/routes/invite-revoke.tsx +++ b/apps/webapp/app/routes/invite-revoke.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json } from "@remix-run/server-runtime"; import { z } from "zod"; import { $replica } from "~/db.server"; diff --git a/apps/webapp/app/routes/invites.tsx b/apps/webapp/app/routes/invites.tsx index cccd1073a5f..5ee1466c245 100644 --- a/apps/webapp/app/routes/invites.tsx +++ b/apps/webapp/app/routes/invites.tsx @@ -1,5 +1,5 @@ import { getFormProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { type ActionFunction, type LoaderFunctionArgs, json, redirect } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -11,16 +11,9 @@ import { Fieldset } from "~/components/primitives/Fieldset"; import { FormTitle } from "~/components/primitives/FormTitle"; import { Header2, Header3 } from "~/components/primitives/Headers"; import { InputGroup } from "~/components/primitives/InputGroup"; -import { FormError } from "~/components/primitives/FormError"; import { Paragraph } from "~/components/primitives/Paragraph"; -import { - acceptInvite, - declineInvite, - ENV_SETUP_INCOMPLETE, - getUsersInvites, - isAcceptInviteFormError, -} from "~/models/member.server"; -import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; +import { acceptInvite, declineInvite, getUsersInvites } from "~/models/member.server"; +import { redirectWithSuccessMessage } from "~/models/message.server"; import { requireUser, requireUserId } from "~/services/session.server"; import { invitesPath, rootPath } from "~/utils/pathBuilder"; import { EnvelopeIcon } from "@heroicons/react/20/solid"; @@ -40,7 +33,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const schema = z.object({ inviteId: z.string(), - organizationId: z.string().optional(), }); export const action: ActionFunction = async ({ request }) => { @@ -59,7 +51,6 @@ export const action: ActionFunction = async ({ request }) => { if (intent === "accept") { const { remainingInvites, organization } = await acceptInvite({ inviteId: submission.value.inviteId, - organizationId: submission.value.organizationId, user: { id: user.id, email: user.email }, }); @@ -91,30 +82,8 @@ export const action: ActionFunction = async ({ request }) => { ); } } - } catch (error) { - if (isAcceptInviteFormError(error)) { - // Membership may already exist while the invite is still present if env - // provisioning failed. With no invites left, the loader would redirect - // and discard a 400 FormError — send the user to orgs with a toast instead. - if (error.message === ENV_SETUP_INCOMPLETE) { - const remainingInvites = await getUsersInvites({ email: user.email }); - if (remainingInvites.length === 0) { - return redirectWithErrorMessage(rootPath(), request, error.message, { - ephemeral: false, - }); - } - } - - return json( - { - intent: submission.intent, - payload: submission.payload, - error: { "": [error.message] }, - }, - { status: 400 } - ); - } - throw error; + } catch (error: any) { + return json({ errors: { body: error.message } }, { status: 400 }); } }; @@ -134,17 +103,13 @@ export default function Page() { return ( - +
} className="mb-0 text-sky-500" title={simplur`You have ${invites.length} new invitation[|s]`} /> - {form.error} {invites.map((invite) => (
@@ -155,10 +120,14 @@ export default function Page() { Invited by {invite.inviter.displayName ?? invite.inviter.email} -
-
- +
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index 6ec787e7505..48ebea3e5c7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -229,16 +229,15 @@ export default function Page() { navigation.formMethod === "post" && navigation.formData?.get("action") === "create"; - const [form, { channelValue: channelValue, alertTypes, environmentTypes, type, integrationId }] = - useForm({ - id: "create-alert", - // TODO: type this - lastResult: lastSubmission as any, - onValidate({ formData }) { - return parseWithZod(formData, { schema: FormSchema }); - }, - shouldRevalidate: "onSubmit", - }); + const [form, { channelValue, alertTypes, environmentTypes, type, integrationId }] = useForm({ + id: "create-alert", + // TODO: type this + lastResult: lastSubmission as any, + onValidate({ formData }) { + return parseWithZod(formData, { schema: FormSchema }); + }, + shouldRevalidate: "onSubmit", + }); useEffect(() => { setIsOpen(true); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index 97d18c25f34..b10163573cc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -79,19 +79,22 @@ const schema = z.object({ if (i === "true") return true; return false; }, z.boolean()), - environmentIds: z.preprocess((i) => { - if (typeof i === "string") return [i]; - - if (Array.isArray(i)) { - const ids = i.filter((v) => typeof v === "string" && v !== ""); - if (ids.length === 0) { - return; + environmentIds: z.preprocess( + (i) => { + if (typeof i === "string") return [i]; + + if (Array.isArray(i)) { + const ids = i.filter((v) => typeof v === "string" && v !== ""); + if (ids.length === 0) { + return; + } + return ids; } - return ids; - } - return; - }, z.array(z.string(), { error: "At least one environment is required" })), + return; + }, + z.array(z.string(), { error: "At least one environment is required" }) + ), variables: z.preprocess((i) => { if (!Array.isArray(i)) { return []; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 5c120193ba7..f6e69161ebd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -1015,387 +1015,379 @@ function ScheduledTaskForm({ method="post" {...getFormProps(form)} > - - - + + + {/* Main area: scrolling form with the toolbar floating on top-right in the same grid cell */}
-
- - {task.taskIdentifier} -
-
- - - - setTimestampValue(val)} - granularity="second" - showNowButton - variant="small" - utc - /> - - This is the timestamp of the CRON, it will come through to your run in the payload. - - {timestamp.errors} - - - - - setLastTimestampValue(val)} - granularity="second" - showNowButton - showClearButton - variant="small" - utc - /> - - This is the timestamp of the previous run. You can use this in your code to find new - data since the previous run. - - {lastTimestamp.errors} - - - - - - The Timestamp and Last timestamp are in UTC so this just changes the timezone string - that comes through in the payload. - - {timezone.errors} - - - - setExternalIdValue(e.target.value)} - variant="small" - /> +
+ + {task.taskIdentifier} +
+
+ + + + setTimestampValue(val)} + granularity="second" + showNowButton + variant="small" + utc + /> + + This is the timestamp of the CRON, it will come through to your run in the + payload. + + {timestamp.errors} + + + + + setLastTimestampValue(val)} + granularity="second" + showNowButton + showClearButton + variant="small" + utc + /> + + This is the timestamp of the previous run. You can use this in your code to find + new data since the previous run. + + {lastTimestamp.errors} + + + + + + The Timestamp and Last timestamp are in UTC so this just changes the timezone + string that comes through in the payload. + + {timezone.errors} + + + + setExternalIdValue(e.target.value)} + variant="small" + /> + + Optionally, you can specify your own IDs (like a user ID) and then use it inside + the run function of your task.{" "} + Read the docs. + + {externalId.errors} + +
- Optionally, you can specify your own IDs (like a user ID) and then use it inside the - run function of your task.{" "} - Read the docs. + Options enable you to control the execution behavior of your task.{" "} + Read the docs. - {externalId.errors} - -
- - Options enable you to control the execution behavior of your task.{" "} - Read the docs. - - - - - Overrides the machine preset. - {machine.errors} - - - - - {disableVersionSelection ? ( - Only the latest version is available in the development environment. - ) : ( - Runs task on a specific version. - )} - {version.errors} - - {regionItems.length > 1 && ( - - )} - - - {allowArbitraryQueues ? ( - setQueueValue(e.target.value)} - /> - ) : ( + + + {disableVersionSelection ? ( + Only the latest version is available in the development environment. + ) : ( + Runs task on a specific version. + )} + {version.errors} + + {regionItems.length > 1 && ( + + + {/* Our Select primitive uses Ariakit under the hood, which treats + value={undefined} as uncontrolled, keeping stale internal state when + switching environments. The key forces a remount so it reinitializes + with the correct defaultValue. */} + + {isDev ? ( + Region is not available in the development environment. + ) : ( + Overrides the region for this run. + )} + {region.errors} + )} - Assign run to a specific queue. - {queue.errors} - - - - - Add tags to easily filter runs. - {tags.errors} - - - - - setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined) - } - onKeyDown={(e) => { - // only allow entering integers > 1 - if (["-", "+", ".", "e", "E"].includes(e.key)) { - e.preventDefault(); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value); - if (value < 1 && e.target.value !== "") { - e.target.value = "1"; + + + {allowArbitraryQueues ? ( + setQueueValue(e.target.value)} + /> + ) : ( + + )} + Assign run to a specific queue. + {queue.errors} + + + + + Add tags to easily filter runs. + {tags.errors} + + + + + setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined) } - }} - /> - Retries failed runs up to the specified number of attempts. - {maxAttempts.errors} - - - - - Overrides the maximum compute time limit for the run. - {maxDurationSeconds.errors} - - - - - {idempotencyKey.errors} - - Specify an idempotency key to ensure that a task is only triggered once with the - same key. - - - - - - Keys expire after 30 days by default. - - {idempotencyKeyTTLSeconds.errors} - - - - - setConcurrencyKeyValue(e.target.value)} - /> - - Limits concurrency by creating a separate queue for each value of the key. - - {concurrencyKey.errors} - - - - - Sets the priority of the run. Higher values mean higher priority. - {prioritySeconds.errors} - - - - - Expires the run if it hasn't started within the TTL. - {ttlSeconds.errors} - -
+ onKeyDown={(e) => { + // only allow entering integers > 1 + if (["-", "+", ".", "e", "E"].includes(e.key)) { + e.preventDefault(); + } + }} + onBlur={(e) => { + const value = parseInt(e.target.value); + if (value < 1 && e.target.value !== "") { + e.target.value = "1"; + } + }} + /> + Retries failed runs up to the specified number of attempts. + {maxAttempts.errors} +
+ + + + Overrides the maximum compute time limit for the run. + {maxDurationSeconds.errors} + + + + + {idempotencyKey.errors} + + Specify an idempotency key to ensure that a task is only triggered once with the + same key. + + + + + + Keys expire after 30 days by default. + + {idempotencyKeyTTLSeconds.errors} + + + + + setConcurrencyKeyValue(e.target.value)} + /> + + Limits concurrency by creating a separate queue for each value of the key. + + {concurrencyKey.errors} + + + + + Sets the priority of the run. Higher values mean higher priority. + {prioritySeconds.errors} + + + + + Expires the run if it hasn't started within the TTL. + {ttlSeconds.errors} + +
{/* Toolbar overlay — same grid cell, sits above scrolling form. Outer diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx index a7790c4ee48..c61bbe72b8f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import type { MetaFunction } from "@remix-run/react"; import { json, redirect } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx index b2d17fdb930..6250fcfc274 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx @@ -941,11 +941,11 @@ export function PurchaseSeatsModal({ // when the role can't manage billing. The action enforces it independently. const noBillingTooltip = "You don't have permission to manage billing"; const trigger = canManageBilling ? ( - triggerButton ?? ( + (triggerButton ?? ( - ) + )) ) : triggerButton ? ( cloneElement(triggerButton, { disabled: true, tooltip: noBillingTooltip }) ) : ( @@ -963,7 +963,11 @@ export function PurchaseSeatsModal({ {trigger} {title} - +
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx index 1a4cf40b371..29222e83d10 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx @@ -361,7 +361,10 @@ export default function Page() { return ( - +
} @@ -388,9 +391,15 @@ export default function Page() { {projectName.errors} {canCreateV3Projects ? ( - + ) : ( - + )}
@@ -452,9 +461,7 @@ export default function Page() { shuffledGoals.indexOf(v) + 1) - )} + value={JSON.stringify(selectedGoals.map((v) => shuffledGoals.indexOf(v) + 1))} /> - + ) : ( <> - + )} diff --git a/apps/webapp/app/routes/invites.tsx b/apps/webapp/app/routes/invites.tsx index 5ee1466c245..b21a0ef2dd5 100644 --- a/apps/webapp/app/routes/invites.tsx +++ b/apps/webapp/app/routes/invites.tsx @@ -103,7 +103,10 @@ export default function Page() { return ( - +
} @@ -122,12 +125,7 @@ export default function Page() {
-