227 lines
6.1 KiB
TypeScript
227 lines
6.1 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 { getFieldOptions } from '../../utils/options';
|
|
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].data;
|
|
const isArrayRelation = field.type === 'array';
|
|
const options = getFieldOptions(field, relationData);
|
|
const keyField = field.enumOption?.key ?? 'id';
|
|
|
|
// Normalize value: API returns whole objects on GET, but form uses key strings
|
|
const normalizedValue = (() => {
|
|
if (isArrayRelation && Array.isArray(value)) {
|
|
return value.map((v: any) => (v != null && typeof v === 'object' ? String(v[keyField] ?? '') : String(v)));
|
|
}
|
|
if (value != null && typeof value === 'object') {
|
|
return String(value[keyField] ?? '');
|
|
}
|
|
return value ?? (isArrayRelation ? [] : "");
|
|
})();
|
|
|
|
return (
|
|
<FormControl fullWidth>
|
|
<InputLabel shrink>{label}</InputLabel>
|
|
<Select
|
|
multiple={isArrayRelation}
|
|
value={normalizedValue}
|
|
label={label}
|
|
displayEmpty
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
renderValue={(selected: any) => {
|
|
if (isArrayRelation) {
|
|
return (selected as string[]).map(k => options.find(o => o.key === k)?.value ?? k).join(', ');
|
|
}
|
|
return options.find(o => o.key === selected)?.value ?? selected;
|
|
}}
|
|
>
|
|
{options.map((opt) => (
|
|
<MenuItem key={opt.key} value={opt.key}>
|
|
{opt.value}
|
|
</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) {
|
|
const options = getFieldOptions(field);
|
|
return (
|
|
<FormControl fullWidth>
|
|
<InputLabel>{label}</InputLabel>
|
|
<Select
|
|
value={value || ''}
|
|
label={label}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
>
|
|
{options.map((opt) => (
|
|
<MenuItem key={opt.key} value={opt.key}>
|
|
{opt.value}
|
|
</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
|
|
/>
|
|
);
|
|
}
|