feat: Implement a comprehensive custom fields system with backend management and frontend rendering capabilities.
This commit is contained in:
195
frontend/src/components/customFields/CustomFieldsRenderer.tsx
Normal file
195
frontend/src/components/customFields/CustomFieldsRenderer.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
TextField,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Box
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { CustomFieldDefinition, CustomFieldType, CustomFieldValues } from '../../types/customFields';
|
||||
import { customFieldService } from '../../services/customFieldService';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface Props {
|
||||
entityName: string;
|
||||
values: CustomFieldValues;
|
||||
onChange: (fieldName: string, value: any) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const CustomFieldsRenderer: React.FC<Props> = ({ entityName, values, onChange, readOnly = false }) => {
|
||||
const [definitions, setDefinitions] = useState<CustomFieldDefinition[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadDefinitions = async () => {
|
||||
try {
|
||||
const defs = await customFieldService.getByEntity(entityName);
|
||||
setDefinitions(defs);
|
||||
} catch (error) {
|
||||
console.error("Failed to load custom fields", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadDefinitions();
|
||||
}, [entityName]);
|
||||
|
||||
if (loading) return null;
|
||||
if (definitions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2, mb: 2, p: 2, border: '1px dashed #ccc', borderRadius: 1 }}>
|
||||
<Typography variant="subtitle1" gutterBottom color="primary">
|
||||
Campi Personalizzati
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap" gap={2}>
|
||||
{definitions.map(def => (
|
||||
<Box key={def.id} flexBasis={{ xs: '100%', sm: '48%', md: '32%' }} flexGrow={1}>
|
||||
<FieldRenderer
|
||||
definition={def}
|
||||
value={values?.[def.fieldName]}
|
||||
onChange={(val) => onChange(def.fieldName, val)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldRenderer: React.FC<{
|
||||
definition: CustomFieldDefinition;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
readOnly: boolean;
|
||||
}> = ({ definition, value, onChange, readOnly }) => {
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
switch (definition.type) {
|
||||
case CustomFieldType.Text:
|
||||
case CustomFieldType.Email:
|
||||
case CustomFieldType.Url:
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={definition.label}
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
required={definition.isRequired}
|
||||
disabled={readOnly}
|
||||
placeholder={definition.placeholder}
|
||||
helperText={definition.description}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
case CustomFieldType.Number:
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label={definition.label}
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
required={definition.isRequired}
|
||||
disabled={readOnly}
|
||||
helperText={definition.description}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
case CustomFieldType.TextArea:
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
label={definition.label}
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
required={definition.isRequired}
|
||||
disabled={readOnly}
|
||||
helperText={definition.description}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
case CustomFieldType.Boolean:
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={!!value}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
}
|
||||
label={definition.label}
|
||||
/>
|
||||
);
|
||||
|
||||
case CustomFieldType.Date:
|
||||
return (
|
||||
<DatePicker
|
||||
label={definition.label}
|
||||
value={value ? dayjs(value) : null}
|
||||
onChange={(newValue) => onChange(newValue ? newValue.toISOString() : null)}
|
||||
disabled={readOnly}
|
||||
slotProps={{ textField: { fullWidth: true, required: definition.isRequired, helperText: definition.description, size: 'small' } }}
|
||||
/>
|
||||
);
|
||||
|
||||
case CustomFieldType.Select:
|
||||
let options: string[] = [];
|
||||
try {
|
||||
options = definition.optionsJson ? JSON.parse(definition.optionsJson) : [];
|
||||
} catch (e) {
|
||||
console.error("Invalid options JSON", e);
|
||||
}
|
||||
return (
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
label={definition.label}
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
required={definition.isRequired}
|
||||
disabled={readOnly}
|
||||
helperText={definition.description}
|
||||
size="small"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>
|
||||
{opt}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
|
||||
case CustomFieldType.Color:
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
type="color"
|
||||
label={definition.label}
|
||||
value={value || '#000000'}
|
||||
onChange={handleChange}
|
||||
required={definition.isRequired}
|
||||
disabled={readOnly}
|
||||
helperText={definition.description}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user