411 lines
14 KiB
TypeScript
411 lines
14 KiB
TypeScript
import { useEffect } from 'react';
|
|
|
|
import {
|
|
CustomModal,
|
|
CustomInput,
|
|
CustomDropdown,
|
|
CustomTextArea,
|
|
CustomSwitch,
|
|
CustomButton,
|
|
CustomAlertBanner,
|
|
} from '../../../../components/custom';
|
|
import type {
|
|
FieldDefinition,
|
|
FieldDefinitionFormData,
|
|
FieldType,
|
|
FieldWidth,
|
|
} from '../ActionBuilderTypes';
|
|
|
|
const FIELD_TYPE_OPTIONS: { label: string; value: FieldType }[] = [
|
|
{ label: 'Text Box (Single Line)', value: 'textbox' },
|
|
{ label: 'Text Area (Multi Line)', value: 'textarea' },
|
|
{ label: 'Email Address', value: 'email' },
|
|
{ label: 'Phone Number', value: 'phone' },
|
|
{ label: 'URL / Link', value: 'url' },
|
|
{ label: 'Number (Integer)', value: 'number' },
|
|
{ label: 'Decimal / Price', value: 'decimal' },
|
|
{ label: 'Percentage (%)', value: 'percentage' },
|
|
{ label: 'Currency (Amount + Currency)', value: 'currency' },
|
|
{ label: 'Dropdown Selection', value: 'dropdown' },
|
|
{ label: 'Radio Selection', value: 'radio' },
|
|
{ label: 'Multi-Select Checkboxes', value: 'multi_select' },
|
|
{ label: 'Single Checkbox', value: 'checkbox' },
|
|
{ label: 'Toggle Switch', value: 'switch' },
|
|
{ label: 'Date Picker', value: 'date' },
|
|
{ label: 'Date & Time Picker', value: 'datetime' },
|
|
{ label: 'Time Picker', value: 'time' },
|
|
{ label: 'Color Picker', value: 'color' },
|
|
{ label: 'Formula (Read-only Computed)', value: 'formula' },
|
|
];
|
|
|
|
const WIDTH_OPTIONS: { label: string; value: FieldWidth }[] = [
|
|
{ label: 'Full Width (12/12)', value: 'full' },
|
|
{ label: 'Half Width (6/12)', value: 'half' },
|
|
{ label: 'One-Third Width (4/12)', value: 'third' },
|
|
{ label: 'Two-Thirds Width (8/12)', value: 'two_thirds' },
|
|
];
|
|
|
|
interface FieldDefinitionFormModalProps {
|
|
isOpen: boolean;
|
|
editingField: FieldDefinition | null;
|
|
fields: FieldDefinition[];
|
|
masterLookupOptions: { label: string; value: string }[];
|
|
formData: FieldDefinitionFormData;
|
|
setFormData: React.Dispatch<React.SetStateAction<FieldDefinitionFormData>>;
|
|
error: string | null;
|
|
setError?: (err: string | null) => void;
|
|
onClose: () => void;
|
|
onSubmit: (e: React.FormEvent) => void;
|
|
}
|
|
|
|
const VALIDATION_RELEVANT_TYPES: FieldType[] = [
|
|
'textbox',
|
|
'textarea',
|
|
'email',
|
|
'phone',
|
|
'url',
|
|
'number',
|
|
'decimal',
|
|
'percentage',
|
|
'currency',
|
|
];
|
|
|
|
export function FieldDefinitionFormModal({
|
|
isOpen,
|
|
editingField,
|
|
fields,
|
|
masterLookupOptions,
|
|
formData,
|
|
setFormData,
|
|
error,
|
|
setError,
|
|
onClose,
|
|
onSubmit,
|
|
}: FieldDefinitionFormModalProps) {
|
|
useEffect(() => {
|
|
if (!error || !setError) return;
|
|
if (error === 'Field Name is required.' && formData.fieldName.trim()) {
|
|
setError(null);
|
|
} else if (error === 'Field Code is required.' && formData.fieldCode.trim()) {
|
|
setError(null);
|
|
} else if (
|
|
error === 'Master Data Lookup Source is required for Dropdown or Multi-Select control types.' &&
|
|
formData.lookupSource?.trim()
|
|
) {
|
|
setError(null);
|
|
}
|
|
}, [formData.fieldName, formData.fieldCode, formData.lookupSource, error, setError]);
|
|
|
|
const dependentOptions = [
|
|
{ label: 'None (No Dependency)', value: '' },
|
|
...fields
|
|
.filter((f) => !editingField || f.id !== editingField.id)
|
|
.map((f) => ({
|
|
label: `${f.fieldName} (${f.fieldCode})`,
|
|
value: f.fieldCode,
|
|
})),
|
|
];
|
|
|
|
const isNumericType = ['number', 'decimal', 'percentage', 'currency'].includes(formData.fieldType);
|
|
const minLabel = isNumericType ? 'Min Value' : 'Min Length';
|
|
const maxLabel = isNumericType ? 'Max Value' : 'Max Length';
|
|
const minPlaceholder = isNumericType ? 'e.g. 0' : 'e.g. 3';
|
|
const maxPlaceholder = isNumericType ? 'e.g. 1000' : 'e.g. 250';
|
|
|
|
return (
|
|
<CustomModal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title={editingField ? `Edit Field: ${editingField.fieldName}` : 'Create Field Definition'}
|
|
description="Configure field code, label, control type, section layout, validation rules, and visibility."
|
|
size="lg"
|
|
>
|
|
{error && (
|
|
<CustomAlertBanner
|
|
message={error}
|
|
type="error"
|
|
onClose={() => setError?.(null)}
|
|
autoClose={false}
|
|
/>
|
|
)}
|
|
|
|
<form onSubmit={onSubmit} noValidate className="space-y-4 pt-1">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<CustomInput
|
|
label="Field Label / Name"
|
|
required
|
|
value={formData.fieldName}
|
|
onChange={(e) => {
|
|
const val = e.target.value;
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
fieldName: val,
|
|
fieldCode: editingField ? prev.fieldCode : val.toLowerCase().replace(/[^a-z0-9_]+/g, '_'),
|
|
}));
|
|
if (setError && val.trim()) {
|
|
setError(null);
|
|
}
|
|
}}
|
|
placeholder="e.g. Field Name"
|
|
/>
|
|
|
|
<CustomInput
|
|
label="Field Code (JSON Key)"
|
|
required
|
|
value={formData.fieldCode}
|
|
onChange={(e) => {
|
|
const val = e.target.value;
|
|
setFormData((prev) => ({ ...prev, fieldCode: val }));
|
|
if (setError && val.trim()) {
|
|
setError(null);
|
|
}
|
|
}}
|
|
placeholder="e.g. field_code"
|
|
/>
|
|
|
|
<CustomDropdown
|
|
label="Control Type"
|
|
required
|
|
options={FIELD_TYPE_OPTIONS}
|
|
value={formData.fieldType}
|
|
onChange={(val) => {
|
|
const newType = val as FieldType;
|
|
const isLookupType = newType === 'dropdown' || newType === 'multi_select';
|
|
const isValRelevant = VALIDATION_RELEVANT_TYPES.includes(newType);
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
fieldType: newType,
|
|
lookupSource: isLookupType ? prev.lookupSource : undefined,
|
|
validationJson: isValRelevant ? prev.validationJson : undefined,
|
|
}));
|
|
if (setError && !isLookupType) {
|
|
setError(null);
|
|
}
|
|
}}
|
|
placeholder="Select Control Type..."
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
className={`grid grid-cols-1 ${formData.fieldType === 'dropdown' || formData.fieldType === 'multi_select' || formData.fieldType === 'currency'
|
|
? 'md:grid-cols-3'
|
|
: 'md:grid-cols-2'
|
|
} gap-4`}
|
|
>
|
|
{(formData.fieldType === 'dropdown' || formData.fieldType === 'multi_select' || formData.fieldType === 'currency') && (
|
|
<CustomDropdown
|
|
label="Master Data Lookup Source"
|
|
required
|
|
options={masterLookupOptions}
|
|
value={formData.lookupSource || ''}
|
|
onChange={(val) => {
|
|
setFormData((prev) => ({ ...prev, lookupSource: val }));
|
|
if (setError && val?.trim()) {
|
|
setError(null);
|
|
}
|
|
}}
|
|
placeholder="Select Lookup Source..."
|
|
/>
|
|
)}
|
|
|
|
<CustomDropdown
|
|
label="Form Width Layout"
|
|
options={WIDTH_OPTIONS}
|
|
value={formData.width}
|
|
onChange={(val) => setFormData((prev) => ({ ...prev, width: val as FieldWidth }))}
|
|
/>
|
|
|
|
<CustomInput
|
|
label="Section Group Name"
|
|
value={formData.section || ''}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, section: e.target.value }))}
|
|
placeholder="e.g. General Information"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<CustomInput
|
|
label="Placeholder"
|
|
value={formData.placeholder || ''}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, placeholder: e.target.value }))}
|
|
placeholder="e.g. Enter value..."
|
|
/>
|
|
|
|
<CustomInput
|
|
label="Default Value"
|
|
value={formData.defaultValue || ''}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, defaultValue: e.target.value }))}
|
|
placeholder="e.g. Default"
|
|
/>
|
|
|
|
<CustomInput
|
|
label="Display Order"
|
|
type="number"
|
|
value={String(formData.displayOrder)}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, displayOrder: parseInt(e.target.value) || 1 }))}
|
|
/>
|
|
</div>
|
|
|
|
<CustomTextArea
|
|
label="Help Text / Instructions"
|
|
value={formData.helpText || ''}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, helpText: e.target.value }))}
|
|
placeholder="e.g. Instructions for end users filling this field"
|
|
rows={2}
|
|
/>
|
|
|
|
{/* Validation JSON Rules - Only shown for relevant field types */}
|
|
{VALIDATION_RELEVANT_TYPES.includes(formData.fieldType) && (
|
|
<div className="p-4 bg-slate-50 border border-slate-200 rounded-[14px] space-y-3">
|
|
<h5 className="text-[13px] font-bold text-slate-800">Validation Rules (validation_json)</h5>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<CustomInput
|
|
label={minLabel}
|
|
type="number"
|
|
value={formData.validationJson?.min !== undefined ? String(formData.validationJson.min) : ''}
|
|
onChange={(e) =>
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
validationJson: {
|
|
...prev.validationJson,
|
|
min: e.target.value !== '' ? Number(e.target.value) : undefined,
|
|
},
|
|
}))
|
|
}
|
|
placeholder={minPlaceholder}
|
|
/>
|
|
|
|
<CustomInput
|
|
label={maxLabel}
|
|
type="number"
|
|
value={formData.validationJson?.max !== undefined ? String(formData.validationJson.max) : ''}
|
|
onChange={(e) =>
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
validationJson: {
|
|
...prev.validationJson,
|
|
max: e.target.value !== '' ? Number(e.target.value) : undefined,
|
|
},
|
|
}))
|
|
}
|
|
placeholder={maxPlaceholder}
|
|
/>
|
|
|
|
<CustomInput
|
|
label="Regex Pattern"
|
|
value={formData.validationJson?.regex || ''}
|
|
onChange={(e) =>
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
validationJson: {
|
|
...prev.validationJson,
|
|
regex: e.target.value,
|
|
},
|
|
}))
|
|
}
|
|
placeholder="e.g. ^[A-Z0-9]+$"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Conditional Visibility */}
|
|
<div className="p-4 bg-amber-50/60 border border-amber-200 rounded-[14px] space-y-3">
|
|
<h5 className="text-[13px] font-bold text-amber-900">Conditional Visibility (visibility_condition_json)</h5>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<CustomDropdown
|
|
label="Depends on Field Code"
|
|
options={dependentOptions}
|
|
value={formData.visibilityConditionJson?.field || ''}
|
|
onChange={(val) =>
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
visibilityConditionJson: {
|
|
...(prev.visibilityConditionJson || { operator: 'equals', value: '' }),
|
|
field: val,
|
|
},
|
|
}))
|
|
}
|
|
placeholder="Select dependent field..."
|
|
/>
|
|
|
|
<CustomDropdown
|
|
label="Operator"
|
|
options={[
|
|
{ label: 'Equals', value: 'equals' },
|
|
{ label: 'Not Equals', value: 'not_equals' },
|
|
{ label: 'Contains', value: 'contains' },
|
|
]}
|
|
value={formData.visibilityConditionJson?.operator || 'equals'}
|
|
onChange={(val) =>
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
visibilityConditionJson: {
|
|
...(prev.visibilityConditionJson || { field: '', value: '' }),
|
|
operator: val as any,
|
|
},
|
|
}))
|
|
}
|
|
/>
|
|
|
|
<CustomInput
|
|
label="Target Value"
|
|
value={formData.visibilityConditionJson?.value || ''}
|
|
onChange={(e) =>
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
visibilityConditionJson: {
|
|
...(prev.visibilityConditionJson || { field: '', operator: 'equals' }),
|
|
value: e.target.value,
|
|
},
|
|
}))
|
|
}
|
|
placeholder="e.g. DGYRHTFUJKYHUSDERTYUIK"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-4 border-t border-gray-100 mt-6">
|
|
<div className="flex items-center gap-6">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-[13px] font-semibold text-slate-700">Required Field</span>
|
|
<CustomSwitch
|
|
checked={formData.isRequired}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, isRequired: e.target.checked }))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-[13px] font-semibold text-slate-700">Active Status</span>
|
|
<CustomSwitch
|
|
checked={formData.isActive}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, isActive: e.target.checked }))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<CustomButton
|
|
|
|
variant="outlined"
|
|
type="button"
|
|
onClick={onClose}
|
|
className="!border-gray-300 !text-gray-600 font-semibold px-5"
|
|
>
|
|
Cancel
|
|
</CustomButton>
|
|
|
|
<CustomButton
|
|
variant="primary"
|
|
type="submit"
|
|
className="!bg-[#1E7D5C] hover:!bg-[#17664B] font-semibold px-6 shadow-sm"
|
|
>
|
|
{editingField ? 'Save Changes' : 'Create Field Definition'}
|
|
</CustomButton>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</CustomModal>
|
|
);
|
|
}
|