fix: validate mobile event drawer steps with schema
This commit is contained in:
@@ -31,6 +31,7 @@ import { useIsMobile } from "@/hooks/use-mobile";
|
|||||||
import {
|
import {
|
||||||
type EventFormValues,
|
type EventFormValues,
|
||||||
getDefaultEventFormValues,
|
getDefaultEventFormValues,
|
||||||
|
validateEventFormStep,
|
||||||
validateEventFormValues,
|
validateEventFormValues,
|
||||||
} from "@/lib/event-form";
|
} from "@/lib/event-form";
|
||||||
import { parseRecurrenceRule, validateRecurrence } from "@/lib/recurrence";
|
import { parseRecurrenceRule, validateRecurrence } from "@/lib/recurrence";
|
||||||
@@ -76,13 +77,14 @@ export const EventDialog = ({
|
|||||||
const saveLabel = editingId ? "Update Event" : "Save Event";
|
const saveLabel = editingId ? "Update Event" : "Save Event";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
clearErrors,
|
||||||
control,
|
control,
|
||||||
|
getValues,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
reset,
|
reset,
|
||||||
setError,
|
setError,
|
||||||
setValue,
|
setValue,
|
||||||
trigger,
|
|
||||||
watch,
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<EventFormValues>({
|
} = useForm<EventFormValues>({
|
||||||
@@ -106,9 +108,23 @@ export const EventDialog = ({
|
|||||||
onOpenChange(nextOpen);
|
onOpenChange(nextOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = () => {
|
||||||
const valid = await trigger(STEP_FIELDS[step]);
|
const stepFields = STEP_FIELDS[step];
|
||||||
if (valid) advanceStep();
|
clearErrors(stepFields);
|
||||||
|
const stepValidation = validateEventFormStep(getValues(), stepFields);
|
||||||
|
if (stepValidation.success) {
|
||||||
|
advanceStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [fieldName, messages] of Object.entries(
|
||||||
|
stepValidation.fieldErrors,
|
||||||
|
)) {
|
||||||
|
const firstMessage = messages?.[0];
|
||||||
|
if (firstMessage) {
|
||||||
|
setError(fieldName as keyof EventFormValues, { message: firstMessage });
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = handleSubmit((values) => {
|
const handleSave = handleSubmit((values) => {
|
||||||
|
|||||||
@@ -110,3 +110,27 @@ export const getEventValidationIssues = (
|
|||||||
|
|
||||||
export const validateEventFormValues = (values: EventFormValues) =>
|
export const validateEventFormValues = (values: EventFormValues) =>
|
||||||
eventFormSchema.safeParse(values);
|
eventFormSchema.safeParse(values);
|
||||||
|
|
||||||
|
export const validateEventFormStep = (
|
||||||
|
values: EventFormValues,
|
||||||
|
fields: Array<keyof EventFormValues>,
|
||||||
|
) => {
|
||||||
|
const result = validateEventFormValues(values);
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true as const, fieldErrors: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFieldErrors = result.error.flatten().fieldErrors;
|
||||||
|
const fieldErrors: Partial<Record<keyof EventFormValues, string[]>> = {};
|
||||||
|
for (const field of fields) {
|
||||||
|
const messages = allFieldErrors[field];
|
||||||
|
if (messages?.length) {
|
||||||
|
fieldErrors[field] = messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: Object.keys(fieldErrors).length === 0,
|
||||||
|
fieldErrors,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -77,6 +77,14 @@ describe("EventDialog public modes", () => {
|
|||||||
expect(source).toContain("setStep(1)");
|
expect(source).toContain("setStep(1)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("event-dialog validates mobile drawer steps with schema-backed step validation", () => {
|
||||||
|
const source = readFileSync("src/components/event-dialog.tsx", "utf8");
|
||||||
|
|
||||||
|
expect(source).toContain("validateEventFormStep");
|
||||||
|
expect(source).toContain("getValues()");
|
||||||
|
expect(source).toContain("fieldErrors");
|
||||||
|
});
|
||||||
|
|
||||||
test("dialog.tsx no longer forks on isMobile in DialogContent", () => {
|
test("dialog.tsx no longer forks on isMobile in DialogContent", () => {
|
||||||
const source = readFileSync("src/components/ui/dialog.tsx", "utf8");
|
const source = readFileSync("src/components/ui/dialog.tsx", "utf8");
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import {
|
import {
|
||||||
getDefaultEventFormValues,
|
getDefaultEventFormValues,
|
||||||
getEventFormValuesFromEvent,
|
getEventFormValuesFromEvent,
|
||||||
|
validateEventFormStep,
|
||||||
validateEventFormValues,
|
validateEventFormValues,
|
||||||
} from "@/lib/event-form";
|
} from "@/lib/event-form";
|
||||||
|
|
||||||
@@ -70,4 +71,31 @@ describe("event form defaults and validation", () => {
|
|||||||
expect(result.error.flatten().fieldErrors.url?.[0]).toContain("valid URL");
|
expect(result.error.flatten().fieldErrors.url?.[0]).toContain("valid URL");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("validates only the requested mobile drawer step", () => {
|
||||||
|
const values = {
|
||||||
|
...getDefaultEventFormValues(),
|
||||||
|
url: "not-a-url",
|
||||||
|
end: "2026-04-09T09:30:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
const detailsResult = validateEventFormStep(values, ["title", "url"]);
|
||||||
|
const scheduleResult = validateEventFormStep(values, ["start", "end"]);
|
||||||
|
|
||||||
|
expect(detailsResult.success).toBe(false);
|
||||||
|
if (!detailsResult.success) {
|
||||||
|
expect(detailsResult.fieldErrors.title?.[0]).toContain("Title is required");
|
||||||
|
expect(detailsResult.fieldErrors.url?.[0]).toContain("valid URL");
|
||||||
|
expect(detailsResult.fieldErrors.start).toBeUndefined();
|
||||||
|
expect(detailsResult.fieldErrors.end).toBeUndefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(scheduleResult.success).toBe(false);
|
||||||
|
if (!scheduleResult.success) {
|
||||||
|
expect(scheduleResult.fieldErrors.start?.[0]).toContain("Start date is required");
|
||||||
|
expect(scheduleResult.fieldErrors.end?.[0]).toContain("valid");
|
||||||
|
expect(scheduleResult.fieldErrors.title).toBeUndefined();
|
||||||
|
expect(scheduleResult.fieldErrors.url).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user