feat: implement Zod hex color validation and error handling for tenant color settings in wizard and edit forms
This commit is contained in:
parent
c927886621
commit
12954e5ba1
@ -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<typeof tenantDetailsSchema>;
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
{settingsForm.formState.errors.primary_color && (
|
||||
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.primary_color.message}</p>
|
||||
)}
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for navigation, headers, and key actions.
|
||||
</p>
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
{settingsForm.formState.errors.secondary_color && (
|
||||
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.secondary_color.message}</p>
|
||||
)}
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for highlights and supporting elements.
|
||||
</p>
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
{settingsForm.formState.errors.accent_color && (
|
||||
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.accent_color.message}</p>
|
||||
)}
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for alerts and special notices.
|
||||
</p>
|
||||
|
||||
@ -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<typeof tenantDetailsSchema>;
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
{settingsForm.formState.errors.primary_color && (
|
||||
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.primary_color.message}</p>
|
||||
)}
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for navigation, headers, and key actions.
|
||||
</p>
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
{settingsForm.formState.errors.secondary_color && (
|
||||
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.secondary_color.message}</p>
|
||||
)}
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for highlights and supporting elements.
|
||||
</p>
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
{settingsForm.formState.errors.accent_color && (
|
||||
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.accent_color.message}</p>
|
||||
)}
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for alerts and special notices.
|
||||
</p>
|
||||
|
||||
@ -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<ModulesResponse>(`/modules?${params.toString()}`);
|
||||
return response.data;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user