From 12954e5ba1f9e72100022fff47440ee92eadf45b Mon Sep 17 00:00:00 2001 From: Yashwin Date: Mon, 18 May 2026 18:22:54 +0530 Subject: [PATCH] feat: implement Zod hex color validation and error handling for tenant color settings in wizard and edit forms --- src/pages/superadmin/CreateTenantWizard.tsx | 62 +++++++++++++++++++-- src/pages/superadmin/EditTenant.tsx | 62 +++++++++++++++++++-- src/services/module-service.ts | 4 +- 3 files changed, 114 insertions(+), 14 deletions(-) diff --git a/src/pages/superadmin/CreateTenantWizard.tsx b/src/pages/superadmin/CreateTenantWizard.tsx index 1636bf5..69800b7 100644 --- a/src/pages/superadmin/CreateTenantWizard.tsx +++ b/src/pages/superadmin/CreateTenantWizard.tsx @@ -92,13 +92,51 @@ const contactDetailsSchema = z.object({ country: z.string().min(1, "Country is required"), }); +const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + // Step 3: Settings Schema const settingsSchema = z.object({ enable_sso: z.boolean(), enable_2fa: z.boolean(), - primary_color: z.string().optional().nullable(), - secondary_color: z.string().optional().nullable(), - accent_color: z.string().optional().nullable(), + primary_color: z + .string() + .optional() + .nullable() + .refine( + (val) => { + if (!val || val.trim() === "") return true; + return hexColorRegex.test(val); + }, + { + message: "Must be a valid hex color code (e.g., #112868)", + } + ), + secondary_color: z + .string() + .optional() + .nullable() + .refine( + (val) => { + if (!val || val.trim() === "") return true; + return hexColorRegex.test(val); + }, + { + message: "Must be a valid hex color code (e.g., #23DCE1)", + } + ), + accent_color: z + .string() + .optional() + .nullable() + .refine( + (val) => { + if (!val || val.trim() === "") return true; + return hexColorRegex.test(val); + }, + { + message: "Must be a valid hex color code (e.g., #084CC8)", + } + ), }); type TenantDetailsForm = z.infer; @@ -1303,7 +1341,7 @@ const CreateTenantWizard = (): ReactElement => { type="text" value={settingsForm.watch("primary_color") || "#112868"} onChange={(e) => - settingsForm.setValue("primary_color", e.target.value) + settingsForm.setValue("primary_color", e.target.value, { shouldValidate: true }) } className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" placeholder="#112868" @@ -1313,11 +1351,14 @@ const CreateTenantWizard = (): ReactElement => { type="color" value={settingsForm.watch("primary_color") || "#112868"} onChange={(e) => - settingsForm.setValue("primary_color", e.target.value) + settingsForm.setValue("primary_color", e.target.value, { shouldValidate: true }) } className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" /> + {settingsForm.formState.errors.primary_color && ( +

{settingsForm.formState.errors.primary_color.message}

+ )}

Used for navigation, headers, and key actions.

@@ -1348,6 +1389,7 @@ const CreateTenantWizard = (): ReactElement => { settingsForm.setValue( "secondary_color", e.target.value, + { shouldValidate: true } ) } className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" @@ -1363,11 +1405,15 @@ const CreateTenantWizard = (): ReactElement => { settingsForm.setValue( "secondary_color", e.target.value, + { shouldValidate: true } ) } className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" /> + {settingsForm.formState.errors.secondary_color && ( +

{settingsForm.formState.errors.secondary_color.message}

+ )}

Used for highlights and supporting elements.

@@ -1396,6 +1442,7 @@ const CreateTenantWizard = (): ReactElement => { settingsForm.setValue( "accent_color", e.target.value, + { shouldValidate: true } ) } className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" @@ -1406,11 +1453,14 @@ const CreateTenantWizard = (): ReactElement => { type="color" value={settingsForm.watch("accent_color") || "#084CC8"} onChange={(e) => - settingsForm.setValue("accent_color", e.target.value) + settingsForm.setValue("accent_color", e.target.value, { shouldValidate: true }) } className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" /> + {settingsForm.formState.errors.accent_color && ( +

