Phase 6.3: SettingsForms on RHF + Zod

- SettingsCurrencies: RHF+Zod (code required max 3, name required, symbol required max 5)
- SettingsTaxes: RHF+Zod (name required, rate numeric 0-100, country max 2)
- SettingsSequences: RHF+Zod (name required, padding numeric 1-10)
- SettingsUsers: RHF+Zod (name required, email valid, password min 8)
- SettingsRoles: RHF+Zod for create form (name required)
- SettingsGroups: RHF+Zod for create form (name required, description optional)
- Error display under each field
- Preserved all existing functionality: CRUD, permissions, members
- Added 2 validation tests for SettingsCurrencies
This commit is contained in:
Agent Zero
2026-07-24 00:28:11 +02:00
parent 2931a850c0
commit c334d02989
7 changed files with 306 additions and 109 deletions
+23 -14
View File
@@ -1,5 +1,8 @@
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useRoles, useCreateRole, useUpdateRole, useDeleteRole, usePermissions } from '@/api/hooks';
import type { PermissionItem, FieldDefinition } from '@/api/hooks';
import { Input } from '@/components/ui/Input';
@@ -69,8 +72,18 @@ export function SettingsRolesPage() {
const updateRoleMutation = useUpdateRole();
const deleteRoleMutation = useDeleteRole();
// ── Zod Schema for create form ──
const roleCreateSchema = z.object({
name: z.string().min(1, 'required'),
});
type RoleCreateFormData = z.infer<typeof roleCreateSchema>;
const { register: registerRole, handleSubmit: handleSubmitRole, reset: resetRole, formState: { errors: roleErrors } } = useForm<RoleCreateFormData>({
resolver: zodResolver(roleCreateSchema),
defaultValues: { name: '' },
});
const [createOpen, setCreateOpen] = useState(false);
const [newRoleName, setNewRoleName] = useState('');
const [newRolePermissions, setNewRolePermissions] = useState<Record<string, any>>({});
const [newRoleDenied, setNewRoleDenied] = useState<string[]>([]);
const [editingRole, setEditingRole] = useState<Role | null>(null);
@@ -117,20 +130,16 @@ export function SettingsRolesPage() {
return groups;
}, [fieldDefinitions]);
const handleCreateRole = async () => {
if (!newRoleName.trim()) {
toast.error(t('validation.required'));
return;
}
const handleCreateRole = async (data: RoleCreateFormData) => {
try {
await createRoleMutation.mutateAsync({
name: newRoleName.trim(),
name: data.name.trim(),
permissions: newRolePermissions,
denied_permissions: newRoleDenied,
field_permissions: {},
} as any);
toast.success(t('settings.roleCreated'));
setNewRoleName('');
resetRole({ name: '' });
setNewRolePermissions({});
setNewRoleDenied([]);
setCreateOpen(false);
@@ -238,7 +247,7 @@ export function SettingsRolesPage() {
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-roles-page">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-secondary-900">{t('settings.roles')}</h1>
<Button onClick={() => setCreateOpen(true)} data-testid="create-role-btn">
<Button onClick={() => { resetRole({ name: '' }); setCreateOpen(true); }} data-testid="create-role-btn">
{t('settings.createRole')}
</Button>
</div>
@@ -292,11 +301,11 @@ export function SettingsRolesPage() {
)}
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title={t('settings.createRole')}>
<div className="space-y-4" data-testid="create-role-form">
<form onSubmit={handleSubmitRole(handleCreateRole)} className="space-y-4" data-testid="create-role-form">
<Input
label={t('settings.roleName')}
value={newRoleName}
onChange={(e) => setNewRoleName(e.target.value)}
{...registerRole('name')}
error={roleErrors.name?.message === 'required' ? t('validation.required') : undefined}
placeholder={t('settings.roleName')}
data-testid="new-role-name"
/>
@@ -350,14 +359,14 @@ export function SettingsRolesPage() {
<div className="flex justify-end gap-3">
<Button variant="secondary" onClick={() => setCreateOpen(false)}>{t('common.cancel')}</Button>
<Button
onClick={handleCreateRole}
type="submit"
isLoading={createRoleMutation.isPending}
data-testid="save-role-btn"
>
{t('common.save')}
</Button>
</div>
</div>
</form>
</Modal>
<Modal open={!!editingRole} onClose={() => setEditingRole(null)} title={t('settings.roles') + ' — ' + (editingRole?.name || '')} size="xl">