From 12e5f113b8cba3ca8beb5b12fa1ad55b7e3eb704 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Thu, 18 Jun 2026 23:20:57 +0530 Subject: [PATCH] khata compliant with new react-openapi --- react-openapi/src/components/fields/utils.ts | 9 +- react-openapi/src/context/useResource.tsx | 23 +++- react-openapi/src/spec-validator.ts | 4 +- .../src/transformers/resource-config.ts | 4 +- react-openapi/src/types.ts | 1 + src/FetchRequestDetail.tsx | 5 +- src/FetchRequests.tsx | 20 ++-- src/ReportSnapshots.tsx | 103 ++++++++---------- .../fetch-requests/useFetchRequests.ts | 22 +--- .../report-snapshots/useReportSnapshots.ts | 14 +-- src/features/report/useReport.ts | 6 +- src/main.jsx | 5 + 12 files changed, 103 insertions(+), 113 deletions(-) diff --git a/react-openapi/src/components/fields/utils.ts b/react-openapi/src/components/fields/utils.ts index c524fab..52dd2b4 100644 --- a/react-openapi/src/components/fields/utils.ts +++ b/react-openapi/src/components/fields/utils.ts @@ -1,4 +1,11 @@ +function getNested(obj: any, path: string): any { + return path.split(".").reduce((o, k) => o?.[k], obj); +} + export function applyDisplayFormat(item: any, format: string): string { if (!item || typeof item !== "object") return String(item ?? ""); - return format.replace(/\{(\w+)\}/g, (_, key) => String(item[key] ?? "")); + return format.replace(/\{([\w.]+)\}/g, (_, key) => { + const val = getNested(item, key); + return val != null ? String(val) : ""; + }); } diff --git a/react-openapi/src/context/useResource.tsx b/react-openapi/src/context/useResource.tsx index 0502cd3..d689b86 100644 --- a/react-openapi/src/context/useResource.tsx +++ b/react-openapi/src/context/useResource.tsx @@ -350,12 +350,24 @@ function buildFilterComponent(field: FieldConfig, resourceName: string): React.F export function useResource(resourceName: string): UseResourceReturn { const { resources } = useAppContext(); const resource = resources.find((r) => r.name === resourceName); - if (!resource) { - throw new Error(`Resource "${resourceName}" not found`); - } const [state, setState] = useState({ loading: false, error: null }); + if (!resource) { + const noop = async () => { throw new Error(`Resource "${resourceName}" not found yet`); }; + return { + resource: null as unknown as ResourceConfig, + components: {}, + list: noop, + get: noop, + create: noop, + update: noop, + remove: noop, + loading: false, + error: null, + }; + } + const setLoading = useCallback((loading: boolean) => { setState((s) => ({ ...s, loading })); }, []); @@ -437,7 +449,8 @@ export function useResource(resourceName: string): UseResourceReturn { setError(null); try { const api = getApi(); - const res = await api.put(`${resource.path}/${id}`, data); + const method = resource.updateMethod ?? "put"; + const res = await (method === "patch" ? api.patch : api.put)(`${resource.path}/${id}`, data); return res.data; } catch (e: any) { setError(parseError(e)); @@ -446,7 +459,7 @@ export function useResource(resourceName: string): UseResourceReturn { setLoading(false); } }, - [resource.path, setLoading, setError] + [resource.path, resource.updateMethod, setLoading, setError] ); const remove = useCallback( diff --git a/react-openapi/src/spec-validator.ts b/react-openapi/src/spec-validator.ts index be643b6..510c92f 100644 --- a/react-openapi/src/spec-validator.ts +++ b/react-openapi/src/spec-validator.ts @@ -120,10 +120,10 @@ export function validateSpec(spec: OpenApiSpec): ValidationMessage[] { messages.push({ type: "error", message: `"${resourcePath}/{id}" has no GET endpoint — detail view not possible` }); } if (!itemPath?.put) { - messages.push({ type: "error", message: `"${resourcePath}/{id}" has no PUT endpoint — update not possible` }); + messages.push({ type: "info", message: `"${resourcePath}/{id}" has no PUT endpoint — update not available` }); } if (!itemPath?.delete) { - messages.push({ type: "error", message: `"${resourcePath}/{id}" has no DELETE endpoint — deletion not possible` }); + messages.push({ type: "info", message: `"${resourcePath}/{id}" has no DELETE endpoint — deletion not available` }); } } } diff --git a/react-openapi/src/transformers/resource-config.ts b/react-openapi/src/transformers/resource-config.ts index 9c23b5f..b17f98c 100644 --- a/react-openapi/src/transformers/resource-config.ts +++ b/react-openapi/src/transformers/resource-config.ts @@ -81,9 +81,10 @@ export function buildResourceConfigs(spec: OpenApiSpec): ResourceConfig[] { list: hasOperation(collectionPathObj, "get"), get: hasOperation(itemPathObj, "get"), create: hasOperation(collectionPathObj, "post"), - update: hasOperation(itemPathObj, "put"), + update: hasOperation(itemPathObj, "put") || hasOperation(itemPathObj, "patch"), delete: hasOperation(itemPathObj, "delete"), }, + updateMethod: hasOperation(itemPathObj, "patch") && !hasOperation(itemPathObj, "put") ? "patch" : "put", pagination: detectPagination(collectionPathObj), relationships, streaming: hasSSE || undefined, @@ -91,6 +92,7 @@ export function buildResourceConfigs(spec: OpenApiSpec): ResourceConfig[] { if (hasSSE) { resource.operations = { list: true, get: false, create: false, update: false, delete: false }; + resource.updateMethod = "put"; resource.pagination = null; resource.relationships = []; resource.fields = [SSE_RECEIVED_FIELD, ...fields.map((f) => ({ ...f, readOnly: true }))]; diff --git a/react-openapi/src/types.ts b/react-openapi/src/types.ts index 161f074..a5a90f0 100644 --- a/react-openapi/src/types.ts +++ b/react-openapi/src/types.ts @@ -42,6 +42,7 @@ export interface ResourceConfig { update: boolean; delete: boolean; }; + updateMethod: "put" | "patch"; pagination: { limitParam: string; offsetParam: string; diff --git a/src/FetchRequestDetail.tsx b/src/FetchRequestDetail.tsx index 9137026..26d7373 100644 --- a/src/FetchRequestDetail.tsx +++ b/src/FetchRequestDetail.tsx @@ -145,9 +145,8 @@ function isMathValid(candidate: { amount: number; balance: number }, prevBalance export default function FetchRequestDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { resources, config } = useAppContext(); - const fetchRequestResource = resources.find(r => r.name === "fetch-requests")!; - const { get, update } = useResource(fetchRequestResource); + const { config } = useAppContext(); + const { get, update } = useResource("fetch-requests"); const { data: fetchRequest, isLoading, error: fetchError, refetch: refetchRequest } = useQuery({ queryKey: ["fetch-requests", "detail", id], diff --git a/src/FetchRequests.tsx b/src/FetchRequests.tsx index 0c08ed2..cffa1b0 100644 --- a/src/FetchRequests.tsx +++ b/src/FetchRequests.tsx @@ -47,7 +47,7 @@ import type { } from "./features/fetch-requests"; import { RETRY_MAX, formatApiError } from "./features/fetch-requests"; import { useNavigate } from "react-router-dom"; -import { useAppContext, useResource, FormFieldRenderer } from "../react-openapi"; +import { useResource, FormFieldRenderer } from "../react-openapi"; import type { FieldConfig } from "../react-openapi"; import { useMutation, useQuery } from "@tanstack/react-query"; @@ -118,9 +118,8 @@ export default function FetchRequests() { const [accountFilter, setAccountFilter] = React.useState(""); const [sourceFilter, setSourceFilter] = React.useState<"all" | "file" | "email">("all"); - const { resources } = useAppContext(); - const fetchRequestsRes = resources.find(r => r.name === "fetch-requests")!; - const { list, create, update, remove } = useResource(fetchRequestsRes); + const fr = useResource("fetch-requests"); + const { list, create, update, remove, resource: fetchRes } = fr; const { data: listData, isLoading, isFetching, refetch } = useQuery({ queryKey: ["fetch-requests", "list", { statusFilter, accountFilter, sourceFilter }], @@ -131,22 +130,21 @@ export default function FetchRequests() { }), }); - const accountsResource = resources.find(r => r.name === "accounts"); - const { list: listAccounts } = accountsResource ? useResource(accountsResource) : { list: async () => ({ items: [] }) }; + const { list: listAccounts } = useResource("accounts"); const { data: accountsData } = useQuery({ queryKey: ["accounts", "list"], queryFn: () => listAccounts(), - enabled: !!accountsResource, }); const accountOptions: string[] = React.useMemo(() => { return (accountsData?.items ?? []).map((a: any) => a.name).filter(Boolean); }, [accountsData]); - const formatField: FieldConfig | undefined = fetchRequestsRes?.orderedFields.find(f => f.name === "format"); + const fields = fetchRes?.orderedFields ?? []; + const formatField: FieldConfig | undefined = fields.find(f => f.name === "format"); const formatOptions: string[] = formatField?.enumValues ?? []; - const startDateField: FieldConfig | undefined = fetchRequestsRes?.orderedFields.find(f => f.name === "start_date"); - const endDateField: FieldConfig | undefined = fetchRequestsRes?.orderedFields.find(f => f.name === "end_date"); - const payorUsernameField: FieldConfig | undefined = fetchRequestsRes?.orderedFields.find(f => f.name === "payor_username"); + const startDateField: FieldConfig | undefined = fields.find(f => f.name === "start_date"); + const endDateField: FieldConfig | undefined = fields.find(f => f.name === "end_date"); + const payorUsernameField: FieldConfig | undefined = fields.find(f => f.name === "payor_username"); const createMutation = useMutation({ mutationFn: (data: any) => create(data), diff --git a/src/ReportSnapshots.tsx b/src/ReportSnapshots.tsx index 077dfde..3bfac2d 100644 --- a/src/ReportSnapshots.tsx +++ b/src/ReportSnapshots.tsx @@ -21,8 +21,7 @@ import DeleteIcon from "@mui/icons-material/Delete"; import AddCircleIcon from "@mui/icons-material/AddCircle"; import RefreshIcon from "@mui/icons-material/Refresh"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import { useAppContext, useResource, FormFieldRenderer } from "../react-openapi"; -import type { FieldConfig } from "../react-openapi"; +import { useResource } from "../react-openapi"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; interface ReportSnapshotQuery { @@ -54,9 +53,7 @@ export default function ReportSnapshots() { const [deleteTarget, setDeleteTarget] = React.useState(null); const [createdSnapshotId, setCreatedSnapshotId] = React.useState(null); - const { resources } = useAppContext(); - const reportsResource = resources.find(r => r.name === "reports")!; - const { list, create, remove } = useResource(reportsResource); + const { list, create, remove } = useResource("reports"); const { data: listData, isLoading, isFetching, refetch } = useQuery({ queryKey: ["reports", "list"], @@ -73,12 +70,6 @@ export default function ReportSnapshots() { }, }); - const ignoreSelfField: FieldConfig | undefined = reportsResource?.orderedFields.find(f => f.name === "ignore_self"); - const startDateField: FieldConfig | undefined = reportsResource?.orderedFields.find(f => f.name === "start_date"); - const endDateField: FieldConfig | undefined = reportsResource?.orderedFields.find(f => f.name === "end_date"); - const minAmountField: FieldConfig | undefined = reportsResource?.orderedFields.find(f => f.name === "min_amount"); - const maxAmountField: FieldConfig | undefined = reportsResource?.orderedFields.find(f => f.name === "max_amount"); - const snapshots: ReportSnapshot[] = listData?.items ?? []; const handleCreate = async () => { @@ -135,58 +126,50 @@ export default function ReportSnapshots() { - {ignoreSelfField ? ( - setIgnoreSelf(val)} - /> - ) : null} - - - - ) => setStartDate(e.target.value)} - size="small" - InputLabelProps={{ shrink: true }} - inputProps={{ max: new Date().toISOString().split("T")[0] }} - /> - - - ) => setEndDate(e.target.value)} - size="small" - InputLabelProps={{ shrink: true }} - inputProps={{ max: new Date().toISOString().split("T")[0] }} - /> - + + + ) => setStartDate(e.target.value)} + size="small" + InputLabelProps={{ shrink: true }} + inputProps={{ max: new Date().toISOString().split("T")[0] }} + /> + + ) => setEndDate(e.target.value)} + size="small" + InputLabelProps={{ shrink: true }} + inputProps={{ max: new Date().toISOString().split("T")[0] }} + /> + + - {minAmountField ? ( - - setMinAmount(val)} - /> - - ) : null} - {maxAmountField ? ( - - setMaxAmount(val)} - /> - - ) : null} + + ) => setMinAmount(e.target.value)} + size="small" + /> + + + ) => setMaxAmount(e.target.value)} + size="small" + /> +