diff --git a/src/app/explorer/info.tsx b/src/app/explorer/info.tsx index 67b6409..5acd300 100644 --- a/src/app/explorer/info.tsx +++ b/src/app/explorer/info.tsx @@ -5,7 +5,7 @@ import { Spinner } from "@/components/ui/spinner"; import { ResourceInstance } from "@/state/fetch"; import { ResourceSchema } from "@/state/openapi"; import { useAppSelector } from "@/hooks/store"; -import { selectChildResources } from "@/state/store"; +import { selectChildResources, selectHeaders } from "@/state/store"; import ResourceListPage from "./resource_list"; import { CustomMethodComponent } from "@/components/custom_method"; @@ -33,21 +33,27 @@ export default function InfoPage(props: InfoPageProps) { : [], ); - useEffect(() => { - // Set parent parameters from URL params, excluding resourceId - const parentParams = new Map(); + const headers = useAppSelector(selectHeaders); + + const parentParams = useMemo(() => { + const parentMap = new Map(); for (const [key, value] of Object.entries(params)) { if (key !== "resourceId" && value) { - parentParams.set(key, value); + parentMap.set(key, value); } } + return parentMap; + }, [params]); + + useEffect(() => { + // Set parent parameters on the resource for other uses props.resource.parents = parentParams; // Fetch the resource instance props.resource - .get(params["resourceId"]!) + .get(params["resourceId"] as string, parentParams, headers) .then((instance) => setState(instance)); - }, [params, props.resource]); + }, [params, props.resource, parentParams, headers]); const properties = useMemo(() => { if (state?.properties) { diff --git a/src/app/explorer/resource_list.tsx b/src/app/explorer/resource_list.tsx index 6b3e0fc..6e71750 100644 --- a/src/app/explorer/resource_list.tsx +++ b/src/app/explorer/resource_list.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Plus, RefreshCw } from "lucide-react"; import { ResourceSchema } from "@/state/openapi"; @@ -18,21 +18,32 @@ type ResourceListProps = { export default function ResourceListPage(props: ResourceListProps) { const navigate = useNavigate(); + const params = useParams(); const [state, setState] = useState({ resources: [], }); const headers = useAppSelector(selectHeaders); + const parentParams = useMemo(() => { + const parentMap = new Map(); + for (const [key, value] of Object.entries(params)) { + if (key !== "resourceId" && value) { + parentMap.set(key, value); + } + } + return parentMap; + }, [params]); + const refreshList = useCallback(() => { - props.resource.list(headers).then((resources) => { + props.resource.list(parentParams, headers).then((resources) => { if (resources) { setState({ resources: resources, }); } }); - }, [props, headers]); + }, [props, headers, parentParams]); useEffect(() => { refreshList(); @@ -55,6 +66,7 @@ export default function ResourceListPage(props: ResourceListProps) { navigate( props.resource.substituteUrlParameters( props.resource.base_url(), + parentParams, ) + "/_create", ) } @@ -69,6 +81,7 @@ export default function ResourceListPage(props: ResourceListProps) { diff --git a/src/app/explorer/update_form.tsx b/src/app/explorer/update_form.tsx index dbeebd3..ac7cf2e 100644 --- a/src/app/explorer/update_form.tsx +++ b/src/app/explorer/update_form.tsx @@ -32,9 +32,9 @@ export default function UpdatePage(props: UpdatePageProps) { useEffect(() => { props.schema - .get(resourceId!) + .get(resourceId as string, parentParams, headers) .then((instance) => setResourceInstance(instance)); - }, [resourceId, props.schema]); + }, [resourceId, props.schema, parentParams, headers]); const handleSuccess = () => { toast({ description: `Updated ${resourceInstance?.properties["path"]}` }); diff --git a/src/components/error_boundary.tsx b/src/components/error_boundary.tsx index cea7b5f..30be065 100644 --- a/src/components/error_boundary.tsx +++ b/src/components/error_boundary.tsx @@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button"; interface ErrorDisplayProps { error: Error | unknown; - reset: () => void; + reset: (path?: string) => void; } export function ErrorDisplay({ error, reset }: ErrorDisplayProps) { @@ -23,7 +23,7 @@ export function ErrorDisplay({ error, reset }: ErrorDisplayProps) { (typeof error === "string" ? error : "Something went wrong."); let title = "An error occurred"; let description = errorMessage; - let action = ; + let action = ; const handler = findErrorHandler(error); if (handler) { @@ -71,9 +71,9 @@ export class ErrorBoundary extends Component { console.error("Uncaught error:", error, errorInfo); } - private handleClose = () => { + private handleClose = (path?: string) => { this.setState({ hasError: false, error: null }); - window.location.href = "/"; + window.location.href = path || "/"; }; public render() { @@ -89,8 +89,8 @@ export function RouteErrorBoundary() { const error = useRouteError(); const navigate = useNavigate(); - const handleClose = () => { - navigate("/"); + const handleClose = (path?: string) => { + navigate(path || "/"); }; return ; diff --git a/src/components/form/form.test.tsx b/src/components/form/form.test.tsx index 694624a..ccbf585 100644 --- a/src/components/form/form.test.tsx +++ b/src/components/form/form.test.tsx @@ -56,7 +56,7 @@ describe("Form", () => { if (additionalFieldsValues.id) { id = additionalFieldsValues.id as string; } - return resource.create(value, id, headers); + return resource.create(value, id, parentParams, headers); }); return render( @@ -194,6 +194,7 @@ describe("Form", () => { active: true, }, undefined, + new Map(), "", ); }); @@ -322,6 +323,7 @@ describe("Form", () => { requiredField: "Required Value", }, undefined, + new Map(), "", ); }); @@ -376,6 +378,7 @@ describe("Form", () => { requiredName: "Test Name", }, undefined, + new Map(), "", ); }); @@ -403,6 +406,7 @@ describe("Form", () => { requiredName: "Test Name", }, undefined, + new Map(), "", ); }); @@ -612,6 +616,7 @@ describe("Form", () => { }, }, undefined, + new Map(), "", ); }); @@ -1078,6 +1083,7 @@ describe("Form", () => { age: 30, }), undefined, + new Map(), "", ); }); @@ -1274,6 +1280,7 @@ describe("Form", () => { name: "Test Name", }, undefined, + new Map(), "", ); }); @@ -1342,6 +1349,7 @@ describe("Form", () => { name: "Test Name", }, "my-resource-id", + new Map(), "", ); }); @@ -1373,6 +1381,7 @@ describe("Form", () => { name: "Test Name", }, undefined, + new Map(), "", ); }); diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index e05812c..4fd1f50 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -347,7 +347,7 @@ export default function CreateForm(props: { resource: ResourceSchema }) { if (additionalFieldsValues.id) { id = additionalFieldsValues.id as string; } - return props.resource.create(value, id, headers); + return props.resource.create(value, id, parentParams, headers); }} additionalFields={ props.resource.supportsUserSettableCreate diff --git a/src/components/resource_list.tsx b/src/components/resource_list.tsx index 27117cb..95e6191 100644 --- a/src/components/resource_list.tsx +++ b/src/components/resource_list.tsx @@ -24,6 +24,7 @@ import { ErrorBoundary } from "@/components/error_boundary"; type ResourceListTableProps = { resource: ResourceSchema; resources: ResourceInstance[]; + parentParams: Map; onRefresh: () => void; }; @@ -39,6 +40,7 @@ type ColumnDef = { export function ResourceListTable({ resource, resources, + parentParams, onRefresh, }: ResourceListTableProps) { const navigate = useNavigate(); @@ -151,8 +153,10 @@ export function ResourceListTable({ + )} + + + + + ); +} + export const MissingParentErrorHandler: SemanticErrorHandler = { match: (error) => error instanceof MissingParentError, @@ -49,10 +125,10 @@ export const MissingParentErrorHandler: SemanticErrorHandler = { description: (error) => { const e = error as MissingParentError; - return `No parents found for ${e.resourceName}. You need to create a ${e.resourceName} first.`; + return `Please provide the missing ${e.resourceName} ID to continue.`; }, - action: (error, { reset, navigate }) => { + action: (error, { reset }) => { const e = error as MissingParentError; // Try to find the parent resource to give a direct link @@ -64,28 +140,29 @@ export const MissingParentErrorHandler: SemanticErrorHandler = { (r: ResourceSchema) => r.singular_name === e.resourceName, ); + let parentUrl; + let parentName; if (parentResource) { - const createUrl = parentResource.substituteUrlParameters( - parentResource.base_url(), - ); - - return ( -
- - -
- ); + try { + const parentsMap = new Map(Object.entries(e.availableParents || {})); + parentUrl = parentResource.substituteUrlParameters( + parentResource.base_url(), + parentsMap, + ); + parentName = parentResource.plural_name; + } catch { + // Unlikely to happen unless the parent itself is missing its own parent + } } - return ; + return ( + + ); }, }; diff --git a/src/state/openapi.ts b/src/state/openapi.ts index 72a9c5d..4f499c6 100644 --- a/src/state/openapi.ts +++ b/src/state/openapi.ts @@ -30,24 +30,30 @@ class ResourceSchema { return this.resource.createMethod?.supportsUserSettableCreate ?? false; } - public substituteUrlParameters(url: string): string { + public substituteUrlParameters( + url: string, + parentIds: Map, + ): string { const paramRegex = /\{([^}]+)\}/g; let match; let resultUrl = url; + const parentsMap = Object.fromEntries(this.parents); while ((match = paramRegex.exec(url)) !== null) { const paramName = match[1]; - let parentId = this.parents.get(paramName); + let parentId = parentIds.get(paramName); + if (parentId === "{" + paramName + "}") { + throw new MissingParentError(paramName, parentsMap); + } if (!parentId) { // Try removing _id suffix if present if (paramName.endsWith("_id")) { const nameWithoutSuffix = paramName.slice(0, -3); - parentId = this.parents.get(nameWithoutSuffix); + parentId = parentIds.get(nameWithoutSuffix); } if (!parentId) { - const parentsMap = Object.fromEntries(this.parents); throw new MissingParentError(paramName, parentsMap); } } @@ -58,16 +64,27 @@ class ResourceSchema { return resultUrl; } - list(headers: string = ""): Promise { + list( + parentIds: Map, + headers: string = "", + ): Promise { const baseUrl = this.base_url(); - const url = this.substituteUrlParameters(`${this.server_url}${baseUrl}`); + const url = this.substituteUrlParameters( + `${this.server_url}${baseUrl}`, + parentIds, + ); return List(url, this, headers); } - get(resourceId: string, headers: string = ""): Promise { + get( + resourceId: string, + parentIds: Map, + headers: string = "", + ): Promise { const baseUrl = this.base_url(); const url = this.substituteUrlParameters( `${this.server_url}${baseUrl}/${resourceId}`, + parentIds, ); return Get(url, this, headers); } @@ -75,6 +92,7 @@ class ResourceSchema { create( body: Record, id: string = "", + parentIds: Map, headers: string = "", ): Promise { const baseUrl = this.base_url(); @@ -82,7 +100,7 @@ class ResourceSchema { if (id) { url += `?id=${id}`; } - url = this.substituteUrlParameters(url); + url = this.substituteUrlParameters(url, parentIds); return Create(url, body, headers); }