diff --git a/app/components/device/new/new-device-stepper.tsx b/app/components/device/new/new-device-stepper.tsx index 5ed8b0b91..58f81aa34 100644 --- a/app/components/device/new/new-device-stepper.tsx +++ b/app/components/device/new/new-device-stepper.tsx @@ -36,7 +36,7 @@ const generalInfoSchema = z.object({ .min(2, 'Name must be at least 2 characters') .min(1, 'Name is required'), exposure: z.enum(['indoor', 'outdoor', 'mobile', 'unknown'], { - errorMap: () => ({ message: 'Exposure is required' }), + error: () => 'Exposure is required', }), temporaryExpirationDate: z .string() @@ -46,8 +46,8 @@ const generalInfoSchema = z.object({ (date) => !date || date <= new Date(Date.now() + 31 * 24 * 60 * 60 * 1000), { - message: 'Temporary expiration date must be within 1 month from now', - }, + error: 'Temporary expiration date must be within 1 month from now' + }, ), tags: z .array( @@ -61,23 +61,21 @@ const generalInfoSchema = z.object({ const locationSchema = z.object({ latitude: z.coerce .number({ - invalid_type_error: 'Latitude must be a valid number', - required_error: 'Latitude is required', - }) + error: (issue) => issue.input === undefined ? 'Latitude is required' : 'Latitude must be a valid number' + }) .min(-90, 'Latitude must be greater than or equal to -90') .max(90, 'Latitude must be less than or equal to 90'), longitude: z.coerce .number({ - invalid_type_error: 'Longitude must be a valid number', - required_error: 'Longitude is required', - }) + error: (issue) => issue.input === undefined ? 'Longitude is required' : 'Longitude must be a valid number' + }) .min(-180, 'Longitude must be greater than or equal to -180') .max(180, 'Longitude must be less than or equal to 180'), }) const deviceSchema = z.object({ model: z.enum(DeviceModelEnum.enumValues, { - errorMap: () => ({ message: 'Please select a device.' }), + error: () => 'Please select a device.', }), }) @@ -88,7 +86,7 @@ const sensorsSchema = z.object({ .min(1, 'Please select at least one sensor'), }) -const advancedSchema = z.record(z.any()) +const advancedSchema = z.record(z.string(), z.any()) export const Stepper = defineStepper( { diff --git a/app/lib/api-schemas/boxes-data-query-schema.ts b/app/lib/api-schemas/boxes-data-query-schema.ts index d946b5c26..a5c648f69 100644 --- a/app/lib/api-schemas/boxes-data-query-schema.ts +++ b/app/lib/api-schemas/boxes-data-query-schema.ts @@ -36,8 +36,8 @@ const BoxesDataQuerySchemaBase = z .transform((arr) => arr.map((x) => Number(x))), ]) .refine((arr) => arr.length === 4 && arr.every((n) => !isNaN(n)), { - message: 'bbox must contain exactly 4 numeric coordinates', - }) + error: 'bbox must contain exactly 4 numeric coordinates' + }) .optional(), exposure: z @@ -61,26 +61,26 @@ const BoxesDataQuerySchemaBase = z .string() .transform((s) => new Date(s)) .refine((d) => !isNaN(d.getTime()), { - message: 'from-date is invalid', - }) + error: 'from-date is invalid' + }) .optional() - .default(() => + .prefault(() => new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), ), toDate: z .string() .transform((s) => new Date(s)) .refine((d) => !isNaN(d.getTime()), { - message: 'to-date is invalid', - }) + error: 'to-date is invalid' + }) .optional() - .default(() => new Date().toISOString()), + .prefault(() => new Date().toISOString()), format: z .enum(['csv', 'json'], { - errorMap: () => ({ message: "Format must be either 'csv' or 'json'" }), + error: () => "Format must be either 'csv' or 'json'", }) - .default('csv'), + .prefault('csv'), // Columns to include columns: z @@ -96,7 +96,7 @@ const BoxesDataQuerySchemaBase = z arr.map((s) => String(s).trim() as BoxesDataColumn), ), ]) - .default([ + .prefault([ 'sensorId', 'createdAt', 'value', @@ -110,15 +110,15 @@ const BoxesDataQuerySchemaBase = z if (typeof v === 'boolean') return v return v !== 'false' && v !== '0' }) - .default(true), + .prefault(true), - delimiter: z.enum(['comma', 'semicolon']).default('comma'), + delimiter: z.enum(['comma', 'semicolon']).prefault('comma'), }) // Validate: must have boxid or bbox, but not both .superRefine((data, ctx) => { if (!data.boxid && !data.bbox && !data.grouptag) { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: 'please specify either boxid, bbox or grouptag', path: ['boxid'], }) @@ -126,7 +126,7 @@ const BoxesDataQuerySchemaBase = z if (!data.phenomenon && !data.grouptag) { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: 'phenomenon parameter is required when grouptag is not provided', path: ['phenomenon'], @@ -163,7 +163,7 @@ export async function parseBoxesDataQuery( const parseResult = BoxesDataQuerySchemaBase.safeParse(params) if (!parseResult.success) { - const firstError = parseResult.error.errors[0] + const firstError = parseResult.error.issues[0] const message = firstError.message || 'Invalid query parameters' if (firstError.path.includes('bbox')) { diff --git a/app/lib/devices-service.server.ts b/app/lib/devices-service.server.ts index 2dc4ff71d..900cb0d51 100644 --- a/app/lib/devices-service.server.ts +++ b/app/lib/devices-service.server.ts @@ -8,11 +8,11 @@ export const CreateBoxSchema = z.object({ exposure: z .enum(['indoor', 'outdoor', 'mobile', 'unknown']) .optional() - .default('unknown'), + .prefault('unknown'), location: z .array(z.number()) .length(2, 'Location must be [longitude, latitude]'), - grouptag: z.array(z.string()).optional().default([]), + grouptag: z.array(z.string()).optional().prefault([]), model: z .enum([ 'homeV2Lora', @@ -23,7 +23,7 @@ export const CreateBoxSchema = z.object({ 'custom', ]) .optional() - .default('custom'), + .prefault('custom'), sensors: z .array( z.object({ @@ -35,32 +35,36 @@ export const CreateBoxSchema = z.object({ }), ) .optional() - .default([]), + .prefault([]), }) export const BoxesQuerySchema = z.object({ format: z .enum(['json', 'geojson'], { - errorMap: () => ({ - message: "Format must be either 'json' or 'geojson'", - }), + error: () => "Format must be either 'json' or 'geojson'", }) - .default('json'), + .prefault('json'), minimal: z .enum(['true', 'false']) - .default('false') + .prefault('false') .transform((v) => v === 'true'), full: z .enum(['true', 'false']) - .default('false') + .prefault('false') .transform((v) => v === 'true'), limit: z .string() - .default('5') + .prefault('5') .transform((val) => parseInt(val, 10)) - .refine((val) => !isNaN(val), { message: 'Limit must be a number' }) - .refine((val) => val >= 1, { message: 'Limit must be at least 1' }) - .refine((val) => val <= 20, { message: 'Limit must not exceed 20' }), + .refine((val) => !isNaN(val), { + error: 'Limit must be a number' + }) + .refine((val) => val >= 1, { + error: 'Limit must be at least 1' + }) + .refine((val) => val <= 20, { + error: 'Limit must not exceed 20' + }), name: z.string().optional(), date: z @@ -111,8 +115,8 @@ export const BoxesQuerySchema = z.object({ near: z .string() .regex(/^[-+]?\d+(\.\d+)?,[-+]?\d+(\.\d+)?$/, { - message: "Invalid 'near' parameter format. Expected: 'lat,lng'", - }) + error: "Invalid 'near' parameter format. Expected: 'lat,lng'" + }) .transform((val) => val.split(',').map(Number) as [number, number]) .optional(), @@ -143,14 +147,10 @@ export const BoxesQuerySchema = z.object({ }) .optional(), - fromDate: z - .string() - .datetime() + fromDate: z.iso.datetime() .transform((v) => new Date(v)) .optional(), - toDate: z - .string() - .datetime() + toDate: z.iso.datetime() .transform((v) => new Date(v)) .optional(), }) diff --git a/app/routes/settings.profile.photo.tsx b/app/routes/settings.profile.photo.tsx index 8b05a4f56..2deca503c 100644 --- a/app/routes/settings.profile.photo.tsx +++ b/app/routes/settings.profile.photo.tsx @@ -1,5 +1,5 @@ -import { conform, useForm } from '@conform-to/react' -import { getFieldsetConstraint, parse } from '@conform-to/zod' +import { useForm, getInputProps, getFormProps } from '@conform-to/react' +import { getZodConstraint, parseWithZod } from '@conform-to/zod/v4' import { type FileUpload, parseFormData } from '@mjackson/form-data-parser' import { eq } from 'drizzle-orm' import { useState } from 'react' @@ -14,7 +14,7 @@ import { useLoaderData, useNavigate, } from 'react-router' -import { z } from 'zod' +import { z } from 'zod/v4' import ErrorMessage from '~/components/error-message' import { LabelButton } from '~/components/label-button' import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar' @@ -74,17 +74,11 @@ export async function action({ request }: ActionFunctionArgs) { async (file: FileUpload) => uploadHandler(file), ) - const submission = parse(formData, { schema: PhotoFormSchema }) + const submission = parseWithZod(formData, { schema: PhotoFormSchema }) - if (submission.intent !== 'submit') { - return { status: 'idle', submission } as const - } - if (!submission.value) { + if (submission.status !== 'success') { return data( - { - status: 'error', - submission, - } as const, + { status: 'error', submission: submission.reply() } as const, { status: 400 }, ) } @@ -127,16 +121,18 @@ export default function PhotoChooserModal() { const actionData = useActionData() const [form, { photoFile }] = useForm({ id: 'profile-photo', - constraint: getFieldsetConstraint(PhotoFormSchema), - lastSubmission: actionData?.submission, + constraint: getZodConstraint(PhotoFormSchema), + lastResult: actionData?.submission, onValidate({ formData }) { - return parse(formData, { schema: PhotoFormSchema }) + return parseWithZod(formData, { schema: PhotoFormSchema }) }, shouldRevalidate: 'onBlur', }) const { t } = useTranslation('settings') + const { key, ...photoFileProps } = getInputProps(photoFile, { type: 'file' }) + const dismissModal = () => navigate('..', { preventScrollReset: true }) return ( @@ -149,11 +145,11 @@ export default function PhotoChooserModal() { {t('profile_photo')}
setNewImageSrc(null)} - {...form.props} > {/* */} =16.8" + "react": ">=18" } }, "node_modules/@conform-to/zod": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-0.9.2.tgz", - "integrity": "sha512-treG9ZcuNuRERQ1uYvJSWT0zZuqHnYTzRwucg20+/WdjgKNSb60Br+Cy6BAHvVQ8dN6wJsGkHenkX2mSVw3xOA==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-1.17.1.tgz", + "integrity": "sha512-oFt2OdCrAhNEKjjyc8QAgiykDI9s+RG9nAuxrqGyp/BFXuRPPiu0e+QIVUAxSnWtZ82swtSaf87DI+jTAyhaLg==", "license": "MIT", + "dependencies": { + "@conform-to/dom": "1.17.1" + }, "peerDependencies": { - "@conform-to/dom": "0.9.2", - "zod": "^3.21.0" + "zod": "^3.21.0 || ^4.0.0" } }, "node_modules/@csstools/color-helpers": { @@ -30174,24 +30176,24 @@ "license": "Unlicense" }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-form-data": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/zod-form-data/-/zod-form-data-2.0.8.tgz", - "integrity": "sha512-X31GkEc8uk5/L387L4TVI1z7obBbN/0MRHBHfHW3uMOWkVJeSsa+grvkTvY9qyFbNshKEnqK+jLJNlY+d7jpLw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/zod-form-data/-/zod-form-data-3.0.1.tgz", + "integrity": "sha512-uwSrDzpLDoXeAxePjPHrjjMelE5pk5zL5JcwLFISvqidGjtPl7hcheH584xGcS76c9IRHq6tqdGkf+A4eKO6Cw==", "license": "MIT", "dependencies": { "@rvf/set-get": "^7.0.0" }, "peerDependencies": { - "zod": ">= 3.11.0" + "zod": ">= 3.25.0" } }, "node_modules/zwitch": { diff --git a/package.json b/package.json index 3569189c6..b9d248ace 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "dependencies": { "@aws-sdk/client-s3": "^3.971.0", "@aws-sdk/s3-request-presigner": "^3.971.0", - "@conform-to/react": "^0.9.2", - "@conform-to/zod": "^0.9.2", + "@conform-to/react": "^1.17.1", + "@conform-to/zod": "^1.17.1", "@directus/sdk": "^19.1.0", "@formatjs/intl": "^4.1.2", "@heroicons/react": "^2.2.0", @@ -151,8 +151,8 @@ "uuid": "^11.1.0", "vaul": "^1.1.2", "vite-plugin-markdown": "^2.2.0", - "zod": "^3.25.67", - "zod-form-data": "^2.0.8" + "zod": "^4.3.6", + "zod-form-data": "^3.0.1" }, "devDependencies": { "@epic-web/config": "^1.21.0",