feat: Add field validation and UI configuration management, and enable editing of field assignments.
This commit is contained in:
@@ -15,12 +15,14 @@ import {
|
||||
FieldType,
|
||||
FieldOption,
|
||||
RangeConfig,
|
||||
FieldValidation,
|
||||
FieldUiConfig,
|
||||
FIELD_TYPES,
|
||||
getErrorMessage,
|
||||
} from '@/services';
|
||||
|
||||
type FieldModalType = 'create' | 'edit' | 'delete' | null;
|
||||
type AssignModalType = 'assign' | null;
|
||||
type AssignModalType = 'assign' | 'editAssignment' | null;
|
||||
|
||||
const REQUIRES_OPTIONS: FieldType[] = ['SELECT', 'RADIO', 'MULTI_SELECT', 'CHECKBOX_GROUP'];
|
||||
|
||||
@@ -58,6 +60,8 @@ export default function SectionDetailPage() {
|
||||
isSearchableOnly: false,
|
||||
options: [],
|
||||
rangeConfig: undefined,
|
||||
validation: undefined,
|
||||
uiConfig: undefined,
|
||||
});
|
||||
|
||||
// Options editor state
|
||||
@@ -114,6 +118,8 @@ export default function SectionDetailPage() {
|
||||
isSearchableOnly: false,
|
||||
options: [],
|
||||
rangeConfig: undefined,
|
||||
validation: undefined,
|
||||
uiConfig: undefined,
|
||||
});
|
||||
setOptionInput({ value: '', label: '' });
|
||||
setModalError('');
|
||||
@@ -135,6 +141,8 @@ export default function SectionDetailPage() {
|
||||
isSearchableOnly: field.isSearchableOnly,
|
||||
options: field.options || [],
|
||||
rangeConfig: field.rangeConfig || undefined,
|
||||
validation: field.validation || undefined,
|
||||
uiConfig: field.uiConfig || undefined,
|
||||
});
|
||||
setOptionInput({ value: '', label: '' });
|
||||
setModalError('');
|
||||
@@ -217,6 +225,30 @@ export default function SectionDetailPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Validation management
|
||||
const updateValidation = (key: keyof FieldValidation, value: number | string) => {
|
||||
const current = fieldForm.validation || {};
|
||||
const parsed = typeof value === 'string' && value === '' ? undefined : typeof value === 'string' ? (isNaN(Number(value)) ? value : Number(value)) : value;
|
||||
const updated = { ...current, [key]: parsed };
|
||||
// Remove undefined keys
|
||||
Object.keys(updated).forEach((k) => {
|
||||
if (updated[k as keyof FieldValidation] === undefined || updated[k as keyof FieldValidation] === '') {
|
||||
delete updated[k as keyof FieldValidation];
|
||||
}
|
||||
});
|
||||
setFieldForm({ ...fieldForm, validation: Object.keys(updated).length > 0 ? updated : undefined });
|
||||
};
|
||||
|
||||
// UI Config management
|
||||
const updateUiConfig = (key: keyof FieldUiConfig, value: number | string | boolean) => {
|
||||
const current = fieldForm.uiConfig || {};
|
||||
const updated = { ...current, [key]: value };
|
||||
if (value === '' || value === undefined) {
|
||||
delete updated[key as keyof FieldUiConfig];
|
||||
}
|
||||
setFieldForm({ ...fieldForm, uiConfig: Object.keys(updated).length > 0 ? updated : undefined });
|
||||
};
|
||||
|
||||
// Range config management
|
||||
const updateRangeConfig = (key: keyof RangeConfig, value: number | string) => {
|
||||
setFieldForm({
|
||||
@@ -257,6 +289,34 @@ export default function SectionDetailPage() {
|
||||
setModalError('');
|
||||
};
|
||||
|
||||
const openEditAssignmentModal = (ats: { agentTypeId: string; sortOrder: number; isRequired: boolean }) => {
|
||||
setAssignForm({
|
||||
agentTypeId: ats.agentTypeId,
|
||||
sortOrder: ats.sortOrder,
|
||||
isRequired: ats.isRequired,
|
||||
});
|
||||
setModalError('');
|
||||
setAssignModalType('editAssignment');
|
||||
};
|
||||
|
||||
const handleUpdateAssignment = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setModalError('');
|
||||
try {
|
||||
await profileSectionsService.updateAssignment(sectionId, assignForm.agentTypeId, {
|
||||
sortOrder: assignForm.sortOrder,
|
||||
isRequired: assignForm.isRequired,
|
||||
});
|
||||
closeAssignModal();
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
setModalError(getErrorMessage(err));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssign = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
@@ -533,16 +593,27 @@ export default function SectionDetailPage() {
|
||||
{ats.isRequired ? 'Required' : 'Optional'} • Order: {ats.sortOrder}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<button
|
||||
onClick={() => openEditAssignmentModal(ats)}
|
||||
className="p-1 text-[#9ca3af] hover:text-[#f5a623]"
|
||||
title="Edit assignment"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRemoveAssignment(ats.agentTypeId)}
|
||||
className="p-1 text-[#9ca3af] hover:text-red-600"
|
||||
title="Remove assignment"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -826,6 +897,120 @@ export default function SectionDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation Rules */}
|
||||
<div className="border border-[#e5e7eb] rounded-xl p-4">
|
||||
<label className="block text-sm font-medium text-[#00293d] mb-3">
|
||||
Validation Rules
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{['TEXT', 'TEXTAREA', 'TAG_INPUT'].includes(fieldForm.fieldType) && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs text-[#666666] mb-1">Min Length</label>
|
||||
<input
|
||||
type="number"
|
||||
value={fieldForm.validation?.minLength ?? ''}
|
||||
onChange={(e) => updateValidation('minLength', e.target.value)}
|
||||
placeholder="No min"
|
||||
min={0}
|
||||
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-[#666666] mb-1">Max Length</label>
|
||||
<input
|
||||
type="number"
|
||||
value={fieldForm.validation?.maxLength ?? ''}
|
||||
onChange={(e) => updateValidation('maxLength', e.target.value)}
|
||||
placeholder="No max"
|
||||
min={0}
|
||||
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{['NUMBER', 'RANGE'].includes(fieldForm.fieldType) && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs text-[#666666] mb-1">Min Value</label>
|
||||
<input
|
||||
type="number"
|
||||
value={fieldForm.validation?.min ?? ''}
|
||||
onChange={(e) => updateValidation('min', e.target.value)}
|
||||
placeholder="No min"
|
||||
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-[#666666] mb-1">Max Value</label>
|
||||
<input
|
||||
type="number"
|
||||
value={fieldForm.validation?.max ?? ''}
|
||||
onChange={(e) => updateValidation('max', e.target.value)}
|
||||
placeholder="No max"
|
||||
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="col-span-2">
|
||||
<label className="block text-xs text-[#666666] mb-1">Pattern (regex)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fieldForm.validation?.pattern ?? ''}
|
||||
onChange={(e) => updateValidation('pattern', e.target.value)}
|
||||
placeholder="e.g., ^[a-zA-Z]+$"
|
||||
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* UI Configuration */}
|
||||
<div className="border border-[#e5e7eb] rounded-xl p-4">
|
||||
<label className="block text-sm font-medium text-[#00293d] mb-3">
|
||||
UI Configuration
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-[#666666] mb-1">Columns (grid layout)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={fieldForm.uiConfig?.columns ?? ''}
|
||||
onChange={(e) => updateUiConfig('columns', e.target.value ? Number(e.target.value) : '')}
|
||||
placeholder="Auto"
|
||||
min={1}
|
||||
max={4}
|
||||
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center pt-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="showInPreview"
|
||||
checked={fieldForm.uiConfig?.showInPreview ?? false}
|
||||
onChange={(e) => updateUiConfig('showInPreview', e.target.checked)}
|
||||
className="rounded border-[#e5e7eb] text-[#f5a623] focus:ring-0"
|
||||
/>
|
||||
<label htmlFor="showInPreview" className="ml-2 text-sm text-[#00293d] font-serif">
|
||||
Show in Preview
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-[#666666] mb-1">Help Text</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fieldForm.uiConfig?.helpText ?? ''}
|
||||
onChange={(e) => updateUiConfig('helpText', e.target.value)}
|
||||
placeholder="Additional help text shown below the field"
|
||||
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-[#e5e7eb] flex justify-end space-x-3">
|
||||
<button
|
||||
@@ -885,19 +1070,22 @@ export default function SectionDetailPage() {
|
||||
)}
|
||||
|
||||
{/* Assign to Agent Type Modal */}
|
||||
{assignModalType === 'assign' && (
|
||||
{(assignModalType === 'assign' || assignModalType === 'editAssignment') && (
|
||||
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-[#e5e7eb]-xl max-w-md w-full mx-4">
|
||||
<div className="px-6 py-4 border-b border-[#e5e7eb]">
|
||||
<h3 className="text-lg font-semibold text-[#00293d] font-fractul">Assign to Agent Type</h3>
|
||||
<h3 className="text-lg font-semibold text-[#00293d] font-fractul">
|
||||
{assignModalType === 'editAssignment' ? 'Edit Assignment' : 'Assign to Agent Type'}
|
||||
</h3>
|
||||
</div>
|
||||
<form onSubmit={handleAssign}>
|
||||
<form onSubmit={assignModalType === 'editAssignment' ? handleUpdateAssignment : handleAssign}>
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
{modalError && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-700 text-sm">{modalError}</p>
|
||||
</div>
|
||||
)}
|
||||
{assignModalType === 'assign' ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#00293d] mb-1">
|
||||
Agent Type <span className="text-red-500">*</span>
|
||||
@@ -916,6 +1104,14 @@ export default function SectionDetailPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#00293d] mb-1">Agent Type</label>
|
||||
<p className="px-3 py-2 bg-[#f5f9f8] rounded-xl text-[#00293d] text-sm">
|
||||
{agentTypes.find((at) => at.id === assignForm.agentTypeId)?.name || 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#00293d] mb-1">
|
||||
Sort Order
|
||||
@@ -953,10 +1149,10 @@ export default function SectionDetailPage() {
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !assignForm.agentTypeId}
|
||||
disabled={isSubmitting || (assignModalType === 'assign' && !assignForm.agentTypeId)}
|
||||
className="px-4 py-2 bg-[#f5a623] text-white rounded-xl hover:bg-[#e09620] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? 'Assigning...' : 'Assign'}
|
||||
{isSubmitting ? 'Saving...' : assignModalType === 'editAssignment' ? 'Save Changes' : 'Assign'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user