openapi-spec-reader #10

Merged
aetos merged 3 commits from openapi-spec-reader into main 2026-06-04 17:23:45 +00:00
8 changed files with 109 additions and 29 deletions
Showing only changes of commit 7c33bd9c7c - Show all commits

View File

@@ -31,6 +31,7 @@ import VisibilityIcon from '@mui/icons-material/Visibility';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { useNavigate } from 'react-router-dom';
import { ResourceConfig } from '../types/config';
import { getFieldOptions, toGridValueOptions } from '../utils/options';
interface EnhancedTableProps {
config: ResourceConfig;
@@ -96,8 +97,7 @@ export default function EnhancedTable({
}
if (muiType === 'singleSelect' && field.options) {
// @ts-ignore
col.valueOptions = field.options;
col.valueOptions = toGridValueOptions(getFieldOptions(field));
}
return col;
@@ -379,6 +379,11 @@ function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate,
if (field.type === 'datetime' || field.type === 'date') return value ? new Date(value).toLocaleString() : '';
if (field.type === 'enum' && field.options) {
const opt = getFieldOptions(field).find(o => o.key === value);
return opt?.value ?? value;
}
if (isPk && !isMobile) {
return (
<Chip

View File

@@ -11,6 +11,7 @@ import {
import DoneIcon from "@mui/icons-material/Done";
import FilterListIcon from "@mui/icons-material/FilterList";
import { ResourceField, ResourceMode } from "../types/config";
import { getFieldOptions } from "../utils/options";
function FilterAutocomplete({
options,
@@ -110,7 +111,9 @@ function extractOptions(
): string[] {
const values = new Set<string>();
if (field.options) return field.options;
if (field.type === 'enum' && field.options) {
return getFieldOptions(field).map(o => o.key);
}
if (!data) return [];
const pull = (item: any): string | null => {

View File

@@ -12,6 +12,7 @@ import {
Divider,
} from '@mui/material';
import { ResourceField } from '../../types/config';
import { getFieldOptions } from '../../utils/options';
import ImageUploadField from './ImageUploadField';
interface FormFieldProps {
@@ -73,40 +74,40 @@ export default function FormField({
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';
// 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;
};
// 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={value || (isArrayRelation ? [] : "")}
value={normalizedValue}
label={label}
displayEmpty
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
renderValue={(selected: any) => {
if (isArrayRelation) {
return (selected as any[]).map(getOptionLabel).join(', ');
return (selected as string[]).map(k => options.find(o => o.key === k)?.value ?? k).join(', ');
}
return getOptionLabel(selected);
return options.find(o => o.key === selected)?.value ?? selected;
}}
>
{relationData.map((option) => (
<MenuItem key={option.id || JSON.stringify(option)} value={getOptionValue(option)}>
{getOptionLabel(option)}
{options.map((opt) => (
<MenuItem key={opt.key} value={opt.key}>
{opt.value}
</MenuItem>
))}
</Select>
@@ -149,6 +150,7 @@ export default function FormField({
// 5. Enum Handling
if (field.type === 'enum' && field.options) {
const options = getFieldOptions(field);
return (
<FormControl fullWidth>
<InputLabel>{label}</InputLabel>
@@ -158,9 +160,9 @@ export default function FormField({
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
>
{field.options.map((opt: string) => (
<MenuItem key={opt} value={opt}>
{opt}
{options.map((opt) => (
<MenuItem key={opt.key} value={opt.key}>
{opt.value}
</MenuItem>
))}
</Select>

View File

@@ -10,6 +10,16 @@ export type FieldType =
| 'object'
| 'array';
export interface SelectOption {
key: string;
value: string;
}
export interface EnumOption {
key: string;
value: string | string[];
}
export interface ResourceField {
type: FieldType;
label: string;
@@ -19,8 +29,10 @@ export interface ResourceField {
schema?: Record<string, ResourceField>;
displayField?: string | string[];
formatter?: (value: any) => string;
relation?: string; // Name of the target resource
relation?: string;
filterType?: "autocomplete" | "multiselect" | "number-range" | "date-range";
enumOption?: EnumOption;
enumLabels?: Record<string, string>;
}
export type ResourceMode = "server" | "client";
@@ -38,6 +50,7 @@ export interface ResourceConfig {
mode?: ResourceMode;
fields?: string[];
};
enumOption?: EnumOption;
}
export interface AppConfig {

View File

@@ -1,13 +1,14 @@
/**
* This file contains application-specific overrides and configuration
* for the generic Admin Panel.
*/
export interface EnumOption {
key: string;
value: string | string[];
}
export interface FieldOverride {
displayField?: string | string[];
display?: boolean;
formatter?: (value: any) => string;
filterType?: "autocomplete" | "multiselect" | "number-range" | "date-range";
enumLabels?: Record<string, string>;
}
export interface ResourceOverride {
@@ -18,4 +19,5 @@ export interface ResourceOverride {
mode?: "server" | "client";
fields?: string[];
};
enumOption?: EnumOption;
}

View File

@@ -80,6 +80,21 @@ function parseSchemaFields(
const relation = schemaToResourceMap.get(targetSchema);
if (relation) {
fields[key].relation = relation;
// Propagate enumOption from target resource config, or derive from target schema
const explicitEnumOption = configuration[relation]?.enumOption;
if (explicitEnumOption) {
fields[key].enumOption = explicitEnumOption;
} else {
const targetProps = targetSchema.properties || {};
const valueField = Object.entries(targetProps).find(
([name, p]: [string, any]) => name !== 'id' && p.type === 'string'
)?.[0];
fields[key].enumOption = {
key: 'id',
value: valueField ?? 'id',
};
}
}
// Recursively parse nested objects (only if not a relation)

View File

@@ -0,0 +1,28 @@
import { ResourceField, SelectOption } from "../types/config";
export function getFieldOptions(field: ResourceField, relationData?: any[]): SelectOption[] {
if (field.type === 'enum' && field.options) {
return field.options.map(opt => ({
key: opt,
value: field.enumLabels?.[opt] ?? opt,
}));
}
if (field.relation) {
const data = relationData ?? [];
const enumOption = field.enumOption ?? { key: 'id', value: 'name' };
return data.map(item => ({
key: String(item[enumOption.key] ?? ''),
value: Array.isArray(enumOption.value)
? enumOption.value.map(k => item[k]).filter(v => v != null).join(' ')
: String(item[enumOption.value] ?? ''),
}));
}
return [];
}
export function toGridValueOptions(options: SelectOption[]): { value: string; label: string }[] {
return options.map(opt => ({ value: opt.key, label: opt.value }));
}

View File

@@ -50,6 +50,18 @@ export const configuration: Record<string, ResourceOverride> = {
}
},
},
accounts: {
enumOption: {
key: 'id',
value: ['name', 'number']
}
},
tags: {
enumOption: {
key: 'id',
value: ['icon', 'name']
}
},
};
export const profileConfiguration = {