271 lines
7.3 KiB
TypeScript
271 lines
7.3 KiB
TypeScript
import React, { useState } from "react";
|
|
import {
|
|
Button,
|
|
Chip,
|
|
Box,
|
|
Typography,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
IconButton,
|
|
Divider,
|
|
} from "@mui/material";
|
|
import AddIcon from "@mui/icons-material/Add";
|
|
import DeleteIcon from "@mui/icons-material/Delete";
|
|
import type { FieldConfig } from "../../../types";
|
|
import { useAppContext } from "../../../context/AppContext";
|
|
import { extractFields } from "../../../transformers/field-config";
|
|
import { FormFieldRenderer } from "../FormFieldRenderer";
|
|
|
|
interface JsonFieldProps {
|
|
field: FieldConfig;
|
|
value: any;
|
|
onChange: (val: any) => void;
|
|
}
|
|
|
|
export function JsonField({ field, value, onChange }: JsonFieldProps) {
|
|
const { schemas } = useAppContext();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const refSchema = field.refSchema ? schemas[field.refSchema] : null;
|
|
const subFields = refSchema
|
|
? extractFields(field.refSchema!, refSchema, schemas)
|
|
: [];
|
|
|
|
const [editValue, setEditValue] = useState<any>(null);
|
|
|
|
const handleOpen = () => {
|
|
setEditValue(initEditValue(value, field, schemas));
|
|
setOpen(true);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
onChange(editValue);
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setEditValue(null);
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleClear = () => {
|
|
onChange(null);
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleAddItem = () => {
|
|
setEditValue((prev: any[]) => [...(prev || []), buildDefaultShape(subFields, schemas)]);
|
|
};
|
|
|
|
const handleRemoveItem = (index: number) => {
|
|
setEditValue((prev: any[]) => prev.filter((_: any, i: number) => i !== index));
|
|
};
|
|
|
|
const handleItemFieldChange = (index: number, fieldName: string, val: any) => {
|
|
setEditValue((prev: any[]) => {
|
|
const next = [...prev];
|
|
next[index] = { ...next[index], [fieldName]: val };
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleFieldChange = (fieldName: string, val: any) => {
|
|
setEditValue((prev: any) => ({ ...prev, [fieldName]: val }));
|
|
};
|
|
|
|
if (!open) {
|
|
if (value === null || value === undefined) {
|
|
return (
|
|
<Button variant="outlined" onClick={handleOpen} size="small">
|
|
Set {field.label}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
if (field.isArray && Array.isArray(value)) {
|
|
if (value.length === 0) {
|
|
return (
|
|
<Button variant="outlined" onClick={handleOpen} size="small">
|
|
Set {field.label}
|
|
</Button>
|
|
);
|
|
}
|
|
return (
|
|
<Chip
|
|
label={`${value.length} item${value.length !== 1 ? "s" : ""}`}
|
|
size="small"
|
|
color="primary"
|
|
variant="outlined"
|
|
onClick={handleOpen}
|
|
onDelete={handleClear}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (typeof value === "object") {
|
|
const summary = field.inlineDisplayFormat
|
|
? applyInlineFormat(value, field.inlineDisplayFormat)
|
|
: Object.entries(value)
|
|
.filter(([, v]) => v != null)
|
|
.map(([k, v]) => `${k}: ${String(v)}`)
|
|
.join(" | ");
|
|
return (
|
|
<Chip
|
|
label={summary || field.label}
|
|
size="small"
|
|
color="primary"
|
|
variant="outlined"
|
|
onClick={handleOpen}
|
|
onDelete={handleClear}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog fullScreen open={open} onClose={handleCancel}>
|
|
<DialogTitle>{field.label}</DialogTitle>
|
|
<DialogContent dividers>
|
|
{field.isArray ? (
|
|
<ArrayEditor
|
|
items={editValue ?? []}
|
|
subFields={subFields}
|
|
onAddItem={handleAddItem}
|
|
onRemoveItem={handleRemoveItem}
|
|
onFieldChange={handleItemFieldChange}
|
|
schemas={schemas}
|
|
/>
|
|
) : (
|
|
<ObjectEditor
|
|
value={editValue}
|
|
subFields={subFields}
|
|
onFieldChange={handleFieldChange}
|
|
schemas={schemas}
|
|
/>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleClear} color="error">
|
|
Clear
|
|
</Button>
|
|
<Button onClick={handleCancel}>Cancel</Button>
|
|
<Button onClick={handleSave} variant="contained">
|
|
Save
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function ObjectEditor({
|
|
value,
|
|
subFields,
|
|
onFieldChange,
|
|
}: {
|
|
value: any;
|
|
subFields: FieldConfig[];
|
|
onFieldChange: (name: string, val: any) => void;
|
|
schemas: Record<string, any>;
|
|
}) {
|
|
return (
|
|
<Box>
|
|
{subFields.map((subField) => (
|
|
<Box key={subField.name} sx={{ mb: 2 }}>
|
|
<FormFieldRenderer
|
|
field={subField}
|
|
value={value?.[subField.name]}
|
|
onChange={(val) => onFieldChange(subField.name, val)}
|
|
/>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function ArrayEditor({
|
|
items,
|
|
subFields,
|
|
onAddItem,
|
|
onRemoveItem,
|
|
onFieldChange,
|
|
schemas,
|
|
}: {
|
|
items: any[];
|
|
subFields: FieldConfig[];
|
|
onAddItem: () => void;
|
|
onRemoveItem: (index: number) => void;
|
|
onFieldChange: (index: number, name: string, val: any) => void;
|
|
schemas: Record<string, any>;
|
|
}) {
|
|
return (
|
|
<Box>
|
|
{items.length === 0 && (
|
|
<Typography variant="body2" color="text.disabled" sx={{ mb: 2 }}>
|
|
No items added yet.
|
|
</Typography>
|
|
)}
|
|
{items.map((item, index) => (
|
|
<Box key={index} sx={{ mb: 2 }}>
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
|
|
<Typography variant="subtitle2" sx={{ flex: 1 }}>
|
|
Item {index + 1}
|
|
</Typography>
|
|
<IconButton size="small" color="error" onClick={() => onRemoveItem(index)}>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</Box>
|
|
<Box sx={{ pl: 2 }}>
|
|
{subFields.map((subField) => (
|
|
<Box key={subField.name} sx={{ mb: 2 }}>
|
|
<FormFieldRenderer
|
|
field={subField}
|
|
value={item?.[subField.name]}
|
|
onChange={(val) => onFieldChange(index, subField.name, val)}
|
|
/>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
<Divider sx={{ mt: 2 }} />
|
|
</Box>
|
|
))}
|
|
<Button startIcon={<AddIcon />} onClick={onAddItem} variant="outlined" size="small">
|
|
Add Item
|
|
</Button>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function buildDefaultShape(fields: FieldConfig[], schemas: Record<string, any>): Record<string, any> {
|
|
const shape: Record<string, any> = {};
|
|
for (const f of fields) {
|
|
if (f.refSchema && !f.fk) {
|
|
const refSchemaObj = schemas[f.refSchema!];
|
|
const nestedFields = refSchemaObj ? extractFields(f.refSchema!, refSchemaObj, schemas) : [];
|
|
shape[f.name] = f.isArray ? [] : buildDefaultShape(nestedFields, schemas);
|
|
} else {
|
|
shape[f.name] = null;
|
|
}
|
|
}
|
|
return shape;
|
|
}
|
|
|
|
function initEditValue(value: any, field: FieldConfig, schemas: Record<string, any>): any {
|
|
if (field.isArray) {
|
|
return value ? value.map((item: any) => ({ ...item })) : [];
|
|
}
|
|
if (value && typeof value === "object") {
|
|
return { ...value };
|
|
}
|
|
return buildDefaultShape(
|
|
field.refSchema ? extractFields(field.refSchema, schemas[field.refSchema], schemas) : [],
|
|
schemas
|
|
);
|
|
}
|
|
|
|
function applyInlineFormat(obj: any, format: string): string {
|
|
if (!obj || typeof obj !== "object") return String(obj ?? "");
|
|
return format.replace(/\{(\w+)\}/g, (_, key) => String(obj[key] ?? ""));
|
|
}
|