diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts index 8b41b7780..bd4e95c01 100644 --- a/client/src/Validation/statusPage.ts +++ b/client/src/Validation/statusPage.ts @@ -1,6 +1,14 @@ import { z } from "zod"; +import type { FieldPath } from "react-hook-form"; import { STATUS_PAGE_THEMES, STATUS_PAGE_THEME_MODES } from "@/Types/StatusPage"; +// Wizard step a field is validated on. Attached inline to each field below so +// the grouping lives next to the field definition; unannotated fields default +// to step 0. Read via `statusPageStepFieldsFor`. Mirrors the monitor schema's +// `monitorStepRegistry` so the create-status-page wizard can't drift from the +// field definitions. +export const statusPageStepRegistry = z.registry<{ step: number }>(); + export const statusPageSchema = z.object({ companyName: z .string() @@ -18,23 +26,53 @@ export const statusPageSchema = z.object({ type: z .array(z.enum(["uptime", "infrastructure"])) .min(1, "At least one type is required"), - color: z.string().min(1, "Color is required"), + color: z.string().min(1, "Color is required").register(statusPageStepRegistry, { + step: 1, + }), monitors: z.array(z.string()).min(1, "At least one monitor is required"), isPublished: z.boolean(), - showCharts: z.boolean(), - showUptimePercentage: z.boolean(), - showAdminLoginLink: z.boolean(), - showInfrastructure: z.boolean(), - customCSS: z.string().optional(), - theme: z.enum(STATUS_PAGE_THEMES).optional(), - themeMode: z.enum(STATUS_PAGE_THEME_MODES).optional(), + showCharts: z.boolean().register(statusPageStepRegistry, { step: 1 }), + showUptimePercentage: z.boolean().register(statusPageStepRegistry, { step: 1 }), + showAdminLoginLink: z.boolean().register(statusPageStepRegistry, { step: 1 }), + showInfrastructure: z.boolean().register(statusPageStepRegistry, { step: 1 }), + customCSS: z.string().optional().register(statusPageStepRegistry, { step: 1 }), + theme: z + .enum(STATUS_PAGE_THEMES) + .optional() + .register(statusPageStepRegistry, { step: 1 }), + themeMode: z + .enum(STATUS_PAGE_THEME_MODES) + .optional() + .register(statusPageStepRegistry, { step: 1 }), logo: z .object({ data: z.string(), contentType: z.string(), }) .nullable() - .optional(), + .optional() + .register(statusPageStepRegistry, { step: 1 }), }); export type StatusPageFormData = z.infer; + +// The step each field is validated on, from the per-field +// `statusPageStepRegistry` metadata (unannotated fields default to step 0). +const fieldSteps = (): [name: FieldPath, step: number][] => + Object.entries(statusPageSchema.shape).map(([name, field]) => [ + name as FieldPath, + statusPageStepRegistry.get(field)?.step ?? 0, + ]); + +// The fields that belong to a given wizard step. Derived from the schema so it +// can't drift from the field definitions. +export const statusPageStepFieldsFor = ( + step: number +): FieldPath[] => + fieldSteps() + .filter(([, fieldStep]) => fieldStep === step) + .map(([name]) => name); + +// Number of wizard steps, derived from the highest step its fields declare. +export const statusPageStepCount = (): number => + Math.max(0, ...fieldSteps().map(([, step]) => step)) + 1;