From 80ca1ac9a9d023a5d867f7c6b68136420b08839d Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Thu, 4 Jun 2026 16:17:03 +0530 Subject: [PATCH] enumOptions and enum reader used everywhere --- react-openapi/components/EnhancedTable.tsx | 4 +- react-openapi/components/FilterBar.tsx | 4 +- react-openapi/components/ResourceView.tsx | 46 ++++++-------- react-openapi/components/fields/FormField.tsx | 2 +- react-openapi/types/config.ts | 1 + react-openapi/utils/openapi_loader.ts | 60 +++++++++++++++---- react-openapi/utils/options.ts | 4 +- src/FetchRequests.tsx | 4 +- 8 files changed, 77 insertions(+), 48 deletions(-) diff --git a/react-openapi/components/EnhancedTable.tsx b/react-openapi/components/EnhancedTable.tsx index cad20b2..067e3a2 100644 --- a/react-openapi/components/EnhancedTable.tsx +++ b/react-openapi/components/EnhancedTable.tsx @@ -96,7 +96,7 @@ export default function EnhancedTable({ }; } - if (muiType === 'singleSelect' && field.options) { + if (muiType === 'singleSelect') { col.valueOptions = toGridValueOptions(getFieldOptions(field)); } @@ -381,7 +381,7 @@ 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) { + if (field.type === 'enum') { const opt = getFieldOptions(field).find(o => o.key === value); return opt?.value ?? value; } diff --git a/react-openapi/components/FilterBar.tsx b/react-openapi/components/FilterBar.tsx index 5893198..e2b8a3e 100644 --- a/react-openapi/components/FilterBar.tsx +++ b/react-openapi/components/FilterBar.tsx @@ -111,8 +111,8 @@ function extractOptions( ): string[] { const values = new Set(); - if (field.type === 'enum' && field.options) { - return getFieldOptions(field).map(o => o.key); + if (field.type === 'enum') { + return getFieldOptions(field).map(o => o.value); } if (!data) return []; diff --git a/react-openapi/components/ResourceView.tsx b/react-openapi/components/ResourceView.tsx index e6ab18d..f03885b 100644 --- a/react-openapi/components/ResourceView.tsx +++ b/react-openapi/components/ResourceView.tsx @@ -16,11 +16,16 @@ interface ResourceViewProps { import { GridPaginationModel } from '@mui/x-data-grid'; -function getFilterDisplayFields(field: ResourceField): string[] { - if (!field.displayField) return []; - return (Array.isArray(field.displayField) ? field.displayField : [field.displayField]).filter( - (df): df is string => !!df - ); +function getDisplayString(item: any, field: ResourceField): string { + if (item == null || typeof item !== 'object') return String(item ?? ''); + if (field.enumOption?.value) return resolveTemplate(field.enumOption.value, item); + const df = field.displayField; + if (!df) return item.name ?? item.title ?? item.label ?? item.id ?? JSON.stringify(item); + if (Array.isArray(df)) { + const parts = df.map((k: string) => item[k]).filter((v: any) => v != null); + return parts.length > 0 ? parts.join(' ') : ''; + } + return String(item[df] ?? ''); } function applyClientFilters( @@ -60,20 +65,12 @@ function applyClientFilters( if (Array.isArray(filterValue)) { if (field.type === "array" && Array.isArray(itemValue)) { - return itemValue.some((el: any) => { - if (el != null && typeof el === "object") { - if (field.enumOption?.value) return filterValue.includes(resolveTemplate(field.enumOption.value, el)); - const dispFields = getFilterDisplayFields(field); - return dispFields.some((df) => filterValue.includes(String(el[df]))); - } - return filterValue.includes(String(el)); - }); + return itemValue.some((el: any) => + filterValue.includes(getDisplayString(el, field)) + ); } if (itemValue && typeof itemValue === "object") { - if (field.enumOption?.value) return filterValue.includes(resolveTemplate(field.enumOption.value, itemValue)); - const dispFields = getFilterDisplayFields(field); - const itemDisplay = dispFields.map((df) => itemValue[df]).filter((v) => v != null).join(" "); - return filterValue.includes(itemDisplay); + return filterValue.includes(getDisplayString(itemValue, field)); } return filterValue.includes(String(itemValue)); } @@ -85,20 +82,13 @@ function applyClientFilters( } if (field.type === "array" && Array.isArray(itemValue)) { - return itemValue.some((el: any) => { - if (el != null && typeof el === "object") { - if (field.enumOption?.value) return resolveTemplate(field.enumOption.value, el) === String(filterValue); - const dispFields = getFilterDisplayFields(field); - return dispFields.some((df) => String(el[df]) === String(filterValue)); - } - return String(el) === String(filterValue); - }); + return itemValue.some((el: any) => + getDisplayString(el, field) === String(filterValue) + ); } if (itemValue && typeof itemValue === "object") { - if (field.enumOption?.value) return resolveTemplate(field.enumOption.value, itemValue) === String(filterValue); - const dispFields = getFilterDisplayFields(field); - return dispFields.some((df) => String(itemValue[df]) === String(filterValue)); + return getDisplayString(itemValue, field) === String(filterValue); } return String(itemValue) === String(filterValue); diff --git a/react-openapi/components/fields/FormField.tsx b/react-openapi/components/fields/FormField.tsx index a194529..b1915b6 100644 --- a/react-openapi/components/fields/FormField.tsx +++ b/react-openapi/components/fields/FormField.tsx @@ -149,7 +149,7 @@ export default function FormField({ } // 5. Enum Handling - if (field.type === 'enum' && field.options) { + if (field.type === 'enum') { const options = getFieldOptions(field); return ( diff --git a/react-openapi/types/config.ts b/react-openapi/types/config.ts index b7be75d..347ab7e 100644 --- a/react-openapi/types/config.ts +++ b/react-openapi/types/config.ts @@ -57,6 +57,7 @@ export interface AppConfig { baseUrl: string; authBaseUrl: string; resources: ResourceConfig[]; + enums: Record; profile?: { resource: string; extraFields?: Record; diff --git a/react-openapi/utils/openapi_loader.ts b/react-openapi/utils/openapi_loader.ts index 82ad104..355c5f6 100644 --- a/react-openapi/utils/openapi_loader.ts +++ b/react-openapi/utils/openapi_loader.ts @@ -36,6 +36,26 @@ function mapOpenApiType(prop: any): FieldType { /** * Recursively converts OpenAPI schemas to ResourceField map */ +function mergeProperties(schema: any): { properties: Record; required: string[] } { + let properties: Record = {}; + let required: string[] = []; + + if (schema.allOf) { + for (const sub of schema.allOf) { + const merged = mergeProperties(sub); + properties = { ...properties, ...merged.properties }; + required = [...required, ...merged.required]; + } + } + if (schema.properties) { + properties = { ...properties, ...schema.properties }; + } + if (schema.required) { + required = [...required, ...schema.required]; + } + return { properties, required }; +} + function parseSchemaFields( schema: any, resourceName: string, @@ -43,12 +63,19 @@ function parseSchemaFields( configuration: Record = {} ): Record { const fields: Record = {}; - const properties = schema.properties || {}; - const required = schema.required || []; + const { properties, required } = mergeProperties(schema); const overrides = configuration[resourceName]?.fields || {}; for (const [key, prop] of Object.entries(properties) as [string, any]) { - const type = mapOpenApiType(prop); + // Resolve oneOf/anyOf by merging all branch properties + let resolvedProp = prop; + if (prop.oneOf || prop.anyOf) { + const branches = prop.oneOf || prop.anyOf; + const merged = mergeProperties({ allOf: branches }); + resolvedProp = { ...prop, type: 'object', properties: merged.properties, required: merged.required }; + } + + const type = mapOpenApiType(resolvedProp); const override = overrides[key]; // Explicitly skip 'id' as it's the primary key and handled elsewhere @@ -57,12 +84,12 @@ function parseSchemaFields( fields[key] = { type, label: - prop.title || + resolvedProp.title || key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "), required: required.includes(key), - options: prop.enum, + options: resolvedProp.enum, readOnly: - prop.readOnly || + resolvedProp.readOnly || key === "created_at" || key === "updated_at", ...override, @@ -71,9 +98,9 @@ function parseSchemaFields( // STRICT RELATION DETECTION // A field is a relation ONLY if its schema object (or items schema) // exactly matches a schema that is defined as a resource. - let targetSchema = prop; - if (type === "array" && prop.items) { - targetSchema = prop.items; + let targetSchema = resolvedProp; + if (type === "array" && resolvedProp.items) { + targetSchema = resolvedProp.items; } // Check if this schema object is registered as a resource @@ -98,8 +125,8 @@ function parseSchemaFields( } // Recursively parse nested objects (only if not a relation) - if (fields[key].type === "object" && prop.properties && !relation) { - fields[key].schema = parseSchemaFields(prop, resourceName, schemaToResourceMap, configuration); + if (fields[key].type === "object" && resolvedProp.properties && !relation) { + fields[key].schema = parseSchemaFields(resolvedProp, resourceName, schemaToResourceMap, configuration); } } @@ -187,6 +214,16 @@ export async function loadConfigFromOpenApi(baseUrl: string, configuration: Reco }); } + // Collect standalone enum schemas (e.g. FetchRequestStatus, AccountType, etc.) + const enums: Record = {}; + if (api.components?.schemas) { + for (const [name, schema] of Object.entries(api.components.schemas) as [string, any]) { + if (schema.enum) { + enums[name] = schema.enum; + } + } + } + // @ts-ignore const serverBaseUrl = import.meta.env.VITE_API_BASE_URL || (api.servers?.[0]?.url ?? "") // @ts-ignore @@ -195,6 +232,7 @@ export async function loadConfigFromOpenApi(baseUrl: string, configuration: Reco baseUrl: serverBaseUrl, authBaseUrl: authBaseUrl, resources, + enums, profile: profileConfiguration, }; } diff --git a/react-openapi/utils/options.ts b/react-openapi/utils/options.ts index c8c3995..13ab92a 100644 --- a/react-openapi/utils/options.ts +++ b/react-openapi/utils/options.ts @@ -8,8 +8,8 @@ export function resolveTemplate(template: string, item: any): string { } export function getFieldOptions(field: ResourceField, relationData?: any[]): SelectOption[] { - if (field.type === 'enum' && field.options) { - return field.options.map(opt => ({ + if (field.type === 'enum') { + return (field.options ?? []).map(opt => ({ key: opt, value: field.enumLabels?.[opt] ?? opt, })); diff --git a/src/FetchRequests.tsx b/src/FetchRequests.tsx index 83b492a..2abfc6f 100644 --- a/src/FetchRequests.tsx +++ b/src/FetchRequests.tsx @@ -129,7 +129,7 @@ export default function FetchRequests() { const config = useConfig(); const fetchRes = config?.resources.find((r: any) => r.name === "fetch-requests"); - const formatOptions: string[] = (fetchRes?.fields?.source?.schema?.format?.options as string[]) ?? ["axis", "icici"]; + const formatOptions: string[] = fetchRes?.fields?.source?.schema?.format?.options as string[] ?? []; const createMutation = useCreateFetchRequest(); const updateMutation = useUpdateFetchRequest(); @@ -345,7 +345,7 @@ export default function FetchRequests() { input={} renderValue={(selected) => (selected as string[]).join(", ")} > - {["pending", "processing", "paused", "raw_expenses_done", "enriched_done", "completed", "failed"].map((s) => ( + {(config?.enums?.FetchRequestStatus ?? []).map((s: string) => ( {s.replace(/_/g, " ")} ))}