diff --git a/react-openapi/components/EnhancedTable.tsx b/react-openapi/components/EnhancedTable.tsx index 10dc971..22be2f0 100644 --- a/react-openapi/components/EnhancedTable.tsx +++ b/react-openapi/components/EnhancedTable.tsx @@ -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 ( (); - 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 => { diff --git a/react-openapi/components/fields/FormField.tsx b/react-openapi/components/fields/FormField.tsx index 114c88f..a194529 100644 --- a/react-openapi/components/fields/FormField.tsx +++ b/react-openapi/components/fields/FormField.tsx @@ -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'; - - // 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 options = getFieldOptions(field, relationData); + const keyField = field.enumOption?.key ?? 'id'; - 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 ( {label} @@ -149,6 +150,7 @@ export default function FormField({ // 5. Enum Handling if (field.type === 'enum' && field.options) { + const options = getFieldOptions(field); return ( {label} @@ -158,9 +160,9 @@ export default function FormField({ onChange={(e) => onChange(e.target.value)} disabled={disabled} > - {field.options.map((opt: string) => ( - - {opt} + {options.map((opt) => ( + + {opt.value} ))} diff --git a/react-openapi/types/config.ts b/react-openapi/types/config.ts index 43be512..d0b46b3 100644 --- a/react-openapi/types/config.ts +++ b/react-openapi/types/config.ts @@ -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; 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; } export type ResourceMode = "server" | "client"; @@ -38,6 +50,7 @@ export interface ResourceConfig { mode?: ResourceMode; fields?: string[]; }; + enumOption?: EnumOption; } export interface AppConfig { diff --git a/react-openapi/types/overrides.ts b/react-openapi/types/overrides.ts index 6200308..e3dbfe8 100644 --- a/react-openapi/types/overrides.ts +++ b/react-openapi/types/overrides.ts @@ -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; } export interface ResourceOverride { @@ -18,4 +19,5 @@ export interface ResourceOverride { mode?: "server" | "client"; fields?: string[]; }; + enumOption?: EnumOption; } diff --git a/react-openapi/utils/openapi_loader.ts b/react-openapi/utils/openapi_loader.ts index ac472f4..82ad104 100644 --- a/react-openapi/utils/openapi_loader.ts +++ b/react-openapi/utils/openapi_loader.ts @@ -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) diff --git a/react-openapi/utils/options.ts b/react-openapi/utils/options.ts new file mode 100644 index 0000000..4914b3b --- /dev/null +++ b/react-openapi/utils/options.ts @@ -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 })); +} diff --git a/src/openapi-config.ts b/src/openapi-config.ts index 638c9c4..57eb1a2 100644 --- a/src/openapi-config.ts +++ b/src/openapi-config.ts @@ -50,6 +50,18 @@ export const configuration: Record = { } }, }, + accounts: { + enumOption: { + key: 'id', + value: ['name', 'number'] + } + }, + tags: { + enumOption: { + key: 'id', + value: ['icon', 'name'] + } + }, }; export const profileConfiguration = {