Refactor the React OpenAPI admin framework to support fully customizable field rendering and UI composition. #11

Merged
aetos merged 15 commits from common-fields into main 2026-06-07 12:35:53 +00:00
12 changed files with 55 additions and 24 deletions
Showing only changes of commit d46213b96b - Show all commits

14
package-lock.json generated
View File

@@ -28,6 +28,7 @@
},
"devDependencies": {
"@vitejs/plugin-react": "latest",
"typescript": "^6.0.3",
"vite": "latest"
}
},
@@ -4103,6 +4104,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",

View File

@@ -28,6 +28,7 @@
},
"devDependencies": {
"@vitejs/plugin-react": "latest",
"typescript": "^6.0.3",
"vite": "latest"
}
}

View File

@@ -100,7 +100,7 @@ export default function EnhancedTable({
}
if (muiType === 'singleSelect') {
col.valueOptions = toGridValueOptions(getFieldOptions(field));
(col as GridColDef & { valueOptions: any[] }).valueOptions = toGridValueOptions(getFieldOptions(field));
}
return col;

View File

@@ -130,6 +130,7 @@ function extractOptions(
if (Array.isArray(df)) {
const parts = df.map((k) => item[k]).filter((v) => v != null);
if (parts.length > 0) return parts.join(" ");
return null;
}
const v = item[df];
if (v != null) return String(v);

View File

@@ -201,7 +201,7 @@ export default function ResourceView({ config, onNavigateToResource, fieldCompon
</Box>
) : (
<Paper sx={{ p: 4 }}>
<components.GenericForm
{components && <components.GenericForm
config={config}
initialData={isCreate ? null : itemQuery.data}
onSave={handleSave}
@@ -209,7 +209,7 @@ export default function ResourceView({ config, onNavigateToResource, fieldCompon
loading={createMutation.isPending || updateMutation.isPending}
readOnly={isView}
onEditClick={() => navigate(`/admin/${config.name}/edit/${id}`)}
/>
/>}
</Paper>
)}
</Box>

View File

@@ -1,4 +1,5 @@
import { FieldComponents } from '../../types/overrides';
import * as React from 'react';
import { FieldComponents, FieldComponentProps } from '../../types/overrides';
import TextFieldEntry from './TextField';
import NumberField from './NumberField';
import BooleanField from './BooleanField';
@@ -8,6 +9,19 @@ import RelationField from './RelationField';
import ImageUploadField from './ImageUploadField';
import FallbackField from './FallbackField';
const WrappedImageUploadField = (props: FieldComponentProps) =>
React.createElement(ImageUploadField, {
label: props.field.label,
value: props.value || '',
onUpload: async (file: File) => {
const url = await props.uploadFile?.(file);
if (url) props.onChange(url);
},
uploading: props.uploading,
baseUrl: props.baseUrl || '',
disabled: props.disabled,
});
export const defaultFieldComponents: FieldComponents = {
string: TextFieldEntry,
markdown: TextFieldEntry,
@@ -16,7 +30,7 @@ export const defaultFieldComponents: FieldComponents = {
date: DateField,
datetime: DateField,
enum: EnumField,
image: ImageUploadField,
image: WrappedImageUploadField,
relation: RelationField,
default: FallbackField,
};

View File

@@ -9,9 +9,9 @@ export interface FormFieldProps {
value: any;
onChange: (val: any) => void;
disabled?: boolean;
uploadFile: (file: File) => Promise<string | null>;
uploading: boolean;
baseUrl: string;
uploadFile?: (file: File) => Promise<string | null>;
uploading?: boolean;
baseUrl?: string;
relationDataMap?: Record<string, any[]>;
components: FieldComponents;
}
@@ -51,9 +51,9 @@ export default function FormField({
value={childProps.value}
onChange={childProps.onChange}
disabled={childProps.disabled}
uploadFile={childProps.uploadFile!}
uploading={childProps.uploading!}
baseUrl={childProps.baseUrl!}
uploadFile={childProps.uploadFile}
uploading={childProps.uploading}
baseUrl={childProps.baseUrl}
relationDataMap={childProps.relationDataMap}
components={components}
/>

View File

@@ -7,7 +7,7 @@ export default function RelationField({ field, value, onChange, disabled, relati
return null;
}
const relationData = relationDataMap[field.relation].data;
const relationData = relationDataMap[field.relation];
const isArrayRelation = field.type === 'array';
const options = getFieldOptions(field, relationData);
const keyField = field.enumOption?.key ?? 'id';

View File

@@ -78,7 +78,7 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
const res = await api.put<T>(`${endpoint}/${id}`, data);
return res.data;
},
onSuccess: (updatedItem) => {
onSuccess: (updatedItem: any) => {
const id = updatedItem[primaryKey];
queryClient.invalidateQueries({ queryKey: [name, "list"] });
queryClient.invalidateQueries({ queryKey: [name, "detail", id] });
@@ -93,7 +93,7 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
const res = await api.patch<T>(`${endpoint}/${id}`, data);
return res.data;
},
onSuccess: (updatedItem) => {
onSuccess: (updatedItem: any) => {
const listId = updatedItem[primaryKey];
queryClient.invalidateQueries({ queryKey: [name, "list"] });
queryClient.invalidateQueries({ queryKey: [name, "detail", listId] });

View File

@@ -222,8 +222,9 @@ export async function loadConfigFromOpenApi(baseUrl: string, configuration: Reco
// Collect standalone enum schemas (e.g. FetchRequestStatus, AccountType, etc.)
const enums: Record<string, string[]> = {};
if (api.components?.schemas) {
for (const [name, schema] of Object.entries(api.components.schemas) as [string, any]) {
const apiDoc = api as any;
if (apiDoc.components?.schemas) {
for (const [name, schema] of Object.entries(apiDoc.components.schemas) as [string, any]) {
if (schema.enum) {
enums[name] = schema.enum;
}

View File

@@ -326,9 +326,9 @@ export default function FetchRequests() {
)}
<Box sx={{ display: "flex", gap: 2 }}>
{startDateField && components?.DateField ? (
{startDateField && components?.date ? (
<Box sx={{ flex: 1 }}>
<components.DateField
<components.date
name="start_date"
field={startDateField}
value={startDate}
@@ -347,9 +347,9 @@ export default function FetchRequests() {
sx={{ flex: 1 }}
/>
)}
{endDateField && components?.DateField ? (
{endDateField && components?.date ? (
<Box sx={{ flex: 1 }}>
<components.DateField
<components.date
name="end_date"
field={endDateField}
value={endDate}

View File

@@ -140,9 +140,9 @@ export default function ReportSnapshots() {
)}
<Box sx={{ display: "flex", gap: 2 }}>
{startDateField && components?.DateField ? (
{startDateField && components?.datetime ? (
<Box sx={{ flex: 1 }}>
<components.DateField
<components.datetime
name="start_date"
field={startDateField}
value={startDate}
@@ -160,9 +160,9 @@ export default function ReportSnapshots() {
sx={{ flex: 1 }}
/>
)}
{endDateField && components?.DateField ? (
{endDateField && components?.datetime ? (
<Box sx={{ flex: 1 }}>
<components.DateField
<components.datetime
name="end_date"
field={endDateField}
value={endDate}