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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user