Align frontend with updated OpenAPI schema and PATCH operation (#13)

## Summary

Align frontend with updated OpenAPI schema (rename `format` → `bank`, add `pipeline`) and introduce a dedicated `PATCH` operation for partial updates in react-openapi.

## Changes

### react-openapi: new `patch` operation support
- Add `patch` to `ResourceConfig.operations` type
- Separate PUT/PATCH detection in resource-config transformer
- `update()` now always calls PUT; new `patch()` method returned conditionally when spec defines PATCH
- Admin form still uses PUT for full replacements

### Fetch Request custom pages: schema alignment
- Rename `format` → `bank` in create form fields and TypeScript models (`FileSource`, `EmailSource`)
- Add `pipeline` field to create form and `FetchRequestCreate` model
- Export `PipelineType` from barrel index

### Fetch Request retry: use PATCH
- Retry handler now calls `patch(id, { status: "pending" })` instead of the generic `update()`
- Removes `useMutation` wrapper; uses simple local `retrying` state for loading

### Fix Account display in detail page
- Resolve FK displayFormat target in detail field rendering (was always using fetch-request format)
- Fix page title to use `applyDisplayFormat` with resolved FK objects instead of non-existent `account_name`
- Move `useMemo` before early returns to fix hooks ordering violation crash

## Testing
- Custom fetch request create form includes `bank` (select) and `pipeline` (select) fields
- Retry sends `PATCH` instead of `PUT` — no more 500 errors
- Detail page shows proper account name instead of `—`

Reviewed-on: #13
Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
This commit is contained in:
2026-07-30 18:49:56 +00:00
committed by aetos
parent 8795894c2c
commit 885ccdcfa7
10 changed files with 94 additions and 1221 deletions

View File

@@ -56,6 +56,7 @@ interface UseResourceReturn {
get: (id: string | number, params?: Record<string, any>) => Promise<any>;
create: (data: any) => Promise<any>;
update: (id: string | number, data: any) => Promise<any>;
patch?: (id: string | number, data: any) => Promise<any>;
remove: (id: string | number) => Promise<void>;
stream?: (handlers: StreamHandlers, pathParams?: Record<string, string | number>) => StreamSubscription;
loading: boolean;
@@ -596,7 +597,7 @@ export function useResource(resourceName: string): UseResourceReturn {
const rPath = resource?.path;
const rPagination = resource?.pagination;
const rUpdateMethod = resource?.updateMethod;
const rPatch = resource ? (resource.operations.patch || undefined) : false;
const rStreaming = resource?.streaming;
const rFields = resource?.fields;
@@ -706,8 +707,7 @@ export function useResource(resourceName: string): UseResourceReturn {
const sanitized = rFields && schemas
? await sanitizePayload(data, rFields, schemas, resolveFk)
: data;
const method = rUpdateMethod ?? "put";
const res = await (method === "patch" ? api.patch : api.put)(`${rPath}/${id}`, sanitized);
const res = await api.put(`${rPath}/${id}`, sanitized);
return res.data;
} catch (e: any) {
setError(parseError(e));
@@ -716,7 +716,29 @@ export function useResource(resourceName: string): UseResourceReturn {
setLoading(false);
}
},
[rPath, rFields, schemas, resolveFk, rUpdateMethod, setLoading, setError]
[rPath, rFields, schemas, resolveFk, setLoading, setError]
);
const _patch = useCallback(
async (id: string | number, data: any): Promise<any> => {
if (!rPath) throw new Error(`Resource "${resourceName}" not found yet`);
setLoading(true);
setError(null);
try {
const api = getApi();
const sanitized = rFields && schemas
? await sanitizePayload(data, rFields, schemas, resolveFk)
: data;
const res = await api.patch(`${rPath}/${id}`, sanitized);
return res.data;
} catch (e: any) {
setError(parseError(e));
throw e;
} finally {
setLoading(false);
}
},
[rPath, rFields, schemas, resolveFk, setLoading, setError]
);
const remove = useCallback(
@@ -791,6 +813,7 @@ export function useResource(resourceName: string): UseResourceReturn {
get,
create,
update,
patch: undefined,
remove,
stream: undefined,
loading: false,
@@ -798,5 +821,5 @@ export function useResource(resourceName: string): UseResourceReturn {
};
}
return { resource, components, list, get, create, update, remove, stream: rStreaming ? stream : undefined, loading: state.loading, error: state.error };
return { resource, components, list, get, create, update, patch: rPatch ? _patch : undefined, remove, stream: rStreaming ? stream : undefined, loading: state.loading, error: state.error };
}

View File

@@ -98,9 +98,9 @@ export function buildResourceConfigs(spec: OpenApiSpec): ResourceConfig[] {
const parent = nameMap.get(parentName);
if (!parent) continue;
if (hasOperation(pathObj, "get")) parent.operations.get = true;
if (hasOperation(pathObj, "put") || hasOperation(pathObj, "patch")) parent.operations.update = true;
if (hasOperation(pathObj, "put")) parent.operations.update = true;
if (hasOperation(pathObj, "patch")) parent.operations.patch = true;
if (hasOperation(pathObj, "delete")) parent.operations.delete = true;
if (hasOperation(pathObj, "patch") && !hasOperation(pathObj, "put")) parent.updateMethod = "patch";
continue;
}
@@ -127,8 +127,8 @@ export function buildResourceConfigs(spec: OpenApiSpec): ResourceConfig[] {
fields: hasSSE ? [SSE_RECEIVED_FIELD, ...fields.map((f) => ({ ...f, readOnly: true }))] : fields,
orderedFields: [],
operations: hasSSE
? { list: true, get: false, create: false, update: false, delete: false }
: { list: hasOperation(pathObj, "get"), get: false, create: false, update: false, delete: false },
? { list: true, get: false, create: false, update: false, patch: false, delete: false }
: { list: hasOperation(pathObj, "get"), get: false, create: false, update: false, patch: false, delete: false },
updateMethod: "put",
pagination: hasSSE ? null : detectPagination(pathObj),
relationships: [],
@@ -172,9 +172,9 @@ export function buildResourceConfigs(spec: OpenApiSpec): ResourceConfig[] {
listColumns: schema?.["x-list-columns"] ?? [],
fields: hasSSE ? [SSE_RECEIVED_FIELD, ...fields.map((f) => ({ ...f, readOnly: true }))] : fields,
orderedFields: [],
operations: hasSSE
? { list: true, get: false, create: false, update: false, delete: false }
: { list: hasOperation(pathObj, "get"), get: false, create: hasOperation(pathObj, "post"), update: false, delete: false },
operations: hasSSE
? { list: true, get: false, create: false, update: false, patch: false, delete: false }
: { list: hasOperation(pathObj, "get"), get: false, create: hasOperation(pathObj, "post"), update: false, patch: false, delete: false },
updateMethod: "put",
pagination: hasSSE ? null : detectPagination(pathObj),
relationships,

View File

@@ -65,6 +65,7 @@ export interface ResourceConfig {
get: boolean;
create: boolean;
update: boolean;
patch: boolean;
delete: boolean;
};
updateMethod: "put" | "patch";