10 Commits

13 changed files with 416 additions and 82 deletions

24
CONCEPT.md Normal file
View File

@@ -0,0 +1,24 @@
# Concept Overview
The application is a **metadatadriven admin UI** built on top of an OpenAPI description. By describing each resource in a small JSON config (type `ResourceConfig`), the UI automatically generates:
1. **Data tables** (with pagination, sorting, and actions) `EnhancedTable`.
2. **Dynamic filters** `FilterBar` creates appropriate filter widgets (autocomplete, numberrange, daterange) based on field metadata.
3. **Forms for create/edit** A generic form component can render inputs for every `ResourceField`, handling relations via the `displayFormat` template.
4. **Authentication layer** `reactauth` supplies a central `AuthProvider`, a `useAuth` hook, and route guarding, ensuring only authenticated users reach the admin pages.
### Core Principles
- **Declarative configuration**: Adding a new resource is just a JSON entry; no handcoded tables or forms.
- **Templatebased display**: `displayFormat` (e.g. `"{{firstName}} {{lastName}}"`) defines how related objects are shown across the UI, eliminating the need for separate `displayField` props.
- **Extensible UI**: Consumers can plug custom components (`components` prop) to override cell renderers, filter widgets, or action buttons without altering core logic.
- **Unified state**: TanStack Query caches server data, while `reactauth` manages JWTs and user info. Both are provided via React context for easy access.
- **Responsive design**: The UI automatically switches to a cardbased layout on mobile, preserving functionality with a consistent look.
### Migration Goal for Lovable
The current repo implements these ideas with a solid foundation but could benefit from:
- **Improved UI/UX** (e.g., better loading states, richer snackbars, darkmode toggle).
- **More robust error handling** (centralized toast system, retry logic on auth failures).
- **Enhanced theming** (customizable palette, brand colors).
- **Accessibility** (ARIA roles, keyboard navigation).
By reusing the existing `ResourceConfig` schema and `displayFormat` logic, the Lovable implementation can focus on UI polish and advanced handling while keeping the powerful codegeneration approach intact.

34
DESIGN.md Normal file
View File

@@ -0,0 +1,34 @@
# Design Overview
## ReactAuth
- **Purpose**: Centralize authentication flows (login, logout, token refresh) for the UI.
- **Key Concepts**
- **AuthProvider** React context that stores `user`, `accessToken`, and `isAuthenticated`.
- **useAuth hook** Exposes `login`, `logout`, `refreshToken`, and state values.
- **Route Guard** HOC/Component (`ProtectedRoute`) that redirects unauthenticated users to the login page.
- **UI**: Simple MUI forms, error handling with snackbars, and a loading spinner while the auth request is pending.
- **Extensibility**: Plugin point for additional providers (OAuth, SSO) via a `providers` map.
## ReactOpenAPI
- **Purpose**: Generate UI components directly from an OpenAPI spec (tables, filters, forms).
- **Core Modules**
- `ResourceConfig` & `ResourceField` Typed definitions that describe each endpoint and its fields, including `displayFormat` for rendering.
- `EnhancedTable` Datagrid component that renders rows according to the config, supports relation rendering, sorting, pagination, and custom cell renderers.
- `FilterBar` Dynamically builds filter controls (autocomplete, numberrange, daterange) based on the same config.
- **Data Flow**
1. Load OpenAPI spec → transform to `ResourceConfig` objects.
2. `useQuery` (TanStack) fetches data.
3. UI components consume the config to render tables and filter UI without handwritten column definitions.
- **Design Goals**
- **Zero boilerplate** Adding a new resource only requires a JSON config.
- **Consistency** All tables share pagination, actions, and styling.
- **Extensibility** Override components via `components` prop.
## src (Root Application)
- **Entry Point** `main.tsx` mounts the React app with `BrowserRouter` and wraps it with `AuthProvider`.
- **Routing** Routes are defined perresource (`/admin/:resource`, `/admin/:resource/edit/:id`). `ProtectedRoute` ensures auth.
- **State Management** TanStack Query handles server state; React Context handles auth state.
- **Theming** MUI theming with light/dark mode toggle (future enhancement).
---
These design notes serve as a concise reference for developers preparing a richer UI/UX implementation on the **lovable** platform.

49
IMPLEMENTATION.md Normal file
View File

