Refactor the React OpenAPI admin framework to support fully customizable field rendering and UI composition. #11
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "latest",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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] });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user