import React, { useEffect, useState } from "react"; import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom"; import { Box, CircularProgress, Alert } from "@mui/material"; import { useAppContext } from "../context/AppContext"; import { Layout } from "./Layout"; import { ResourceList } from "./ResourceList"; import { ResourceForm } from "./ResourceForm"; import { ResourceDetail } from "./ResourceDetail"; import { ValidationAlert } from "./ValidationAlert"; import { getApi } from "../hooks/useApi"; export interface AdminProps { basePath: string; } export function Admin({ basePath }: AdminProps) { const { resources, loading, errors, warnings } = useAppContext(); if (loading) { return ( ); } if (errors.length > 0) { return ; } if (resources.length === 0) { return ( No resources found in the OpenAPI spec. ); } const topLevel = resources.filter((r) => !r.parent); return ( <> {warnings.length > 0 && } } /> {topLevel.map((r) => ( } /> {!r.streaming && ( <> } /> } /> } /> )} ))} ); } export interface ProfileRoute { path: string; method: string; operationId: string; summary?: string; } /** Derive a friendly profile route path from the operation's spec path. * e.g. "/users/me" → "me", "/users/me/" → "me" */ function friendlyProfilePath(specPath: string): string { return specPath.replace(/^\/[^/]+\/?/, "").replace(/\/+$/, ""); } export function ProfileRoutes() { const { profileOperations, profileComponents } = useAppContext(); const ops = profileOperations; const getOp = ops.find((o) => o.method === "GET"); const location = useLocation(); const navigate = useNavigate(); console.log("[ProfileRoutes] render pathname=%s ops=%o", location.pathname, ops.map(o => o.path)); // Redirect /:username to /me (only "me" and "me/edit" are valid) const match = location.pathname.match(/^\/profile\/(.+)$/); const username = match ? match[1] : ""; console.log("[ProfileRoutes] match=%o username=%s", match && match[1], username); if (username !== "me" && username !== "me/edit") { console.log("[ProfileRoutes] redirecting to /profile/me"); navigate("/profile/me", { replace: true }); return null; } const isEdit = username === "me/edit"; const mode = isEdit ? "edit" as const : "view" as const; console.log("[ProfileRoutes] mode=%s", mode); const op = ops.find((o) => { if (isEdit && (o.method === "PUT" || o.method === "PATCH")) return true; if (!isEdit && o.method === "GET") return true; return false; }); if (!op) return null; let Component: React.ComponentType | undefined; if (mode === "view" && profileComponents.view) { Component = profileComponents.view; } else if (mode === "edit" && profileComponents.edit) { Component = profileComponents.edit; } if (!Component) return null; return ( ); } function ProfileComponentWrapper({ Component, mode, operation, getOperation, }: { Component: React.ComponentType; mode: "view" | "create" | "edit"; operation: { path: string; method: string }; getOperation?: { path: string }; }) { const navigate = useNavigate(); const [data, setData] = useState(null); const [loading, setLoading] = useState(mode !== "create"); const [error, setError] = useState(null); console.log("[ProfileComponentWrapper] render mode=%s fetchPath=%s", mode, getOperation?.path ?? operation.path); const fetchPath = getOperation?.path ?? operation.path; const fetchData = async () => { console.log("[ProfileComponentWrapper] fetchData START path=%s", fetchPath); setLoading(true); setError(null); try { const res = await getApi().get(fetchPath); console.log("[ProfileComponentWrapper] fetchData SUCCESS", res.data); setData(res.data); } catch (e: any) { console.log("[ProfileComponentWrapper] fetchData ERROR", e.message, e.response?.status); setError(e.response?.data?.detail ?? e.message ?? "Failed to load profile"); } finally { setLoading(false); } }; useEffect(() => { if (mode !== "create") { console.log("[ProfileComponentWrapper] useEffect calling fetchData"); fetchData(); } }, [fetchPath, mode]); const handleSubmit = async (formData: Record) => { console.log("[ProfileComponentWrapper] handleSubmit START"); setLoading(true); setError(null); try { if (mode === "create") { await getApi().post(operation.path, formData); } else { await getApi()({ method: operation.method.toLowerCase(), url: operation.path, data: formData }); } console.log("[ProfileComponentWrapper] handleSubmit SUCCESS, navigating to /profile/me"); const base = friendlyProfilePath(operation.path); navigate(`/profile/${base}`, { replace: true }); } catch (e: any) { console.log("[ProfileComponentWrapper] handleSubmit ERROR", e.message, e.response?.status); setError(e.response?.data?.detail ?? e.message ?? "Operation failed"); } finally { setLoading(false); } }; if (loading && !data && mode !== "create") { return ( ); } if (error && !data && mode !== "create") { return ( {error} ); } const props: Record = {}; if (mode === "view") { if (data) { props.name = data.name; props.username = data.username; props.email = data.email; } props.onEdit = () => navigate("/profile/me/edit"); props.loading = loading; } if (mode === "create") { props.defaultUsername = data?.username; props.defaultEmail = data?.email; props.onBack = () => window.history.back(); props.onSubmit = handleSubmit; props.loading = loading; props.error = error; } if (mode === "edit") { if (data) { props.name = data.name; props.username = data.username; props.email = data.email; } props.onBack = () => window.history.back(); props.onSubmit = handleSubmit; props.loading = loading; props.error = error; } return ; }