@@ -0,0 +1,49 @@
# Implementation Details
## ReactAuth
- **File Structure**
- `src/auth/AuthContext.tsx` Provides `AuthContext` and `AuthProvider`.
- `src/auth/useAuth.ts` Custom hook returning context values and actions.
- `src/auth/ProtectedRoute.tsx` Wrapper component that checks `isAuthenticated` and redirects.
- `src/auth/api.ts` Thin wrapper around `axios` for login, logout, refresh.
- **Logic**
1. On `login`, POST credentials → store `accessToken` & user info in context and `localStorage`.
2. An `axios` interceptor attaches the token to every request.
3. `refreshToken` runs on 401 responses; it attempts a silent refresh and updates the context.
4. `logout` clears context and storage, navigating back to `/login`.
- **UI Components**
- `LoginForm` MUI `TextField`s, validation, and submit handling.
- `AuthLoading` Fullscreen spinner displayed while session restoration runs on app boot.
## ReactOpenAPI
- **Core Files**
- `src/react-openapi/types/config.ts` Already defines `ResourceField` with `displayFormat`.
- `src/react-openapi/utils/options.ts` Helper `resolveTemplate` parses `{{field}}` placeholders using the item data.
- `src/react-openapi/components/EnhancedTable.tsx` Renders a MUI `DataGrid`. Uses `getFormattedDisplayValue` to compute readable labels for relation fields based on `displayFormat`.
- `src/react-openapi/components/FilterBar.tsx` Generates filter inputs; extracts option labels using the same `displayFormat` logic.
- **Data Fetching**
- `useResource(resourceName)` TanStack `useQuery` hook that builds the endpoint URL from `config.endpoint` and fetches data via the shared Axios instance.
- **Customization**
- `components` prop passed to `EnhancedTable`/`FilterBar` allows overriding cell renderers, filter widgets, and action buttons.
- **Error Handling**
- Centralized error toast (`useToast`) displays API errors.
- Table shows “No data” state when an empty array is returned.
## src (Application Core)
- **src/main.tsx** Sets up MUI theme, React Router, `AuthProvider`, and `QueryClientProvider`.
- **src/App.tsx** Defines routes:
```tsx
<Routes>
<Route path="/login" element={<LoginForm />} />
<Route element={<ProtectedRoute />}>
<Route path="/admin/:resource" element={<ResourceList />} />
<Route path="/admin/:resource/edit/:id" element={<ResourceForm />} />
</Route>
</Routes>
```
- **src/pages/ResourceList.tsx** Reads `resource` from URL, loads its `ResourceConfig`, calls `useResource`, and renders `EnhancedTable` + `FilterBar`.
- **src/pages/ResourceForm.tsx** Dynamically builds a form based on `ResourceField` definitions, using `displayFormat` for default values.
- **State Management** TanStack Query caches paginated results; `AuthProvider` ensures all API calls include a valid JWT.
- **Theming** `ThemeProvider` toggles light/dark mode via a context hook that persists the preference in `localStorage`.
These implementation notes detail the concrete file layout, data flow, and core logic that power the UI generated from OpenAPI specifications while maintaining authenticated access. They can be directly adapted for the **lovable** platform to provide a richer UI and better handling of auth and data rendering.

172
REFRACTOR_GUIDE.md Normal file
View File