{settingsForm.formState.errors.accent_color.message}

+ )}

Used for alerts and special notices.

diff --git a/src/pages/superadmin/EditTenant.tsx b/src/pages/superadmin/EditTenant.tsx index 275a071..74b1d6e 100644 --- a/src/pages/superadmin/EditTenant.tsx +++ b/src/pages/superadmin/EditTenant.tsx @@ -99,13 +99,51 @@ const contactDetailsSchema = z.object({ country: z.string().min(1, "Country is required"), }); +const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + // Step 3: Settings Schema const settingsSchema = z.object({ enable_sso: z.boolean(), enable_2fa: z.boolean(), - primary_color: z.string().optional().nullable(), - secondary_color: z.string().optional().nullable(), - accent_color: z.string().optional().nullable(), + primary_color: z + .string() + .optional() + .nullable() + .refine( + (val) => { + if (!val || val.trim() === "") return true; + return hexColorRegex.test(val); + }, + { + message: "Must be a valid hex color code (e.g., #112868)", + } + ), + secondary_color: z + .string() + .optional() + .nullable() + .refine( + (val) => { + if (!val || val.trim() === "") return true; + return hexColorRegex.test(val); + }, + { + message: "Must be a valid hex color code (e.g., #23DCE1)", + } + ), + accent_color: z + .string() + .optional() + .nullable() + .refine( + (val) => { + if (!val || val.trim() === "") return true; + return hexColorRegex.test(val); + }, + { + message: "Must be a valid hex color code (e.g., #084CC8)", + } + ), }); type TenantDetailsForm = z.infer; @@ -1465,7 +1503,7 @@ const EditTenant = (): ReactElement => { type="text" value={settingsForm.watch("primary_color") || "#112868"} onChange={(e) => - settingsForm.setValue("primary_color", e.target.value) + settingsForm.setValue("primary_color", e.target.value, { shouldValidate: true }) } className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" placeholder="#112868" @@ -1475,11 +1513,14 @@ const EditTenant = (): ReactElement => { type="color" value={settingsForm.watch("primary_color") || "#112868"} onChange={(e) => - settingsForm.setValue("primary_color", e.target.value) + settingsForm.setValue("primary_color", e.target.value, { shouldValidate: true }) } className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" /> + {settingsForm.formState.errors.primary_color && ( +

{settingsForm.formState.errors.primary_color.message}

+ )}

Used for navigation, headers, and key actions.

@@ -1509,6 +1550,7 @@ const EditTenant = (): ReactElement => { settingsForm.setValue( "secondary_color", e.target.value, + { shouldValidate: true } ) } className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" @@ -1524,11 +1566,15 @@ const EditTenant = (): ReactElement => { settingsForm.setValue( "secondary_color", e.target.value, + { shouldValidate: true } ) } className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" /> + {settingsForm.formState.errors.secondary_color && ( +

{settingsForm.formState.errors.secondary_color.message}

+ )}

Used for highlights and supporting elements.

@@ -1556,6 +1602,7 @@ const EditTenant = (): ReactElement => { settingsForm.setValue( "accent_color", e.target.value, + { shouldValidate: true } ) } className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" @@ -1566,11 +1613,14 @@ const EditTenant = (): ReactElement => { type="color" value={settingsForm.watch("accent_color") || "#084CC8"} onChange={(e) => - settingsForm.setValue("accent_color", e.target.value) + settingsForm.setValue("accent_color", e.target.value, { shouldValidate: true }) } className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" /> + {settingsForm.formState.errors.accent_color && ( +

{settingsForm.formState.errors.accent_color.message}

+ )}

Used for alerts and special notices.

diff --git a/src/services/module-service.ts b/src/services/module-service.ts index b3bc26e..e750c5e 100644 --- a/src/services/module-service.ts +++ b/src/services/module-service.ts @@ -23,8 +23,8 @@ export const moduleService = { params.append('search', search); } if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) { - params.append('orderBy[]', orderBy[0]); - params.append('orderBy[]', orderBy[1]); + params.append('sort_by', orderBy[0]); + params.append('sort_order', orderBy[1]); } const response = await apiClient.get(`/modules?${params.toString()}`); return response.data;