updated react-openapi

This commit is contained in:
2026-06-17 21:03:08 +05:30
parent cd89eb4c88
commit 0a668cf98d
64 changed files with 2412 additions and 2921 deletions

View File

@@ -0,0 +1,27 @@
import React from "react";
import { FormControl, FormControlLabel, Switch, FormHelperText } from "@mui/material";
import type { FieldConfig } from "../../../types";
interface Props {
field: FieldConfig;
value: any;
onChange: (value: any) => void;
}
export function BooleanField({ field, value, onChange }: Props) {
return (
<FormControl component="fieldset" fullWidth size="small">
<FormControlLabel
control={
<Switch
checked={!!value}
onChange={(e) => onChange(e.target.checked)}
disabled={field.readOnly}
/>
}
label={field.label}
/>
{field.description && <FormHelperText>{field.description}</FormHelperText>}
</FormControl>
);
}

View File

@@ -0,0 +1,30 @@
import React from "react";
import { TextField } from "@mui/material";
import type { FieldConfig } from "../../../types";
interface Props {
field: FieldConfig;
value: any;
onChange: (value: any) => void;
error?: string;
}
export function DateField({ field, value, onChange, error }: Props) {
const inputType = field.format === "date" ? "date" : "datetime-local";
return (
<TextField
fullWidth
label={field.label}
type={inputType}
value={value ?? ""}
onChange={(e) => onChange(e.target.value)}
error={!!error}
helperText={error ?? field.description}
placeholder={field.description}
size="small"
disabled={field.readOnly}
InputLabelProps={{ shrink: true }}
/>
);
}

View File