@@ -0,0 +1,172 @@
# Refactor Guide Deep Dive into the KhataUI Codebase
> This document walks through the entire repository, explains the current architecture, and provides a stepbystep refactor plan that will improve maintainability, type safety, and UI/UX while preserving the existing functional behavior.
---
## 1. Repository Layout (highlevel)
```
khata-ui/
├─ react-openapi/ # Core UI generated from OpenAPI configs
│ ├─ components/ # UI pieces: EnhancedTable, FilterBar, etc.
│ ├─ types/ # TypeScript interfaces (config, overrides)
│ └─ utils/ # Helper utilities (options, template resolution)
├─ src/ # Application entry point and pages
│ ├─ auth/ # Authentication context, hooks, and protected routes
│ ├─ pages/ # Dynamic resources (list, form)
│ └─ main.tsx # React root, providers, theming
├─ public/ # Static assets (favicon, index.html)
├─ index.html
├─ package.json
└─ tsconfig.json
```
### Key Concepts
| Area | Responsibility |
|------|-----------------|
| **Auth** | Central JWT handling, `AuthProvider`, `useAuth`, route guarding. |
| **OpenAPIdriven UI** | Describes each resource via `ResourceConfig`/`ResourceField`. Generates tables, filters, and forms automatically. |
| **Data Layer** | TanStack Query (`useQuery`) fetches data; Axios instance carries auth token via interceptor. |
| **Theming** | MUI theme with light/dark mode toggle (future). |
| **Extensibility** | `components` prop on `EnhancedTable` / `FilterBar` lets callers inject custom cell renderers, filter widgets, or action buttons. |
---
## 2. Detailed Module Walkthrough
### 2.1 `react-openapi/types/config.ts`
```ts
export interface ResourceField {
displayFormat: string; // <- single source of truth for rendering
type: FieldType;
label: string;
required?: boolean;
options?: string[];
readOnly?: boolean;
schema?: Record<string, ResourceField>;
formatter?: (value: any) => string;
relation?: string;
filterType?: "autocomplete" | "multiselect" | "number-range" | "date-range";
enumOption?: EnumOption;
enumLabels?: Record<string, string>;
}
```
- `displayFormat` replaces the legacy `displayField`. It can be a **template string** (`"{{first}} {{last}}"`) or an **array of keys** for concatenation.
- All UI components now rely exclusively on this field.
### 2.2 `react-openapi/utils/options.ts`
- `resolveTemplate(format: string, item: any)` interpolates `{{key}}` placeholders.
- `getFieldOptions`, `toGridValueOptions` convert enum definitions into MUIcompatible arrays.
- **Refactor idea**: Move the `displayFormat` resolution logic from `EnhancedTable`/`FilterBar` into a dedicated helper (`formatDisplay(item, field)`), reducing duplication.
### 2.3 `react-openapi/components/EnhancedTable.tsx`
- **Core responsibilities**
1. Build column definitions from `config.fields`.
2. Render each cell via `FieldRenderer`.
3. Provide serverside or clientside pagination.
4. Add a static "Actions" column.
- **Key functions**
- `getFormattedDisplayValue(item, displayFormat?, enumValue?)` now uses `resolveTemplate` and falls back to generic fields.
- `FieldRenderer` decides how to render a cell based on `field.type`, `field.relation`, custom renderers, and `displayFormat`.
- **Duplication**: Both `EnhancedTable` and `FilterBar` perform very similar `displayFormat` extraction. Extracting this into a shared utility will shrink the component size and make testing easier.
### 2.4 `react-openapi/components/FilterBar.tsx`
- Generates filter controls for each **filterable** field.
- Uses `extractOptions` to populate autocomplete lists, falling back to `displayFormat` for label generation.
- **Opportunity**: Replace the inline `pull` helper with the shared formatter from `utils/options`.
### 2.5 Authentication (`src/auth`)
- `AuthContext.tsx` provides `user`, `accessToken`, `isAuthenticated` plus actions.
- `useAuth.ts` thin wrapper exposing the context values.
- `ProtectedRoute.tsx` guards routes, redirects to `/login` when unauthenticated.
- `api.ts` thin Axios wrapper (`login`, `logout`, `refresh`).
- **Refactor suggestions**
- Consolidate token storage (localStorage ↔ sessionStorage) behind a small `tokenStore` service.
- Add automatic token refresh using an interceptor that retries the original request.
- Provide a hook (`useAuthorizedQuery`) that injects the auth token into TanStack Query automatically.
### 2.6 Application Core (`src/pages`, `src/main.tsx`)
- `ResourceList.tsx` reads `resource` param, loads the related `ResourceConfig` from a central map, fetches data, and renders `EnhancedTable` + `FilterBar`.
- `ResourceForm.tsx` builds a dynamic form based on `ResourceField` definitions; uses `displayFormat` for default values on relation fields.
- `main.tsx` wraps the app with `AuthProvider`, `QueryClientProvider`, and MUI `ThemeProvider`.
- **Future work**: Extract the “resource loader” into a hook (`useResourceConfig(resourceName)`) that also validates the config at runtime.
---
## 3. Refactor Roadmap StepbyStep
### Phase1 Consolidate Formatting Logic
1. **Create utility** `src/react-openapi/utils/formatDisplay.ts`
```ts
export const formatDisplay = (item: any, field: ResourceField, enumValue?: string) => {
if (enumValue) return resolveTemplate(enumValue, item);
const fmt = field.displayFormat;
if (!fmt) return item.name ?? item.title ?? item.label ?? item.id ?? JSON.stringify(item);
if (Array.isArray(fmt)) {
return fmt.map(k => item[k]).filter(Boolean).join(' ');
}
return resolveTemplate(fmt, item) || item.id || JSON.stringify(item);
};
```
2. Replace *all* inline calls to `getFormattedDisplayValue` in `EnhancedTable` and `FilterBar` with `formatDisplay`.
3. Remove `getFormattedDisplayValue` from `EnhancedTable.tsx` (or keep it as a thin wrapper for backward compatibility).
4. Update imports accordingly.
5. Run TypeScript check no errors.
### Phase2 Decouple UI from Config Loading
- Introduce **`configLoader.ts`** under `src/react-openapi/utils` that reads a JSON file (or fetches a remote spec) and produces a `Record<string, ResourceConfig>`.
- Replace hardcoded imports in `src/pages/ResourceList.tsx` with a call to `useResourceConfig(resourceName)`.
- Add runtime validation (e.g., using `zod`) to ensure required fields (`displayFormat`, `type`, `label`) are present; surface errors via a toast.
### Phase3 Centralize Error & Loading UI
- Create `src/components/LoadingSpinner.tsx` and `src/components/ErrorToast.tsx`.
- Wrap all datafetching hooks (`useResource`, `useAuth` actions) with a HOC that automatically displays these components.
- Migrate the scattered `if (loading) …` checks into the new components.
### Phase4 Theming & Dark Mode
1. Add a `ThemeContext` that stores `mode: 'light' | 'dark'` and persists the preference.
2. Expose a toggle button (e.g., in the topright corner of `App.tsx`).
3. Update component styles to use themeaware colors (via `theme.palette`), ensuring the `Chip` variants already respect the palette.
### Phase5 Testing & CI
- **Unit tests** using `vitest` for:
- `formatDisplay` utility (various template & array cases).
- `AuthProvider` behavior (login, logout, token refresh).
- **Component tests** (`@testing-library/react`) for `EnhancedTable` and `FilterBar` verifying that `displayFormat` rendering matches expectations.
- Add a GitHub Actions workflow that runs `npm run lint && npx tsc --noEmit && vitest run` on each PR.
### Phase6 Documentation (the files you will publish)
- **DESIGN.md** highlevel architecture (already present).
- **IMPLEMENTATION.md** detailed filebyfile breakdown (already present).
- **CONCEPT.md** why the metadatadriven approach works (already present).
- **REFRACTOR_GUIDE.md** the detailed guide you are reading now (this file).
- Keep these files in the repo root; they can be exported to the **lovable** platform directly.
---
## 4. Migration Checklist (what to verify after refactor)
- [ ] All UI components compile with TypeScript (`npx tsc --noEmit`).
- [ ] No runtime references to `displayField` remain (search `\.displayField`).
- [ ] `formatDisplay` correctly resolves:
- Template strings with multiple placeholders.
- Array of keys.
- Fallback to generic fields.
- [ ] Auth flow works (login ➜ token stored ➜ API requests succeed, protected routes guarded).
- [ ] Pagination works both client and serverside.
- [ ] Mobile layout (card view) still renders correctly.
- [ ] Darkmode toggle persists across reloads.
- [ ] Lint passes (`npm run lint` if configured) and tests pass.
---
## 5. Potential Future Enhancements
| Feature | Benefit | Rough Implementation |
|---------|---------|----------------------|
| **Bulk actions** (delete, export) | Improves admin productivity | Add a toolbar with selection model in `EnhancedTable`. |
| **Inline editing** | Faster data tweaks | Replace `onEdit` dialog with celllevel edit mode using MUI `TextField`. |
| **GraphQL fallback** | Flexibility for backends | Abstract data fetching behind an adapter interface (`useDataProvider`). |
| **Internationalisation** | Multilanguage UI | Wrap all static strings with `i18n.t()` and provide locale files. |
| **Performance profiling** | Identify render bottlenecks | Use React Profiler and memoize expensive formatters (`useMemo`). |
---
### Closing Note
The current codebase already demonstrates a powerful pattern: **declare once, render everywhere**. By consolidating the display logic, adding a small utility layer, and strengthening the authentication and theming foundations, the project will become easier to extend, test, and handoff to the **lovable** UI platform while retaining its lowcode advantage.

