225 lines
5.9 KiB
TypeScript
225 lines
5.9 KiB
TypeScript
import * as React from 'react';
|
|
import {
|
|
TextField,
|
|
FormControl,
|
|
InputLabel,
|
|
Select,
|
|
MenuItem,
|
|
FormControlLabel,
|
|
Checkbox,
|
|
Typography,
|
|
Box,
|
|
Divider,
|
|
} from '@mui/material';
|
|
import { ResourceField } from '../../types/config';
|
|
import ImageUploadField from './ImageUploadField';
|
|
|
|
interface FormFieldProps {
|
|
name: string;
|
|
field: ResourceField;
|
|
value: any;
|
|
onChange: (val: any) => void;
|
|
disabled?: boolean;
|
|
uploadFile: (file: File) => Promise<string | null>;
|
|
uploading: boolean;
|
|
baseUrl: string;
|
|
relationDataMap?: Record<string, any[]>; // Map of relation name to data array
|
|
}
|
|
|
|
export default function FormField({
|
|
name,
|
|
field,
|
|
value,
|
|
onChange,
|
|
disabled,
|
|
uploadFile,
|
|
uploading,
|
|
baseUrl,
|
|
relationDataMap = {},
|
|
}: FormFieldProps) {
|
|
const label = field.label;
|
|
|
|
// 1. Recursive Rendering for Objects (Not Relations)
|
|
if (field.type === 'object' && field.schema && !field.relation) {
|
|
return (
|
|
<Box sx={{ ml: 2, mt: 2, p: 2, borderLeft: '2px solid #e0e0e0' }}>
|
|
<Typography variant="subtitle2" color="primary" gutterBottom>
|
|
{label}
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
{Object.entries(field.schema).map(([subKey, subField]) => (
|
|
<FormField
|
|
key={subKey}
|
|
name={`${name}.${subKey}`}
|
|
field={subField}
|
|
value={value?.[subKey]}
|
|
onChange={(newVal) => {
|
|
const updated = { ...(value || {}), [subKey]: newVal };
|
|
onChange(updated);
|
|
}}
|
|
disabled={disabled}
|
|
uploadFile={uploadFile}
|
|
uploading={uploading}
|
|
baseUrl={baseUrl}
|
|
relationDataMap={relationDataMap}
|
|
/>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// 2. Relation Handling (Select / Multi-Select)
|
|
if (field.relation && relationDataMap[field.relation]) {
|
|
const relationData = relationDataMap[field.relation];
|
|
const isArrayRelation = field.type === 'array';
|
|
|
|
// Determine how to display the related item
|
|
const getOptionLabel = (option: any) => {
|
|
if (!option) return "";
|
|
if (field.displayField && option[field.displayField]) return option[field.displayField];
|
|
// Standard naming fields
|
|
return option.name || option.title || option.label || option.id || JSON.stringify(option);
|
|
};
|
|
|
|
const getOptionValue = (option: any) => {
|
|
// Return the whole object to maintain identity
|
|
return option;
|
|
};
|
|
|
|
return (
|
|
<FormControl fullWidth>
|
|
<InputLabel shrink>{label}</InputLabel>
|
|
<Select
|
|
multiple={isArrayRelation}
|
|
value={value || (isArrayRelation ? [] : "")}
|
|
label={label}
|
|
displayEmpty
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
renderValue={(selected: any) => {
|
|
if (isArrayRelation) {
|
|
return (selected as any[]).map(getOptionLabel).join(', ');
|
|
}
|
|
return getOptionLabel(selected);
|
|
}}
|
|
>
|
|
{relationData.map((option) => (
|
|
<MenuItem key={option.id || JSON.stringify(option)} value={getOptionValue(option)}>
|
|
{getOptionLabel(option)}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
);
|
|
}
|
|
|
|
// 3. Image Handling
|
|
if (field.type === 'image') {
|
|
return (
|
|
<ImageUploadField
|
|
label={label}
|
|
value={value}
|
|
onUpload={async (file: any) => {
|
|
const url = await uploadFile(file);
|
|
if (url) onChange(url);
|
|
}}
|
|
uploading={uploading}
|
|
baseUrl={baseUrl}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 4. Boolean Handling
|
|
if (field.type === 'boolean') {
|
|
return (
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={!!value}
|
|
onChange={(e) => onChange(e.target.checked)}
|
|
disabled={disabled}
|
|
/>
|
|
}
|
|
label={label}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 5. Enum Handling
|
|
if (field.type === 'enum' && field.options) {
|
|
return (
|
|
<FormControl fullWidth>
|
|
<InputLabel>{label}</InputLabel>
|
|
<Select
|
|
value={value || ''}
|
|
label={label}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
>
|
|
{field.options.map((opt: string) => (
|
|
<MenuItem key={opt} value={opt}>
|
|
{opt}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
);
|
|
}
|
|
|
|
// 6. Common Text Fields
|
|
if (field.type === 'datetime' || field.type === 'date') {
|
|
return (
|
|
<TextField
|
|
fullWidth
|
|
label={label}
|
|
type={field.type === 'datetime' ? "datetime-local" : "date"}
|
|
InputLabelProps={{ shrink: true }}
|
|
value={value ? new Date(value).toISOString().slice(0, field.type === 'datetime' ? 16 : 10) : ''}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
required={field.required}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (field.type === 'markdown' || field.type === 'string') {
|
|
return (
|
|
<TextField
|
|
fullWidth
|
|
label={label}
|
|
value={value || ''}
|
|
multiline={field.type === 'markdown'}
|
|
rows={field.type === 'markdown' ? 4 : 1}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
required={field.required}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (field.type === 'number') {
|
|
return (
|
|
<TextField
|
|
fullWidth
|
|
label={label}
|
|
type="number"
|
|
value={value === undefined || value === null ? '' : value}
|
|
onChange={(e) => onChange(e.target.value === '' ? '' : Number(e.target.value))}
|
|
disabled={disabled}
|
|
required={field.required}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TextField
|
|
fullWidth
|
|
label={label}
|
|
value={typeof value === 'object' ? JSON.stringify(value) : value || ''}
|
|
disabled
|
|
/>
|
|
);
|
|
}
|