@@ -0,0 +1,34 @@
import React from "react";
import { FormControl, InputLabel, Select, MenuItem, FormHelperText } from "@mui/material";
import type { FieldConfig } from "../../../types";
interface Props {
field: FieldConfig;
value: any;
onChange: (value: any) => void;
error?: string;
}
export function EnumField({ field, value, onChange, error }: Props) {
return (
<FormControl fullWidth size="small" error={!!error}>
<InputLabel>{field.label}</InputLabel>
<Select
value={value ?? ""}
label={field.label}
onChange={(e) => onChange(e.target.value)}
disabled={field.readOnly}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{(field.enumValues ?? []).map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
{field.description && <FormHelperText>{field.description}</FormHelperText>}
</FormControl>
);
}

View File

@@ -0,0 +1,32 @@
import React from "react";
import { TextField, Autocomplete } from "@mui/material";
import type { FieldConfig } from "../../../types";
interface Props {
field: FieldConfig;
value: any;
onChange: (value: any) => void;
fkOptions?: { value: any; label: string }[];
fkLoading?: boolean;
onOpen?: () => void;
}
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)}`);
return (
<Autocomplete
multiple
options={fkOptions ?? []}
getOptionLabel={(o) => o.label}
value={fkOptions?.filter((o) => (value ?? []).includes(o.value)) ?? []}
onChange={(_, newVal) => onChange(newVal.map((v) => v.value))}
onOpen={() => onOpen?.()}
loading={fkLoading}
renderInput={(params) => (
<TextField {...params} label={field.label} helperText={field.description} size="small" />
)}
size="small"
disabled={field.readOnly}
/>
);
}

View File

@@ -0,0 +1,38 @@
import React from "react";
import { FormControl, InputLabel, Select, MenuItem, FormHelperText } from "@mui/material";
import type { FieldConfig } from "../../../types";
interface Props {
field: FieldConfig;
value: any;
onChange: (value: any) => void;
error?: string;
fkOptions?: { value: any; label: string }[];
onOpen?: () => void;
}
export function FkSelectField({ field, value, onChange, error, fkOptions, onOpen }: Props) {
console.log(`[FkSelectField] render field="${field.name}" fkOptions=${fkOptions ? `${fkOptions.length} items` : "undefined"} value=${value}`);
return (
<FormControl fullWidth size="small" error={!!error}>
<InputLabel>{field.label}</InputLabel>
<Select
value={value ?? ""}
label={field.label}
onChange={(e) => onChange(e.target.value)}
onOpen={() => onOpen?.()}
disabled={field.readOnly}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{(fkOptions ?? []).map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</Select>
{field.description && <FormHelperText>{field.description}</FormHelperText>}
</FormControl>
);
}

View File

@@ -0,0 +1,60 @@
import React from "react";
import { Box, Typography, Avatar, FormHelperText } from "@mui/material";
import Button from "@mui/material/Button";
import type { FieldConfig } from "../../../types";
import { getApi } from "../../../hooks/useApi";
interface Props {
field: FieldConfig;
value: any;
onChange: (value: any) => void;
id?: string | number;
uploadUrl?: string;
}
export function ImageField({ field, value, onChange, id, uploadUrl }: Props) {
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!id || !uploadUrl) {
const reader = new FileReader();
reader.onload = () => onChange(reader.result);
reader.readAsDataURL(file);
return;
}
const formData = new FormData();
formData.append("file", file);
try {
const api = getApi();
const url = uploadUrl.replace("{id}", String(id));
const res = await api.post(url, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
onChange(res.data.url ?? res.data);
} catch {
const reader = new FileReader();
reader.onload = () => onChange(reader.result);
reader.readAsDataURL(file);
}
};
return (
<Box>
<Typography variant="body2" fontWeight={600} sx={{ mb: 0.5 }}>
{field.label}
</Typography>
{value ? (
<Avatar src={value} variant="rounded" sx={{ width: 120, height: 120 }} />
) : (
<Button variant="outlined" component="label" size="small">
Upload {field.label}
<input type="file" hidden accept="image/*" onChange={handleUpload} />
</Button>
)}
<FormHelperText>{field.description}</FormHelperText>
</Box>
);
}

View File

@@ -0,0 +1,38 @@
import React from "react";
import { Box, Typography, Chip } from "@mui/material";
import type { FieldConfig } from "../../../types";
import { applyDisplayFormat } from "../utils";
interface Props {
field: FieldConfig;
value: any;
displayFormat?: string;
}
export function InlineRefField({ field, value, displayFormat }: Props) {
if (!value || typeof value !== "object") {
return <Typography variant="body2" color="text.disabled"></Typography>;
}
if (displayFormat) {
return <Typography variant="body2">{applyDisplayFormat(value, displayFormat)}</Typography>;
}
const entries = Object.entries(value).filter(([, v]) => v !== null && v !== undefined);
if (entries.length === 0) {
return <Typography variant="body2" color="text.disabled"></Typography>;
}
return (
<Box sx={{ display: "flex", gap: 0.5, flexWrap: "wrap" }}>
{entries.map(([key, v]) => (
<Chip
key={key}
label={`${key}: ${String(v)}`}
size="small"
variant="outlined"
/>
))}
</Box>
);
}

View File

@@ -0,0 +1,270 @@
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] ?? ""));
}

View File

@@ -0,0 +1,37 @@
import React from "react";
import { TextField } from "@mui/material";
import type { FieldConfig } from "../../../types";
interface Props {
field: FieldConfig;
value: any;
onChange: (value: any) => void;
error?: string;
}
export function NumberField({ field, value, onChange, error }: Props) {
const isFloat = field.type === "number" || field.format === "float";
return (
<TextField
fullWidth
label={field.label}
type="number"
value={value ?? ""}
onChange={(e) => {
const raw = e.target.value;
if (raw === "") {
onChange("");
} else {
onChange(isFloat ? parseFloat(raw) : parseInt(raw, 10));
}
}}
error={!!error}
helperText={error ?? field.description}
placeholder={field.description}
size="small"
disabled={field.readOnly}
inputProps={isFloat ? { step: "any" } : undefined}
/>
);
}

View File

@@ -0,0 +1,29 @@
import React from "react";
import { TextField } from "@mui/material";
import type { FieldConfig } from "../../../types";
interface Props {
field: FieldConfig;
value: any;
onChange: (value: any) => void;
error?: string;
}
export function StringField({ field, value, onChange, error }: Props) {
const inputType = field.format === "email" ? "email" : "text";
return (
<TextField
fullWidth
label={field.label}
type={inputType}
value={value ?? ""}
onChange={(e) => onChange(e.target.value)}
error={!!error}
helperText={error ?? field.description}
placeholder={field.description}
size="small"
disabled={field.readOnly}
/>
);
}