new react-openapi
This commit is contained in:
@@ -7,9 +7,10 @@ interface DetailFieldProps {
|
||||
field: FieldConfig;
|
||||
value: any;
|
||||
displayFormat?: string;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
export function DetailFieldRenderer({ field, value, displayFormat }: DetailFieldProps) {
|
||||
export function DetailFieldRenderer({ field, value, displayFormat, basePath }: DetailFieldProps) {
|
||||
if (field.hidden?.detail) return null;
|
||||
|
||||
return (
|
||||
@@ -17,7 +18,7 @@ export function DetailFieldRenderer({ field, value, displayFormat }: DetailField
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ mb: 0.25, display: "block" }}>
|
||||
{field.label}
|
||||
</Typography>
|
||||
<ListCellRenderer field={field} value={value} displayFormat={displayFormat} />
|
||||
<ListCellRenderer field={field} value={value} displayFormat={displayFormat} basePath={basePath} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { FkSelectField } from "./renderers/FkSelectField";
|
||||
import { FkMultiSelectField } from "./renderers/FkMultiSelectField";
|
||||
import { ImageField } from "./renderers/ImageField";
|
||||
import { JsonField } from "./renderers/JsonField";
|
||||
import { DiscriminatorField } from "./renderers/DiscriminatorField";
|
||||
|
||||
interface FormFieldProps {
|
||||
field: FieldConfig;
|
||||
@@ -106,6 +107,17 @@ export function FormFieldRenderer({ field, value, onChange, error, fkOptions, fk
|
||||
);
|
||||
}
|
||||
|
||||
if (field.oneOfOptions) {
|
||||
return (
|
||||
<DiscriminatorField
|
||||
field={field}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.refSchema && !field.fk) {
|
||||
return (
|
||||
<JsonField
|
||||
|
||||
@@ -1,24 +1,71 @@
|
||||
import React from "react";
|
||||
import { Box, Typography, Chip, Avatar } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Box, Typography, Chip, Avatar, Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid } from "@mui/material";
|
||||
import type { FieldConfig } from "../../types";
|
||||
import { applyDisplayFormat } from "./utils";
|
||||
import { InlineRefField } from "./renderers/InlineRefField";
|
||||
import { extractFields } from "../../transformers/field-config";
|
||||
import { useAppContext } from "../../context/AppContext";
|
||||
|
||||
interface ListCellProps {
|
||||
field: FieldConfig;
|
||||
value: any;
|
||||
displayFormat?: string;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
export function ListCellRenderer({ field, value, displayFormat }: ListCellProps) {
|
||||
export function ListCellRenderer({ field, value, displayFormat, basePath }: ListCellProps) {
|
||||
const navigate = useNavigate();
|
||||
const { schemas } = useAppContext();
|
||||
const [inlineItem, setInlineItem] = useState<any>(null);
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
return <Typography variant="body2" color="text.disabled">—</Typography>;
|
||||
}
|
||||
|
||||
const handleFkClick = (e: React.MouseEvent, fkValue: any) => {
|
||||
e.stopPropagation();
|
||||
if (!basePath || !field.fk || typeof fkValue !== "object") return;
|
||||
const id = fkValue?.id;
|
||||
if (id != null) {
|
||||
navigate(`${basePath}/${field.fk.resource}/${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (field.refSchema && !field.fk && !field.isArray && typeof value === "object") {
|
||||
return <InlineRefField field={field} value={value} displayFormat={displayFormat} />;
|
||||
return <InlineRefField field={field} value={value} />;
|
||||
}
|
||||
|
||||
const renderInlineItemFields = (itemValue: any) => {
|
||||
const schema = field.refSchema ? schemas[field.refSchema] : undefined;
|
||||
let fields: FieldConfig[] = [];
|
||||
if (field.oneOfOptions && field.discriminatorProperty) {
|
||||
const opt = field.oneOfOptions.find((o) => o.value === itemValue?.[field.discriminatorProperty!]);
|
||||
if (opt) fields = opt.fields;
|
||||
} else if (schema && field.refSchema) {
|
||||
fields = extractFields(field.refSchema, schema, schemas);
|
||||
}
|
||||
return (
|
||||
<Grid container spacing={2} sx={{ mt: 0.5 }}>
|
||||
{fields.map((sf) => {
|
||||
const fv = itemValue?.[sf.name];
|
||||
return (
|
||||
<Grid key={sf.name} item xs={12} sm={6}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ mb: 0.25, display: "block" }}>
|
||||
{sf.label}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{fv == null ? "—" : typeof fv === "object" ? (sf.inlineDisplayFormat ? applyDisplayFormat(fv, sf.inlineDisplayFormat) : JSON.stringify(fv)) : String(fv)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
if (field.isArray && Array.isArray(value) && field.refSchema && !field.fk) {
|
||||
if (value.length === 0) {
|
||||
return <Typography variant="body2" color="text.disabled">—</Typography>;
|
||||
@@ -29,14 +76,40 @@ export function ListCellRenderer({ field, value, displayFormat }: ListCellProps)
|
||||
const label = typeof item === "object"
|
||||
? applyDisplayFormat(item, displayFormat ?? "")
|
||||
: String(item);
|
||||
return <Chip key={i} label={label} size="small" variant="outlined" />;
|
||||
return (
|
||||
<Chip
|
||||
key={i}
|
||||
label={label}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(e) => { e.stopPropagation(); setInlineItem(item); }}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Dialog open={!!inlineItem} onClose={() => setInlineItem(null)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{field.label}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{inlineItem && renderInlineItemFields(inlineItem)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setInlineItem(null)}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.fk && typeof value === "object" && !field.isArray) {
|
||||
return <Typography variant="body2">{applyDisplayFormat(value, displayFormat ?? "")}</Typography>;
|
||||
return (
|
||||
<Chip
|
||||
label={applyDisplayFormat(value, displayFormat ?? "")}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(e) => handleFkClick(e, value)}
|
||||
sx={basePath ? { cursor: "pointer" } : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.isArray && Array.isArray(value) && field.fk) {
|
||||
@@ -44,7 +117,16 @@ export function ListCellRenderer({ field, value, displayFormat }: ListCellProps)
|
||||
<Box sx={{ display: "flex", gap: 0.5, flexWrap: "wrap" }}>
|
||||
{value.map((item: any, i: number) => {
|
||||
const label = typeof item === "object" ? applyDisplayFormat(item, displayFormat ?? "") : String(item);
|
||||
return <Chip key={i} label={label} size="small" variant="outlined" />;
|
||||
return (
|
||||
<Chip
|
||||
key={i}
|
||||
label={label}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(e) => handleFkClick(e, item)}
|
||||
sx={basePath ? { cursor: "pointer" } : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -12,16 +12,20 @@ interface Props {
|
||||
export function DateField({ field, value, onChange, error }: Props) {
|
||||
const inputType = field.format === "date" ? "date" : "datetime-local";
|
||||
|
||||
const normalized = field.format === "date-time" && typeof value === "string"
|
||||
? value.replace(/\.\d+Z$/, "").replace(/Z$/, "")
|
||||
: value;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={field.label}
|
||||
type={inputType}
|
||||
value={value ?? ""}
|
||||
value={normalized ?? ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
error={!!error}
|
||||
helperText={error ?? field.description}
|
||||
placeholder={field.description}
|
||||
helperText={error || field.description || undefined}
|
||||
placeholder={field.description || undefined}
|
||||
size="small"
|
||||
disabled={field.readOnly}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Box, FormControl, InputLabel, Select, MenuItem, Typography } from "@mui/material";
|
||||
import type { FieldConfig } from "../../../types";
|
||||
import { FormFieldRenderer } from "../FormFieldRenderer";
|
||||
|
||||
interface Props {
|
||||
field: FieldConfig;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function DiscriminatorField({ field, value, onChange, error }: Props) {
|
||||
const options = field.oneOfOptions ?? [];
|
||||
const discProp = field.discriminatorProperty ?? "type";
|
||||
const currentType = value?.[discProp] ?? "";
|
||||
|
||||
const handleTypeChange = useCallback((e: any) => {
|
||||
const newType = e.target.value;
|
||||
const option = options.find((o) => o.value === newType);
|
||||
const newValue: Record<string, any> = { [discProp]: newType };
|
||||
if (option) {
|
||||
for (const f of option.fields) {
|
||||
newValue[f.name] = f.enumValues?.[0] ?? f.type === "number" ? 0 : f.type === "integer" ? 0 : "";
|
||||
}
|
||||
}
|
||||
onChange(newValue);
|
||||
}, [discProp, onChange, options]);
|
||||
|
||||
const handleFieldChange = useCallback((fieldName: string, fieldValue: any) => {
|
||||
onChange({ ...(value ?? {}), [fieldName]: fieldValue });
|
||||
}, [onChange, value]);
|
||||
|
||||
const activeFields = options.find((o) => o.value === currentType)?.fields ?? [];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<FormControl fullWidth size="small" sx={{ mb: 2 }}>
|
||||
<InputLabel>{field.label}</InputLabel>
|
||||
<Select
|
||||
value={currentType}
|
||||
label={field.label}
|
||||
onChange={handleTypeChange}
|
||||
error={!!error}
|
||||
>
|
||||
<MenuItem value="" disabled>Select type</MenuItem>
|
||||
{options.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>{opt.label}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{currentType && activeFields.length > 0 && (
|
||||
<Box sx={{ pl: 2, borderLeft: "2px solid", borderColor: "divider" }}>
|
||||
{activeFields.map((f) => (
|
||||
<FormFieldRenderer
|
||||
key={f.name}
|
||||
field={f}
|
||||
value={value?.[f.name]}
|
||||
onChange={(v) => handleFieldChange(f.name, v)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { TextField, Autocomplete } from "@mui/material";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { TextField, Autocomplete, Chip, Box } from "@mui/material";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
import type { FieldConfig } from "../../../types";
|
||||
|
||||
interface Props {
|
||||
@@ -12,20 +13,86 @@ interface Props {
|
||||
}
|
||||
|
||||
export function FkMultiSelectField({ field, value, onChange, fkOptions, fkLoading, onOpen }: Props) {
|
||||
console.log(`[FkMultiSelectField] render field="${field.name}" fkOptions=${fkOptions ? `${fkOptions.length} items` : "undefined"} fkLoading=${fkLoading} value=${JSON.stringify(value)}`);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [frozenValue, setFrozenValue] = useState<any[]>([]);
|
||||
|
||||
const handleOpen = () => {
|
||||
onOpen?.();
|
||||
setFrozenValue(value ?? []);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const sortedOptions = useMemo(() => {
|
||||
const sel = new Set(frozenValue);
|
||||
const picked: { value: any; label: string }[] = [];
|
||||
const rest: { value: any; label: string }[] = [];
|
||||
for (const opt of fkOptions ?? []) {
|
||||
(sel.has(opt.value) ? picked : rest).push(opt);
|
||||
}
|
||||
return [...picked, ...rest];
|
||||
}, [fkOptions, frozenValue]);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={fkOptions ?? []}
|
||||
disableCloseOnSelect
|
||||
open={open}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
options={sortedOptions}
|
||||
getOptionLabel={(o) => o.label}
|
||||
value={fkOptions?.filter((o) => (value ?? []).includes(o.value)) ?? []}
|
||||
onChange={(_, newVal) => onChange(newVal.map((v) => v.value))}
|
||||
onOpen={() => onOpen?.()}
|
||||
loading={fkLoading}
|
||||
renderOption={(props, option, { selected }) => (
|
||||
<li {...props}>
|
||||
{selected ? (
|
||||
<DoneIcon sx={{ fontSize: 14, mr: 1, color: "primary.main" }} />
|
||||
) : (
|
||||
<Box sx={{ width: 22, mr: 1 }} />
|
||||
)}
|
||||
{option.label}
|
||||
</li>
|
||||
)}
|
||||
renderTags={(tagValue, getTagProps) => {
|
||||
const maxChips = 1;
|
||||
return (
|
||||
<>
|
||||
{tagValue.slice(0, maxChips).map((tag, index) => {
|
||||
const { key, ...tagProps } = getTagProps({ index });
|
||||
return (
|
||||
<Chip
|
||||
key={key}
|
||||
{...tagProps}
|
||||
label={tag.label.length > 10 ? `${tag.label.slice(0, 8)}..` : tag.label}
|
||||
size="small"
|
||||
onClick={open ? handleClose : handleOpen}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{tagValue.length > maxChips && (
|
||||
<Chip
|
||||
label={`+${tagValue.length - maxChips}`}
|
||||
size="small"
|
||||
onClick={open ? handleClose : handleOpen}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label={field.label} helperText={field.description} size="small" />
|
||||
<TextField {...params} label={field.label} helperText={field.description || undefined} size="small" />
|
||||
)}
|
||||
size="small"
|
||||
sx={{
|
||||
"& .MuiAutocomplete-popupIndicator, & .MuiAutocomplete-clearIndicator": { width: 20, height: 20, fontSize: 16 },
|
||||
}}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export function ImageField({ field, value, onChange, id, uploadUrl }: Props) {
|
||||
<input type="file" hidden accept="image/*" onChange={handleUpload} />
|
||||
</Button>
|
||||
)}
|
||||
<FormHelperText>{field.description}</FormHelperText>
|
||||
{field.description && <FormHelperText>{field.description}</FormHelperText>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,83 @@
|
||||
import React from "react";
|
||||
import { Box, Typography, Chip } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import { Box, Typography, Chip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid } from "@mui/material";
|
||||
import type { FieldConfig } from "../../../types";
|
||||
import { applyDisplayFormat } from "../utils";
|
||||
import { extractFields } from "../../../transformers/field-config";
|
||||
import { useAppContext } from "../../../context/AppContext";
|
||||
|
||||
|
||||
interface Props {
|
||||
field: FieldConfig;
|
||||
value: any;
|
||||
displayFormat?: string;
|
||||
}
|
||||
|
||||
export function InlineRefField({ field, value, displayFormat }: Props) {
|
||||
const displayLabels: Record<string, string> = {
|
||||
basic: "Basic",
|
||||
heart_rate: "Heart Rate",
|
||||
dental: "Dental",
|
||||
vaccine: "Vaccine",
|
||||
preop: "PreOp",
|
||||
surgery: "Surgery",
|
||||
};
|
||||
|
||||
export function InlineRefField({ field, value }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { schemas } = useAppContext();
|
||||
|
||||
if (!value || typeof value !== "object") {
|
||||
return <Typography variant="body2" color="text.disabled">—</Typography>;
|
||||
}
|
||||
|
||||
if (displayFormat) {
|
||||
return <Typography variant="body2">{applyDisplayFormat(value, displayFormat)}</Typography>;
|
||||
}
|
||||
const discProp = field.discriminatorProperty;
|
||||
const discValue = discProp ? value[discProp] : undefined;
|
||||
const discChip = discValue ? displayLabels[discValue] ?? discValue : undefined;
|
||||
const tooltip = field.inlineDisplayFormat
|
||||
? applyDisplayFormat(value, field.inlineDisplayFormat)
|
||||
: undefined;
|
||||
|
||||
const entries = Object.entries(value).filter(([, v]) => v !== null && v !== undefined);
|
||||
if (entries.length === 0) {
|
||||
return <Typography variant="body2" color="text.disabled">—</Typography>;
|
||||
const schema = field.refSchema ? schemas[field.refSchema] : undefined;
|
||||
let subFields: FieldConfig[] = [];
|
||||
if (field.oneOfOptions && field.discriminatorProperty) {
|
||||
const activeOption = field.oneOfOptions.find((o) => o.value === value[field.discriminatorProperty!]);
|
||||
if (activeOption) subFields = activeOption.fields;
|
||||
} else if (schema && field.refSchema) {
|
||||
subFields = extractFields(field.refSchema, schema, schemas);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 0.5, flexWrap: "wrap" }}>
|
||||
{entries.map(([key, v]) => (
|
||||
<>
|
||||
<Box sx={{ display: "flex", gap: 0.5, flexWrap: "wrap", alignItems: "center" }}>
|
||||
{discChip && <Chip label={discChip} size="small" color="primary" variant="outlined" />}
|
||||
<Chip
|
||||
key={key}
|
||||
label={`${key}: ${String(v)}`}
|
||||
label={field.label}
|
||||
title={tooltip}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
onClick={() => setOpen(true)}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{field.label}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2} sx={{ mt: 0.5 }}>
|
||||
{subFields.map((sf) => (
|
||||
<Grid key={sf.name} item xs={12} sm={6}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ mb: 0.25, display: "block" }}>
|
||||
{sf.label}
|
||||
</Typography>
|
||||
<Typography variant="body2">{value?.[sf.name] == null ? "—" : typeof value[sf.name] === "object" ? (sf.inlineDisplayFormat ? applyDisplayFormat(value[sf.name], sf.inlineDisplayFormat) : JSON.stringify(value[sf.name])) : String(value[sf.name])}</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(false)}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { FieldConfig } from "../../../types";
|
||||
import { useAppContext } from "../../../context/AppContext";
|
||||
import { extractFields } from "../../../transformers/field-config";
|
||||
import { FormFieldRenderer } from "../FormFieldRenderer";
|
||||
import { applyDisplayFormat } from "../utils";
|
||||
|
||||
interface JsonFieldProps {
|
||||
field: FieldConfig;
|
||||
@@ -78,8 +79,8 @@ export function JsonField({ field, value, onChange }: JsonFieldProps) {
|
||||
if (!open) {
|
||||
if (value === null || value === undefined) {
|
||||
return (
|
||||
<Button variant="outlined" onClick={handleOpen} size="small">
|
||||
Set {field.label}
|
||||
<Button variant="outlined" onClick={handleOpen} size="small" startIcon={field.isArray ? <AddIcon /> : undefined}>
|
||||
{field.isArray ? `Add ${field.label}` : `Set ${field.label}`}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -87,14 +88,14 @@ export function JsonField({ field, value, onChange }: JsonFieldProps) {
|
||||
if (field.isArray && Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return (
|
||||
<Button variant="outlined" onClick={handleOpen} size="small">
|
||||
Set {field.label}
|
||||
<Button variant="outlined" onClick={handleOpen} size="small" startIcon={<AddIcon />}>
|
||||
Add {field.label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Chip
|
||||
label={`${value.length} item${value.length !== 1 ? "s" : ""}`}
|
||||
label={`${field.label} (${value.length})`}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@@ -105,15 +106,13 @@ export function JsonField({ field, value, onChange }: JsonFieldProps) {
|
||||
}
|
||||
|
||||
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(" | ");
|
||||
const tooltip = field.inlineDisplayFormat
|
||||
? applyDisplayFormat(value, field.inlineDisplayFormat)
|
||||
: undefined;
|
||||
return (
|
||||
<Chip
|
||||
label={summary || field.label}
|
||||
label={field.label}
|
||||
title={tooltip}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@@ -264,7 +263,4 @@ function initEditValue(value: any, field: FieldConfig, schemas: Record<string, a
|
||||
);
|
||||
}
|
||||
|
||||
function applyInlineFormat(obj: any, format: string): string {
|
||||
if (!obj || typeof obj !== "object") return String(obj ?? "");
|
||||
return format.replace(/\{(\w+)\}/g, (_, key) => String(obj[key] ?? ""));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ export function NumberField({ field, value, onChange, error }: Props) {
|
||||
}
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={error ?? field.description}
|
||||
placeholder={field.description}
|
||||
helperText={error || field.description || undefined}
|
||||
placeholder={field.description || undefined}
|
||||
size="small"
|
||||
disabled={field.readOnly}
|
||||
inputProps={isFloat ? { step: "any" } : undefined}
|
||||
|
||||
@@ -20,8 +20,8 @@ export function StringField({ field, value, onChange, error }: Props) {
|
||||
value={value ?? ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
error={!!error}
|
||||
helperText={error ?? field.description}
|
||||
placeholder={field.description}
|
||||
helperText={error || field.description || undefined}
|
||||
placeholder={field.description || undefined}
|
||||
size="small"
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user