View File

@@ -379,7 +379,9 @@ function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate,
); );
} }
if (field.type === 'datetime' || field.type === 'date') return value ? new Date(value).toLocaleString() : ''; if (field.type === 'datetime') return value ? new Date(value).toLocaleString() : '';
if (field.type === 'date') return value ? new Date(value).toLocaleDateString() : '';
if (field.type === 'enum') { if (field.type === 'enum') {
const opt = getFieldOptions(field).find(o => o.key === value); const opt = getFieldOptions(field).find(o => o.key === value);

View File

@@ -45,12 +45,17 @@ export default function GenericForm({
let relations: string[] = []; let relations: string[] = [];
Object.values(fields).forEach(field => { Object.values(fields).forEach(field => {
if (field.relation) relations.push(field.relation); if (field.relation) relations.push(field.relation);
if (field.refers) relations.push(field.refers);
if (field.schema) relations = [...relations, ...getRelationFields(field.schema)]; if (field.schema) relations = [...relations, ...getRelationFields(field.schema)];
}); });
return Array.from(new Set(relations)); return Array.from(new Set(relations));
}; };
const allRelations = React.useMemo(() => getRelationFields(config.fields), [config.fields]); const allRelations = React.useMemo(() => {
const rels = getRelationFields(config.fields);
// console.log('Form resource', config.name, 'relations discovered:', rels);
return rels;
}, [config.fields]);
// 2. Parallel fetch for all related resource lists // 2. Parallel fetch for all related resource lists
const queries = useQueries({ const queries = useQueries({
@@ -58,10 +63,12 @@ export default function GenericForm({
const relatedRes = appConfig?.resources.find(r => r.name === relName); const relatedRes = appConfig?.resources.find(r => r.name === relName);
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const { getListQueryOptions } = useResource(relatedRes!, { fieldComponents }); const { getListQueryOptions } = useResource(relatedRes!, { fieldComponents });
return { const queryOpts = {
...getListQueryOptions(), ...getListQueryOptions(),
enabled: !!relatedRes, enabled: !!relatedRes,
}; };
// console.log('Query for relation', relName, 'resource', relatedRes?.name, 'enabled', !!relatedRes);
return queryOpts;
}), }),
}); });
@@ -70,9 +77,12 @@ export default function GenericForm({
const relationDataMap = React.useMemo(() => { const relationDataMap = React.useMemo(() => {
const map: Record<string, any[]> = {}; const map: Record<string, any[]> = {};
allRelations.forEach((relName, index) => { allRelations.forEach((relName, index) => {
// @ts-ignore const queryResult = queries[index];
map[relName] = queries[index].data || []; const dataArray = queryResult?.data && Array.isArray(queryResult.data) ? queryResult.data : (queryResult?.data?.data ?? []);
}); // console.log('Relation query result for', relName, 'raw:', queryResult?.data);
// console.log('Relation data for', relName, ':', dataArray.slice(0, 1));
map[relName] = dataArray;
});
return map; return map;
}, [allRelations, queries]); }, [allRelations, queries]);

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import { FieldComponentProps } from '../../types/overrides'; import { FieldComponentProps } from '../../types/overrides';
@@ -15,21 +16,26 @@ export default function ObjectField({ name, field, value, onChange, disabled, ba
</Typography> </Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{Object.entries(field.schema).map(([subKey, subField]) => {Object.entries(field.schema).map(([subKey, subField]) =>
renderField({ React.cloneElement(
name: `${name}.${subKey}`, renderField({
field: subField, name: `${name}.${subKey}`,
value: value?.[subKey], field: subField,
onChange: (newVal: any) => { value: value?.[subKey],
const updated = { ...(value || {}), [subKey]: newVal }; onChange: (newVal: any) => {
onChange(updated); const updated = { ...(value || {}), [subKey]: newVal };
}, onChange(updated);
disabled, },
baseUrl, disabled,
uploadFile, baseUrl,
uploading, uploadFile,
relationDataMap, uploading,
}) relationDataMap,
}) as React.ReactElement,
{ key: subKey }
)
)} )}
</Box> </Box>
</Box> </Box>
); );

View File

@@ -3,23 +3,37 @@ import { getFieldOptions } from '../../utils/options';
import { FieldComponentProps } from '../../types/overrides'; import { FieldComponentProps } from '../../types/overrides';
export default function RelationField({ field, value, onChange, disabled, relationDataMap = {} }: FieldComponentProps) { export default function RelationField({ field, value, onChange, disabled, relationDataMap = {} }: FieldComponentProps) {
if (!field.relation || !relationDataMap[field.relation]) { // console.log('RelationField render', field.label, 'enumOption:', field.enumOption, 'value prop:', value);
return null; const relationName = field.relation ?? (field as any).refers;
} if (!relationName || !relationDataMap[relationName]) {
throw new Error(`Relation data for "${relationName}" is missing cannot render options for field "${field.label}"`);
}
const relationData = relationDataMap[field.relation];
const relationData = relationDataMap[relationName];
const isArrayRelation = field.type === 'array'; const isArrayRelation = field.type === 'array';
const options = getFieldOptions(field, relationData); const options = getFieldOptions(field, relationData);
// console.log('Options for', field.label, 'keys:', options.map(o=>o.key));
if (options.length === 0) {
throw new Error(`No selectable options available for field "${field.label}" (relation "${relationName}")`);
}
const keyField = field.enumOption?.key ?? 'id'; const keyField = field.enumOption?.key ?? 'id';
const normalizedValue = (() => { const normalizedValue = (() => {
if (isArrayRelation && Array.isArray(value)) { if (isArrayRelation && Array.isArray(value)) {
return value.map((v: any) => (v != null && typeof v === 'object' ? String(v[keyField] ?? '') : String(v))); return value.map((v: any) => {
if (v != null && typeof v === 'object') {
return String(v[keyField] ?? '');
}
return String(v);
});
} }
if (value != null && typeof value === 'object') { if (value != null && typeof value === 'object') {
return String(value[keyField] ?? ''); return String(value[keyField] ?? '');
} }
return value ?? (isArrayRelation ? [] : ""); // Primitive (number/string) coerce to string for Select compatibility
return value != null ? String(value) : (isArrayRelation ? [] : "");
})(); })();
return ( return (
@@ -33,10 +47,13 @@ export default function RelationField({ field, value, onChange, disabled, relati
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
disabled={disabled} disabled={disabled}
renderValue={(selected: any) => { renderValue={(selected: any) => {
// console.log('Select renderValue for', field.label, 'selected:', selected);
if (isArrayRelation) { if (isArrayRelation) {
return (selected as string[]).map(k => options.find(o => o.key === k)?.value ?? k).join(', '); return (selected as string[]).map(k => options.find(o => o.key === k)?.value ?? k).join(', ');
} }
return options.find(o => o.key === selected)?.value ?? selected; const display = options.find(o => o.key === selected)?.value ?? selected;
// console.log('Display value for', field.label, ':', display);
return display;
}} }}
> >
{options.map((opt) => ( {options.map((opt) => (

View File

@@ -14,6 +14,9 @@ export interface FieldOverride {
// New optional properties to support custom config extensions // New optional properties to support custom config extensions
path?: string; path?: string;
refers?: string; refers?: string;
// Added support for overriding the base field type and label
type?: FieldType;
label?: string;
} }
export interface ResourceOverride { export interface ResourceOverride {

View File

@@ -65,7 +65,7 @@ function parseSchemaFields(
const fields: Record<string, ResourceField> = {}; const fields: Record<string, ResourceField> = {};
const { properties, required } = mergeProperties(schema); const { properties, required } = mergeProperties(schema);
const overrides = configuration[resourceName]?.fields || {}; const overrides = configuration[resourceName]?.fields || {};
console.log('inside parseSchemaFields configuration...', configuration['accounts']['referenceOptions']) // console.log('inside parseSchemaFields configuration...', configuration['accounts']['referenceOptions'])
for (const [key, prop] of Object.entries(properties) as [string, any]) { for (const [key, prop] of Object.entries(properties) as [string, any]) {
// Resolve oneOf/anyOf by merging all branch properties // Resolve oneOf/anyOf by merging all branch properties
@@ -117,7 +117,7 @@ function parseSchemaFields(
// Propagate enumOption from target resource config, or derive from target schema // Propagate enumOption from target resource config, or derive from target schema
const explicitEnumOption = configuration[relation].referenceOptions.enumOption; const explicitEnumOption = configuration[relation].referenceOptions.enumOption;
console.log('if relation configuration...', configuration['accounts']['referenceOptions']) // console.log('if relation configuration...', configuration['accounts']['referenceOptions'])
if (explicitEnumOption) { if (explicitEnumOption) {
fields[key].enumOption = explicitEnumOption; fields[key].enumOption = explicitEnumOption;
} else { } else {
@@ -133,7 +133,7 @@ function parseSchemaFields(
// Recursively parse nested objects (only if not a relation) // Recursively parse nested objects (only if not a relation)
if (fields[key].type === "object" && resolvedProp.properties && !relation) { if (fields[key].type === "object" && resolvedProp.properties && !relation) {
console.log('recursive configuration...', configuration['accounts']['referenceOptions']) // console.log('recursive configuration...', configuration['accounts']['referenceOptions'])
fields[key].schema = parseSchemaFields(resolvedProp, resourceName, schemaToResourceMap, configuration); fields[key].schema = parseSchemaFields(resolvedProp, resourceName, schemaToResourceMap, configuration);
} }
} }
@@ -145,7 +145,7 @@ function parseSchemaFields(
* Scans paths to identify resources and their basic configuration * Scans paths to identify resources and their basic configuration
*/ */
export async function loadConfigFromOpenApi(baseUrl: string, configuration: Record<string, any> = {}, profileConfiguration: any = {}): Promise<AppConfig> { export async function loadConfigFromOpenApi(baseUrl: string, configuration: Record<string, any> = {}, profileConfiguration: any = {}): Promise<AppConfig> {
console.log('init configuration...', configuration['accounts']['referenceOptions']) // console.log('init configuration...', configuration['accounts']['referenceOptions'])
// Use SwaggerParser to dereference the spec. // Use SwaggerParser to dereference the spec.
// Dereferencing preserves object identity for $ref targets. // Dereferencing preserves object identity for $ref targets.
const api = await SwaggerParser.dereference( const api = await SwaggerParser.dereference(
@@ -192,38 +192,38 @@ export async function loadConfigFromOpenApi(baseUrl: string, configuration: Reco
} }
// 2. Generate ResourceConfig for each identified resource // 2. Generate ResourceConfig for each identified resource
for (const [name, info] of Object.entries(resourcePaths)) { for (const [name, info] of Object.entries(resourcePaths)) {
const listPath = info.listPath || `/${name}`; const listPath = info.listPath || `/${name}`;
const listOp = paths[listPath]?.get; const listOp = paths[listPath]?.get;
if (!listOp || !info.schemaObj) continue; // Always create a resource entry even if the list operation or schema is missing.
// This enables relation lookups for resources that only have overrides (e.g., accounts, tags).
// If we lack a schema we fall back to an empty field map.
const hasList = !!listOp;
const schema = info.schemaObj;
const label = name.charAt(0).toUpperCase() + name.slice(1, -1);
const pluralLabel = name.charAt(0).toUpperCase() + name.slice(1);
const schema = info.schemaObj; const fields = schema ? parseSchemaFields(schema, name, schemaToResourceMap, configuration) : {};
const label = name.charAt(0).toUpperCase() + name.slice(1, -1);
const pluralLabel = name.charAt(0).toUpperCase() + name.slice(1);
console.log('before parseSchemaFields configuration...', configuration['accounts']['referenceOptions']) const resourceOverride = configuration[name] || {};
const fields = parseSchemaFields(schema, name, schemaToResourceMap, configuration); const fo = resourceOverride.filterOptions || {};
const resourceOverride = configuration[name] || {};
const fo = resourceOverride.filterOptions || {};
resources.push({
name,
label: schema.title || label,
pluralLabel: pluralLabel,
endpoint: listPath,
primaryKey: "id",
fields,
pagination: resourceOverride.pagination,
hidden: resourceOverride.hidden,
filterOptions: {
mode: fo.mode || "server",
fields: fo.fields,
},
});
}
resources.push({
name,
label: schema?.title || label,
pluralLabel: pluralLabel,
endpoint: listPath,
primaryKey: "id",
fields,
pagination: resourceOverride.pagination,
hidden: resourceOverride.hidden,
filterOptions: {
mode: fo.mode || "server",
fields: fo.fields,
},
});
// console.log('Loaded resource:', name, 'endpoint:', listPath, 'fields count:', Object.keys(fields).length);
}
// Collect standalone enum schemas (e.g. FetchRequestStatus, AccountType, etc.) // Collect standalone enum schemas (e.g. FetchRequestStatus, AccountType, etc.)
const enums: Record<string, string[]> = {}; const enums: Record<string, string[]> = {};
const apiDoc = api as any; const apiDoc = api as any;

View File

@@ -8,6 +8,7 @@ export function resolveTemplate(template: string, item: any): string {
} }
export function getFieldOptions(field: ResourceField, relationData?: any[]): SelectOption[] { export function getFieldOptions(field: ResourceField, relationData?: any[]): SelectOption[] {
// console.log('getFieldOptions called for field', field.label, 'type', field.type, 'enumOption', field.enumOption);
if (field.type === 'enum') { if (field.type === 'enum') {
return (field.options ?? []).map(opt => ({ return (field.options ?? []).map(opt => ({
key: opt, key: opt,
@@ -16,7 +17,11 @@ export function getFieldOptions(field: ResourceField, relationData?: any[]): Sel
} }
if (field.relation) { if (field.relation) {
const data = relationData ?? []; const data = Array.isArray(relationData) ? relationData : [];
// console.log('Getting options for relation', field.relation, 'data count:', data.length);
if (data.length === 0) {
throw new Error(`Relation data for "${field.relation}" is missing or empty cannot build options for field "${field.label}"`);
}
const enumOption = field.enumOption; const enumOption = field.enumOption;
if (!enumOption) { if (!enumOption) {
throw new Error( throw new Error(
@@ -24,11 +29,12 @@ export function getFieldOptions(field: ResourceField, relationData?: any[]): Sel
`Define referenceOptions.enumOption in the configuration for resource "${field.relation}".` `Define referenceOptions.enumOption in the configuration for resource "${field.relation}".`
); );
} }
const result = data.map(item => ({
return data.map(item => ({ key: String(item[enumOption.key] ?? item.id ?? item._id),
key: String(item[enumOption.key]),
value: resolveTemplate(enumOption.value, item), value: resolveTemplate(enumOption.value, item),
})); }));
// console.log('Option map for', field.relation, 'first entry:', data[0], 'result key:', result[0]?.key);
return result;
} }
return []; return [];

View File

@@ -15,6 +15,7 @@ import {
DialogContentText, DialogContentText,
DialogActions, DialogActions,
Chip, Chip,
TextField,
} from "@mui/material"; } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import AddCircleIcon from "@mui/icons-material/AddCircle"; import AddCircleIcon from "@mui/icons-material/AddCircle";
@@ -131,28 +132,30 @@ export default function ReportSnapshots() {
/> />
)} )}
<Box sx={{ display: "flex", gap: 2 }}> <Box sx={{ display: "flex", gap: 2 }}>
{startDateField && components?.datetime && (
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<components.datetime <TextField
name="start_date" label="Start Date"
field={startDateField} type="date"
value={startDate} value={startDate}
onChange={(val: string) => setStartDate(val)} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setStartDate(e.target.value)}
size="small"
InputLabelProps={{ shrink: true }}
inputProps={{ max: new Date().toISOString().split("T")[0] }}
/> />
</Box> </Box>
)}
{endDateField && components?.datetime && (
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<components.datetime <TextField
name="end_date" label="End Date"
field={endDateField} type="date"
value={endDate} value={endDate}
onChange={(val: string) => setEndDate(val)} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEndDate(e.target.value)}
size="small"
InputLabelProps={{ shrink: true }}
inputProps={{ max: new Date().toISOString().split("T")[0] }}
/> />
</Box> </Box>
)} </Box>
</Box>
<Box sx={{ display: "flex", gap: 2 }}> <Box sx={{ display: "flex", gap: 2 }}>
{minAmountField && components?.FormField && ( {minAmountField && components?.FormField && (

View File

@@ -57,6 +57,14 @@ export const configuration: Record<string, ResourceOverride> = {
format: { format: {
path: 'source.format', path: 'source.format',
}, },
start_date: {
type: 'date',
label: 'Start Date',
},
end_date: {
type: 'date',
label: 'End Date',
},
// account: { // account: {
// refers: 'accounts', // refers: 'accounts',
// }, // },
@@ -68,7 +76,7 @@ export const configuration: Record<string, ResourceOverride> = {
accounts: { accounts: {
referenceOptions: { referenceOptions: {
enumOption: { enumOption: {
key: 'id', key: '_id',
value: '{name} - XX{number}', value: '{name} - XX{number}',
}, },
autoComplete: true, autoComplete: true,
@@ -78,7 +86,7 @@ export const configuration: Record<string, ResourceOverride> = {
tags: { tags: {
referenceOptions: { referenceOptions: {
enumOption: { enumOption: {
key: 'id', key: '_id',
value: '{icon} {name}', value: '{icon} {name}',
}, },
autoComplete: true, autoComplete: true,