diff --git a/react-openapi/Admin.tsx b/react-openapi/Admin.tsx
index 6fc16c4..c50c191 100644
--- a/react-openapi/Admin.tsx
+++ b/react-openapi/Admin.tsx
@@ -16,8 +16,9 @@ import {
} from "react-router-dom";
import { ConfigContext } from "./providers/ConfigContext";
+import ProfileView from "./components/ProfileView";
-function Dashboard({ basePath }: { basePath: string }) {
+function DefaultDashboard({ basePath }: { basePath: string }) {
const config = React.useContext(ConfigContext);
const navigate = useNavigate();
@@ -32,7 +33,6 @@ function Dashboard({ basePath }: { basePath: string }) {
Select a resource from the sidebar to manage data.
-
{visibleResources.map((res) => (
- ;
+ Layout?: React.ComponentType;
+ LoginPage?: React.ComponentType;
+}
-function AdminApp({ basePath, fieldComponents }: { basePath: string; fieldComponents?: FieldComponents }) {
+function AdminApp({ basePath, fieldComponents, Dashboard = DefaultDashboard, Layout = AdminLayout, LoginPage = AuthPage }: AdminAppProps) {
const { currentUser, login, logout, loading, error } = useAuth();
const config = React.useContext(ConfigContext);
const navigate = useNavigate();
@@ -74,10 +80,10 @@ function AdminApp({ basePath, fieldComponents }: { basePath: string; fieldCompon
if (!currentUser) {
return (
- {}} // Disable registration for Admin
+ register={async () => {}}
loading={loading}
error={error}
onSwitchMode={() => {}}
@@ -88,7 +94,7 @@ function AdminApp({ basePath, fieldComponents }: { basePath: string; fieldCompon
}
return (
- navigate(`/admin/${name}`)}
@@ -102,11 +108,11 @@ function AdminApp({ basePath, fieldComponents }: { basePath: string; fieldCompon
} />
} />
-
+
);
}
-function ResourceRouteWrapper({ fieldComponents }: { fieldComponents?: FieldComponents }) {
+function ResourceRouteWrapper({ fieldComponents }: { fieldComponents: FieldComponents }) {
const { resourceName } = useParams();
const config = React.useContext(ConfigContext);
const selectedResource = config?.resources.find((r) => r.name === resourceName);
@@ -116,14 +122,25 @@ function ResourceRouteWrapper({ fieldComponents }: { fieldComponents?: FieldComp
return ;
}
+interface AdminLayoutProps {
+ children: React.ReactNode;
+ onSelectResource: (resourceName: string | null) => void;
+ onLogout: () => void;
+ username?: string;
+ resources: import("./types/config").ResourceConfig[];
+}
+
interface AdminProps {
basePath?: string;
resourceOverrides?: Record;
profileConfig?: any;
- fieldComponents?: FieldComponents;
+ fieldComponents: FieldComponents;
+ Dashboard?: React.ComponentType<{ basePath: string }>;
+ Layout?: React.ComponentType;
+ LoginPage?: React.ComponentType;
}
-export default function Admin({ basePath = "/admin", resourceOverrides = {}, profileConfig = {}, fieldComponents = {} }: AdminProps) {
+export default function Admin({ basePath = "/admin", resourceOverrides = {}, profileConfig = {}, fieldComponents, Dashboard, Layout, LoginPage }: AdminProps) {
const existingConfig = React.useContext(ConfigContext);
const [config, setConfig] = React.useState(existingConfig);
@@ -153,16 +170,14 @@ export default function Admin({ basePath = "/admin", resourceOverrides = {}, pro
const content = (
-
+
);
- // If we have an existing config, we are already inside a Provider and QueryClient
if (existingConfig) {
return content;
}
- // Fallback for standalone usage
return (
{content}
diff --git a/react-openapi/components/EnhancedTable.tsx b/react-openapi/components/EnhancedTable.tsx
index 067e3a2..5b28b54 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 { EnhancedTableComponents } from '../types/overrides';
import { getFieldOptions, toGridValueOptions, resolveTemplate } from '../utils/options';
interface EnhancedTableProps {
@@ -44,6 +45,7 @@ interface EnhancedTableProps {
onDelete: (id: string) => void;
onCreate: () => void;
onNavigateToResource?: (resourceName: string, id: string) => void;
+ components?: EnhancedTableComponents;
}
export default function EnhancedTable({
@@ -57,6 +59,7 @@ export default function EnhancedTable({
onDelete,
onCreate,
onNavigateToResource,
+ components: tableComponents,
}: EnhancedTableProps) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@@ -85,7 +88,7 @@ export default function EnhancedTable({
type: muiType,
flex: 1,
minWidth: 150,
- renderCell: (params: GridRenderCellParams) =>
+ renderCell: (params: GridRenderCellParams) =>
};
if (muiType === 'date' || muiType === 'dateTime') {
@@ -158,6 +161,7 @@ export default function EnhancedTable({
onDelete={onDelete}
onNavigate={onNavigateToResource}
navigate={navigate}
+ components={tableComponents}
/>
))}
@@ -225,7 +229,7 @@ export default function EnhancedTable({
);
}
-function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) {
+function MobileCardRow({ row, config, onDelete, onNavigate, navigate, components }: any) {
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const id = row[config.primaryKey];
@@ -261,7 +265,7 @@ function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) {
{field.label}
-
+
))}
@@ -289,12 +293,17 @@ function getFormattedDisplayValue(item: any, displayField?: string | string[], e
return item[displayField] || item.id || JSON.stringify(item);
}
-function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate, isMobile }: any) {
+function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate, isMobile, components }: any) {
const value = params.value;
const isPk = fieldKey === config.primaryKey;
if (field.formatter) return field.formatter(value);
+ const customRenderer = components?.cellRenderers?.[field.type as string];
+ if (customRenderer) {
+ return React.createElement(customRenderer, { value, row: params.row, field, fieldKey, config, onNavigate, isMobile });
+ }
+
// 1. Single Relation
if (field.relation && value && !Array.isArray(value)) {
const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value;
diff --git a/react-openapi/components/FilterBar.tsx b/react-openapi/components/FilterBar.tsx
index e2b8a3e..f751fe4 100644
--- a/react-openapi/components/FilterBar.tsx
+++ b/react-openapi/components/FilterBar.tsx
@@ -11,9 +11,10 @@ import {
import DoneIcon from "@mui/icons-material/Done";
import FilterListIcon from "@mui/icons-material/FilterList";
import { ResourceField, ResourceMode } from "../types/config";
+import { FilterBarComponents } from "../types/overrides";
import { getFieldOptions, resolveTemplate } from "../utils/options";
-function FilterAutocomplete({
+export function FilterAutocomplete({
options,
value,
label,
@@ -160,8 +161,14 @@ function renderFilterInput(
field: ResourceField,
options: string[],
value: any,
- onChange: (key: string, val: any) => void
+ onChange: (key: string, val: any) => void,
+ components?: FilterBarComponents,
) {
+ const CustomInput = components?.filterInputs?.[fieldName];
+ if (CustomInput) {
+ return onChange("value", val)} options={options} />;
+ }
+
const filterType = field.filterType;
if (filterType === "number-range") {
@@ -208,6 +215,7 @@ export interface FilterBarProps {
appliedValues: Record;
onApply: (values: Record) => void;
onClear: () => void;
+ components?: FilterBarComponents;
}
export default function FilterBar({
@@ -217,6 +225,7 @@ export default function FilterBar({
appliedValues,
onApply,
onClear,
+ components: filterComponents,
}: FilterBarProps) {
const [open, setOpen] = React.useState(false);
const [draft, setDraft] = React.useState>(() => ({ ...appliedValues }));
@@ -294,7 +303,7 @@ export default function FilterBar({
{field.label}
{renderFilterInput(fieldName, field, options, raw, (key, val) =>
- updateDraft(fieldName, key, val)
+ updateDraft(fieldName, key, val), filterComponents
)}
);
diff --git a/react-openapi/components/GenericForm.tsx b/react-openapi/components/GenericForm.tsx
index fe7d1e6..9a51076 100644
--- a/react-openapi/components/GenericForm.tsx
+++ b/react-openapi/components/GenericForm.tsx
@@ -22,7 +22,7 @@ interface GenericFormProps {
loading?: boolean;
readOnly?: boolean;
onEditClick?: () => void;
- fieldComponents?: FieldComponents;
+ fieldComponents: FieldComponents;
}
export default function GenericForm({
@@ -57,7 +57,7 @@ export default function GenericForm({
queries: allRelations.map(relName => {
const relatedRes = appConfig?.resources.find(r => r.name === relName);
// eslint-disable-next-line react-hooks/rules-of-hooks
- const { getListQueryOptions } = useResource(relatedRes!);
+ const { getListQueryOptions } = useResource(relatedRes!, { fieldComponents });
return {
...getListQueryOptions(),
enabled: !!relatedRes,
diff --git a/react-openapi/components/ProfileView.tsx b/react-openapi/components/ProfileView.tsx
index ac76e61..7ebc57d 100644
--- a/react-openapi/components/ProfileView.tsx
+++ b/react-openapi/components/ProfileView.tsx
@@ -3,6 +3,7 @@ import { Box, Typography, Paper, CircularProgress, Alert } from '@mui/material';
import { useResource } from '../hooks/useResource';
import GenericForm from './GenericForm';
import { ConfigContext } from '../providers/ConfigContext';
+import { defaultFieldComponents } from './fields/DefaultFieldComponents';
export default function ProfileView() {
const appConfig = React.useContext(ConfigContext);
@@ -13,11 +14,10 @@ export default function ProfileView() {
return Profile configuration not found.;
}
- // Create a modified config where only extraFields are editable
const editableConfig = React.useMemo(() => {
const newFields = { ...resourceConfig.fields };
const extraFields = profileConfig.extraFields || [];
-
+
Object.keys(newFields).forEach(key => {
newFields[key] = {
...newFields[key],
@@ -31,13 +31,12 @@ export default function ProfileView() {
};
}, [resourceConfig, profileConfig.extraFields]);
- const { useMe, useUpdateMe } = useResource(resourceConfig);
+ const { useMe, useUpdateMe } = useResource(resourceConfig, { fieldComponents: defaultFieldComponents });
const { data: profile, isLoading, error } = useMe();
const updateMutation = useUpdateMe();
const handleSave = async (formData: any) => {
try {
- // Only send editable fields to prevent accidental overwrites of read-only data
const extraFields = profileConfig.extraFields || [];
const dataToSave = Object.keys(formData)
.filter(key => extraFields.includes(key))
@@ -76,6 +75,7 @@ export default function ProfileView() {
onSave={handleSave}
onCancel={() => window.history.back()}
loading={updateMutation.isPending}
+ fieldComponents={defaultFieldComponents}
/>
diff --git a/react-openapi/components/ResourceView.tsx b/react-openapi/components/ResourceView.tsx
index 568bf8f..f3930bf 100644
--- a/react-openapi/components/ResourceView.tsx
+++ b/react-openapi/components/ResourceView.tsx
@@ -5,7 +5,6 @@ import type { ResourceField } from '../types/config';
import { FieldComponents } from '../types/overrides';
import { useResource } from '../hooks/useResource';
import { resolveTemplate } from '../utils/options';
-import GenericForm from './GenericForm';
import EnhancedTable from './EnhancedTable';
import FilterBar from './FilterBar';
import { useParams, useLocation, useNavigate } from 'react-router-dom';
@@ -13,7 +12,7 @@ import { useParams, useLocation, useNavigate } from 'react-router-dom';
interface ResourceViewProps {
config: ResourceConfig;
onNavigateToResource?: (resourceName: string, id: string) => void;
- fieldComponents?: FieldComponents;
+ fieldComponents: FieldComponents;
}
import { GridPaginationModel } from '@mui/x-data-grid';
@@ -117,7 +116,7 @@ export default function ResourceView({ config, onNavigateToResource, fieldCompon
const [appliedFilters, setAppliedFilters] = React.useState>({});
- const { useList, useRead, useCreate, useUpdate, useDelete } = useResource(config);
+ const { useList, useRead, useCreate, useUpdate, useDelete, components } = useResource(config, { fieldComponents });
const queryParams = React.useMemo(() => {
if (!isServer) return { limit: 10000 };
@@ -202,7 +201,7 @@ export default function ResourceView({ config, onNavigateToResource, fieldCompon
) : (
- navigate(`/admin/${config.name}/edit/${id}`)}
- fieldComponents={fieldComponents}
/>
)}
diff --git a/react-openapi/components/fields/DefaultFieldComponents.ts b/react-openapi/components/fields/DefaultFieldComponents.ts
index 48c591a..1c783bd 100644
--- a/react-openapi/components/fields/DefaultFieldComponents.ts
+++ b/react-openapi/components/fields/DefaultFieldComponents.ts
@@ -6,6 +6,7 @@ import DateField from './DateField';
import EnumField from './EnumField';
import RelationField from './RelationField';
import ImageUploadField from './ImageUploadField';
+import FallbackField from './FallbackField';
export const defaultFieldComponents: FieldComponents = {
string: TextFieldEntry,
@@ -17,4 +18,5 @@ export const defaultFieldComponents: FieldComponents = {
enum: EnumField,
image: ImageUploadField,
relation: RelationField,
+ default: FallbackField,
};
diff --git a/react-openapi/components/fields/FallbackField.tsx b/react-openapi/components/fields/FallbackField.tsx
new file mode 100644
index 0000000..f5d6dd8
--- /dev/null
+++ b/react-openapi/components/fields/FallbackField.tsx
@@ -0,0 +1,13 @@
+import { TextField } from '@mui/material';
+import { FieldComponentProps } from '../../types/overrides';
+
+export default function FallbackField({ field, value }: FieldComponentProps) {
+ return (
+
+ );
+}
diff --git a/react-openapi/components/fields/FormField.tsx b/react-openapi/components/fields/FormField.tsx
index 976338a..ce9750f 100644
--- a/react-openapi/components/fields/FormField.tsx
+++ b/react-openapi/components/fields/FormField.tsx
@@ -1,12 +1,9 @@
import * as React from 'react';
-import { TextField as MuiTextField } from '@mui/material';
import { ResourceField } from '../../types/config';
import { FieldComponentProps, FieldComponents } from '../../types/overrides';
-import { defaultFieldComponents } from './DefaultFieldComponents';
import ObjectField from './ObjectField';
-import ImageUploadField from './ImageUploadField';
-interface FormFieldProps {
+export interface FormFieldProps {
name: string;
field: ResourceField;
value: any;
@@ -16,18 +13,7 @@ interface FormFieldProps {
uploading: boolean;
baseUrl: string;
relationDataMap?: Record;
- components?: FieldComponents;
-}
-
-function FallbackField({ field, value }: FieldComponentProps) {
- return (
-
- );
+ components: FieldComponents;
}
export default function FormField({
@@ -40,13 +26,8 @@ export default function FormField({
uploading,
baseUrl,
relationDataMap = {},
- components: componentsProp,
+ components,
}: FormFieldProps) {
- const components = React.useMemo(
- () => ({ ...defaultFieldComponents, ...componentsProp }),
- [componentsProp],
- );
-
const fieldProps: FieldComponentProps = {
name,
field,
@@ -59,6 +40,8 @@ export default function FormField({
uploading,
};
+ const childComponents = components;
+
// 1. Object (recursive) - requires parent FormField for recursion
if (field.type === 'object' && field.schema && !field.relation) {
const renderChild = (childProps: FieldComponentProps) => (
@@ -72,7 +55,7 @@ export default function FormField({
uploading={childProps.uploading!}
baseUrl={childProps.baseUrl!}
relationDataMap={childProps.relationDataMap}
- components={componentsProp}
+ components={components}
/>
);
return ;
@@ -80,22 +63,23 @@ export default function FormField({
// 2. Image
if (field.type === 'image') {
- const ImageField = components.image || ImageUploadField;
+ const ImageField = components.image;
+ if (!ImageField) return null;
return ;
}
// 3. Relation
if (field.relation && relationDataMap[field.relation]) {
- const RelationFieldComp = components.relation || defaultFieldComponents.relation!;
+ const RelationFieldComp = components.relation;
+ if (!RelationFieldComp) return null;
return ;
}
// 4. Lookup by field type
- const Component = components[field.type];
+ const Component = components[field.type] || components.default;
if (Component) {
return ;
}
- // 5. Fallback for unknown types
- return ;
+ return null;
}
diff --git a/react-openapi/components/fields/index.ts b/react-openapi/components/fields/index.ts
index 9af5170..8bc1a85 100644
--- a/react-openapi/components/fields/index.ts
+++ b/react-openapi/components/fields/index.ts
@@ -7,5 +7,6 @@ export { default as DateField } from './DateField';
export { default as EnumField } from './EnumField';
export { default as RelationField } from './RelationField';
export { default as ObjectField } from './ObjectField';
+export { default as FallbackField } from './FallbackField';
export { defaultFieldComponents } from './DefaultFieldComponents';
export type { ObjectFieldProps } from './ObjectField';
diff --git a/react-openapi/hooks/useResource.ts b/react-openapi/hooks/useResource.ts
index 7805654..9dae5e6 100644
--- a/react-openapi/hooks/useResource.ts
+++ b/react-openapi/hooks/useResource.ts
@@ -1,31 +1,44 @@
import { useQuery, useMutation, useQueryClient, keepPreviousData } from "@tanstack/react-query";
+import * as React from "react";
import { api } from "../api/client";
import { ResourceConfig } from "../types/config";
import { ConfigContext } from "../providers/ConfigContext";
-import * as React from "react";
-
-import { FieldComponents } from "../types/overrides";
+import { FieldComponents, FieldComponentProps } from "../types/overrides";
import { defaultFieldComponents } from "../components/fields/DefaultFieldComponents";
+import FormField from "../components/fields/FormField";
+import GenericForm from "../components/GenericForm";
-export function useResource(config: ResourceConfig | undefined, options?: { fieldComponents?: FieldComponents }) {
+function wrapFormField(merged: FieldComponents) {
+ return (props: Omit, 'components'>) =>
+ ;
+}
+
+function wrapGenericForm(merged: FieldComponents) {
+ return (props: Omit, 'fieldComponents'>) =>
+ ;
+}
+
+export function useResource(config: ResourceConfig | undefined, options?: { fieldComponents: FieldComponents }) {
const queryClient = useQueryClient();
-
- // Return empty/disabled hooks if config is missing
+
const { name = '', endpoint = '', primaryKey = 'id' } = config || {};
+ const mergedComponents = React.useMemo(
+ () => options?.fieldComponents ? ({ ...defaultFieldComponents, ...options.fieldComponents }) : undefined,
+ [options?.fieldComponents],
+ );
+
// --- READ ALL ---
- const useList = (params?: any) =>
+ const useList = (params?: any) =>
useQuery({
queryKey: [name, "list", params],
queryFn: async () => {
if (!endpoint) return { data: [], total: 0 };
- console.log('params:', params);
- // @ts-ignore
const res = await api.get(endpoint, { params });
const total = res.headers ? parseInt(res.headers['x-total-count'] || res.headers['X-Total-Count']) : undefined;
- return {
- data: res.data,
- total: isNaN(total as any) ? undefined : total
+ return {
+ data: res.data,
+ total: isNaN(total as any) ? undefined : total
};
},
enabled: !!endpoint,
@@ -38,7 +51,6 @@ export function useResource(config: ResourceConfig | undefined, options
queryKey: [name, "detail", id, params],
queryFn: async () => {
if (!id || !endpoint) return null;
- // @ts-ignore
const res = await api.get(`${endpoint}/${id}`, params ? { params } : undefined);
return res.data;
},
@@ -50,7 +62,6 @@ export function useResource(config: ResourceConfig | undefined, options
useMutation({
mutationFn: async (data: Partial) => {
if (!endpoint) throw new Error("Endpoint not defined");
- // @ts-ignore
const res = await api.post(endpoint, data);
return res.data;
},
@@ -64,12 +75,10 @@ export function useResource(config: ResourceConfig | undefined, options
useMutation({
mutationFn: async ({ id, data }: { id: string; data: Partial }) => {
if (!endpoint) throw new Error("Endpoint not defined");
- // @ts-ignore
const res = await api.put(`${endpoint}/${id}`, data);
return res.data;
},
onSuccess: (updatedItem) => {
- // @ts-ignore
const id = updatedItem[primaryKey];
queryClient.invalidateQueries({ queryKey: [name, "list"] });
queryClient.invalidateQueries({ queryKey: [name, "detail", id] });
@@ -81,15 +90,13 @@ export function useResource(config: ResourceConfig | undefined, options
useMutation({
mutationFn: async ({ id, data }: { id: string; data: Partial }) => {
if (!endpoint) throw new Error("Endpoint not defined");
- // @ts-ignore
const res = await api.patch(`${endpoint}/${id}`, data);
return res.data;
},
onSuccess: (updatedItem) => {
- // @ts-ignore
- const id = updatedItem[primaryKey];
+ const listId = updatedItem[primaryKey];
queryClient.invalidateQueries({ queryKey: [name, "list"] });
- queryClient.invalidateQueries({ queryKey: [name, "detail", id] });
+ queryClient.invalidateQueries({ queryKey: [name, "detail", listId] });
},
});
@@ -111,12 +118,11 @@ export function useResource(config: ResourceConfig | undefined, options
queryKey: [name, "list", params],
queryFn: async () => {
if (!endpoint) return { data: [], total: 0 };
- // @ts-ignore
const res = await api.get(endpoint, { params });
const total = res.headers ? parseInt(res.headers['x-total-count'] || res.headers['X-Total-Count']) : undefined;
- return {
- data: res.data,
- total: isNaN(total as any) ? undefined : total
+ return {
+ data: res.data,
+ total: isNaN(total as any) ? undefined : total
};
},
enabled: !!endpoint,
@@ -128,7 +134,6 @@ export function useResource(config: ResourceConfig | undefined, options
queryKey: [name, "me"],
queryFn: async () => {
if (!endpoint) return null;
- // @ts-ignore
const res = await api.get(`${endpoint}/me`);
return res.data;
},
@@ -140,7 +145,6 @@ export function useResource(config: ResourceConfig | undefined, options
useMutation({
mutationFn: async (data: Partial) => {
if (!endpoint) throw new Error("Endpoint not defined");
- // @ts-ignore
const res = await api.put(`${endpoint}/me`, data);
return res.data;
},
@@ -150,10 +154,14 @@ export function useResource(config: ResourceConfig | undefined, options
},
});
- const components = {
- ...defaultFieldComponents,
- ...options?.fieldComponents,
- };
+ const components = React.useMemo(() => {
+ if (!mergedComponents) return undefined;
+ return {
+ ...mergedComponents,
+ FormField: wrapFormField(mergedComponents),
+ GenericForm: wrapGenericForm(mergedComponents),
+ };
+ }, [mergedComponents]);
return {
useList,
@@ -169,9 +177,8 @@ export function useResource(config: ResourceConfig | undefined, options
};
}
-export function useResourceByName(name: string, options?: { fieldComponents?: FieldComponents }) {
+export function useResourceByName(name: string, options?: { fieldComponents: FieldComponents }) {
const config = React.useContext(ConfigContext);
const resourceConfig = config?.resources.find((r) => r.name === name);
return useResource(resourceConfig, options);
}
-
diff --git a/react-openapi/index.ts b/react-openapi/index.ts
index 604a90d..6f60374 100644
--- a/react-openapi/index.ts
+++ b/react-openapi/index.ts
@@ -2,20 +2,12 @@ export { default as Admin } from "./Admin";
export { api, auth, initializeApiClients } from "./api/client";
export { getAppConfig } from "./config";
export type { AppConfig, ResourceConfig, ResourceField, ResourceMode } from "./types/config";
-export type { FieldComponents, FieldComponentProps, FieldComponent, FieldOverride, ResourceOverride } from "./types/overrides";
+export type { FieldComponents, FieldComponentProps, FieldComponent, FieldOverride, ResourceOverride, EnhancedTableComponents, FilterBarComponents, CellRendererProps, CellRenderer } from "./types/overrides";
export { AppProvider } from "./providers/AppProvider";
export { ConfigContext, useConfig } from "./providers/ConfigContext";
export { useResource, useResourceByName } from "./hooks/useResource";
-export { default as FilterBar } from "./components/FilterBar";
-export {
- defaultFieldComponents,
- FormField,
- TextField,
- NumberField,
- BooleanField,
- DateField,
- EnumField,
- RelationField,
- ObjectField,
- ImageUploadField,
-} from "./components/fields";
+export { default as FilterBar, FilterAutocomplete } from "./components/FilterBar";
+export { default as EnhancedTable } from "./components/EnhancedTable";
+export { default as GenericForm } from "./components/GenericForm";
+export { default as ResourceView } from "./components/ResourceView";
+export { defaultFieldComponents, FormField, TextField, NumberField, BooleanField, DateField, EnumField, RelationField, ObjectField, ImageUploadField, FallbackField } from "./components/fields";
diff --git a/react-openapi/types/overrides.ts b/react-openapi/types/overrides.ts
index b54907c..f376397 100644
--- a/react-openapi/types/overrides.ts
+++ b/react-openapi/types/overrides.ts
@@ -42,4 +42,32 @@ export type FieldComponent = React.ComponentType;
export type FieldComponents = Partial> & {
relation?: FieldComponent;
image?: FieldComponent;
+ default?: FieldComponent;
};
+
+export interface CellRendererProps {
+ value: any;
+ row: any;
+ field: ResourceField;
+ fieldKey: string;
+ config: import('./config').ResourceConfig;
+ onNavigate?: (resourceName: string, id: string) => void;
+ isMobile?: boolean;
+}
+
+export type CellRenderer = React.ComponentType;
+
+export interface EnhancedTableComponents {
+ cellRenderers?: Partial>;
+}
+
+export interface FilterBarComponents {
+ filterInputs?: Record void;
+ options: string[];
+ }>>;
+}
+
+export type { FieldType };
diff --git a/src/FetchRequestDetail.tsx b/src/FetchRequestDetail.tsx
index b55ad77..03e6678 100644
--- a/src/FetchRequestDetail.tsx
+++ b/src/FetchRequestDetail.tsx
@@ -26,8 +26,6 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline";
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
import {
- useFetchRequest,
- useUpdateFetchRequest,
useFetchRequestAmbiguities,
useResolveAmbiguity,
} from "./features/fetch-requests";
@@ -37,7 +35,7 @@ import type {
ProgressMessage,
} from "./features/fetch-requests";
import { RETRY_MAX, formatApiError } from "./features/fetch-requests";
-import { useConfig } from "../react-openapi";
+import { useResourceByName, useConfig, defaultFieldComponents } from "../react-openapi";
const statusColors: Record = {
pending: "default",
@@ -148,8 +146,9 @@ export default function FetchRequestDetail() {
const navigate = useNavigate();
const config = useConfig();
- const { data: fetchRequest, isLoading, error: fetchError, refetch: refetchRequest } = useFetchRequest(id!);
- const updateMutation = useUpdateFetchRequest();
+ const { useRead, usePatch } = useResourceByName("fetch-requests", { fieldComponents: defaultFieldComponents });
+ const { data: fetchRequest, isLoading, error: fetchError, refetch: refetchRequest } = useRead(id!);
+ const updateMutation = usePatch();
const resolveMutation = useResolveAmbiguity();
const { data: ambiguities, refetch: refetchAmbiguities } = useFetchRequestAmbiguities(id!);
diff --git a/src/FetchRequests.tsx b/src/FetchRequests.tsx
index 2abfc6f..2c6f006 100644
--- a/src/FetchRequests.tsx
+++ b/src/FetchRequests.tsx
@@ -4,16 +4,9 @@ import {
Container,
Paper,
Typography,
- TextField,
Button,
ToggleButtonGroup,
ToggleButton,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
Chip,
IconButton,
CircularProgress,
@@ -25,6 +18,7 @@ import {
DialogContentText,
DialogActions,
Tooltip,
+ TextField,
Select,
MenuItem,
InputLabel,
@@ -43,10 +37,6 @@ import ScheduleIcon from "@mui/icons-material/Schedule";
import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty";
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import {
- useFetchRequestsList,
- useCreateFetchRequest,
- useUpdateFetchRequest,
- useDeleteFetchRequest,
useUploadFile,
} from "./features/fetch-requests";
import type {
@@ -57,7 +47,8 @@ import type {
} from "./features/fetch-requests";
import { RETRY_MAX, formatApiError } from "./features/fetch-requests";
import { useNavigate } from "react-router-dom";
-import { useResourceByName, useConfig } from "../react-openapi";
+import { useResourceByName, useConfig, defaultFieldComponents } from "../react-openapi";
+import type { ResourceField } from "../react-openapi";
const statusColors: Record = {
pending: "default",
@@ -85,14 +76,14 @@ function formatDate(iso: string) {
}
function formatDateRange(start?: string, end?: string) {
- if (!start && !end) return "—";
+ if (!start && !end) return "\u2014";
const s = start ? new Date(start).toLocaleDateString() : "?";
const e = end ? new Date(end).toLocaleDateString() : "?";
- return `${s} → ${e}`;
+ return `${s} \u2192 ${e}`;
}
function shortId(fp: string) {
- return fp.length > 8 ? fp.slice(0, 8) + "…" : fp;
+ return fp.length > 8 ? fp.slice(0, 8) + "\u2026" : fp;
}
export default function FetchRequests() {
@@ -116,11 +107,13 @@ export default function FetchRequests() {
const [accountFilter, setAccountFilter] = React.useState("");
const [sourceFilter, setSourceFilter] = React.useState<"all" | "file" | "email">("all");
- const { data: listData, isLoading, isFetching, refetch } = useFetchRequestsList({
+ const { useList, useCreate, usePatch, useDelete, components } = useResourceByName("fetch-requests", { fieldComponents: defaultFieldComponents });
+ const { data: listData, isLoading, isFetching, refetch } = useList({
...(statusFilter.length > 0 ? { status: statusFilter.join(",") } : {}),
...(accountFilter ? { account_name: accountFilter } : {}),
...(sourceFilter !== "all" ? { source_type: sourceFilter } : {}),
});
+
const { useList: useAccountsList } = useResourceByName("accounts");
const { data: accountsData } = useAccountsList();
const accountOptions: string[] = React.useMemo(() => {
@@ -129,11 +122,15 @@ 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[] ?? [];
+ const formatField: ResourceField | undefined = fetchRes?.fields?.source?.schema?.fields?.format;
+ const formatOptions: string[] = formatField?.options ?? formatField?.schema?.options as string[] ?? [];
+ const startDateField: ResourceField | undefined = fetchRes?.fields?.start_date;
+ const endDateField: ResourceField | undefined = fetchRes?.fields?.end_date;
+ const payorUsernameField: ResourceField | undefined = fetchRes?.fields?.payor_username;
- const createMutation = useCreateFetchRequest();
- const updateMutation = useUpdateFetchRequest();
- const deleteMutation = useDeleteFetchRequest();
+ const createMutation = useCreate();
+ const updateMutation = usePatch();
+ const deleteMutation = useDelete();
const uploadMutation = useUploadFile();
const requests = listData?.data ?? [];
@@ -178,7 +175,7 @@ export default function FetchRequests() {
navigate(`/fetch-requests/${result.id}`);
} catch (err: any) {
if (err?.response?.status === 409) {
- setSnackbar({ message: "Duplicate — same fingerprint already exists", severity: "error" });
+ setSnackbar({ message: "Duplicate \u2014 same fingerprint already exists", severity: "error" });
} else {
setSnackbar({ message: formatApiError(err) || "Failed to create fetch request", severity: "error" });
}
@@ -265,25 +262,43 @@ export default function FetchRequests() {
Uploaded as: {uploadedPath}
)}
-
- Format
-
-
+ {formatField && components?.FormField ? (
+
+ ) : (
+
+ Format
+
+
+ )}
>
) : (
<>
-
- Format
-
-
+ {formatField && components?.FormField ? (
+
+ ) : (
+
+ Format
+
+
+ )}
setFromEmail(e.target.value)} size="small" />
setSubject(e.target.value)} size="small" />
setRawTerms(e.target.value)} size="small" helperText="Comma-separated search terms" />
@@ -299,29 +314,60 @@ export default function FetchRequests() {
)}
sx={{ "& .MuiOutlinedInput-root": { height: "auto", minHeight: "2.5rem" } }}
/>
- setPayorUsername(e.target.value)} size="small" helperText="Default: aetos" />
+ {payorUsernameField && components?.FormField ? (
+
+ ) : (
+ setPayorUsername(e.target.value)} size="small" helperText="Default: aetos" />
+ )}
- setStartDate(e.target.value)}
- size="small"
- InputLabelProps={{ shrink: true }}
- inputProps={{ max: new Date().toISOString().split("T")[0] }}
- sx={{ flex: 1 }}
- />
- setEndDate(e.target.value)}
- size="small"
- InputLabelProps={{ shrink: true }}
- inputProps={{ max: new Date().toISOString().split("T")[0] }}
- sx={{ flex: 1 }}
- />
+ {startDateField && components?.DateField ? (
+
+
+
+ ) : (
+ setStartDate(e.target.value)}
+ size="small"
+ InputLabelProps={{ shrink: true }}
+ inputProps={{ max: new Date().toISOString().split("T")[0] }}
+ sx={{ flex: 1 }}
+ />
+ )}
+ {endDateField && components?.DateField ? (
+
+
+
+ ) : (
+ setEndDate(e.target.value)}
+ size="small"
+ InputLabelProps={{ shrink: true }}
+ inputProps={{ max: new Date().toISOString().split("T")[0] }}
+ sx={{ flex: 1 }}
+ />
+ )}