ui: add company-aware not found handling
This commit is contained in:
@@ -28,6 +28,7 @@ import { NewAgent } from "./pages/NewAgent";
|
||||
import { AuthPage } from "./pages/Auth";
|
||||
import { BoardClaimPage } from "./pages/BoardClaim";
|
||||
import { InviteLandingPage } from "./pages/InviteLanding";
|
||||
import { NotFoundPage } from "./pages/NotFound";
|
||||
import { queryKeys } from "./lib/queryKeys";
|
||||
import { useCompany } from "./context/CompanyContext";
|
||||
import { useDialog } from "./context/DialogContext";
|
||||
@@ -141,6 +142,7 @@ function boardRoutes() {
|
||||
<Route path="inbox/new" element={<Inbox />} />
|
||||
<Route path="inbox/all" element={<Inbox />} />
|
||||
<Route path="design-guide" element={<DesignGuide />} />
|
||||
<Route path="*" element={<NotFoundPage scope="board" />} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -240,6 +242,7 @@ export function App() {
|
||||
<Route path=":companyPrefix" element={<Layout />}>
|
||||
{boardRoutes()}
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage scope="global" />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<OnboardingWizard />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState, type UIEvent } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type UIEvent } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { BookOpen, Moon, Sun } from "lucide-react";
|
||||
import { Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
|
||||
@@ -24,13 +24,20 @@ import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
|
||||
import { healthApi } from "../api/health";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { cn } from "../lib/utils";
|
||||
import { NotFoundPage } from "../pages/NotFound";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function Layout() {
|
||||
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
|
||||
const { openNewIssue, openOnboarding } = useDialog();
|
||||
const { togglePanelVisible } = usePanel();
|
||||
const { companies, loading: companiesLoading, selectedCompanyId, setSelectedCompanyId } = useCompany();
|
||||
const {
|
||||
companies,
|
||||
loading: companiesLoading,
|
||||
selectedCompany,
|
||||
selectedCompanyId,
|
||||
setSelectedCompanyId,
|
||||
} = useCompany();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { companyPrefix } = useParams<{ companyPrefix: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -39,6 +46,13 @@ export function Layout() {
|
||||
const lastMainScrollTop = useRef(0);
|
||||
const [mobileNavVisible, setMobileNavVisible] = useState(true);
|
||||
const nextTheme = theme === "dark" ? "light" : "dark";
|
||||
const matchedCompany = useMemo(() => {
|
||||
if (!companyPrefix) return null;
|
||||
const requestedPrefix = companyPrefix.toUpperCase();
|
||||
return companies.find((company) => company.issuePrefix.toUpperCase() === requestedPrefix) ?? null;
|
||||
}, [companies, companyPrefix]);
|
||||
const hasUnknownCompanyPrefix =
|
||||
Boolean(companyPrefix) && !companiesLoading && companies.length > 0 && !matchedCompany;
|
||||
const { data: health } = useQuery({
|
||||
queryKey: queryKeys.health,
|
||||
queryFn: () => healthApi.get(),
|
||||
@@ -57,30 +71,30 @@ export function Layout() {
|
||||
useEffect(() => {
|
||||
if (!companyPrefix || companiesLoading || companies.length === 0) return;
|
||||
|
||||
const requestedPrefix = companyPrefix.toUpperCase();
|
||||
const matched = companies.find((company) => company.issuePrefix.toUpperCase() === requestedPrefix);
|
||||
|
||||
if (!matched) {
|
||||
const fallback =
|
||||
(selectedCompanyId ? companies.find((company) => company.id === selectedCompanyId) : null)
|
||||
?? companies[0]!;
|
||||
navigate(`/${fallback.issuePrefix}/dashboard`, { replace: true });
|
||||
if (!matchedCompany) {
|
||||
const fallback = (selectedCompanyId ? companies.find((company) => company.id === selectedCompanyId) : null)
|
||||
?? companies[0]
|
||||
?? null;
|
||||
if (fallback && selectedCompanyId !== fallback.id) {
|
||||
setSelectedCompanyId(fallback.id, { source: "route_sync" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (companyPrefix !== matched.issuePrefix) {
|
||||
if (companyPrefix !== matchedCompany.issuePrefix) {
|
||||
const suffix = location.pathname.replace(/^\/[^/]+/, "");
|
||||
navigate(`/${matched.issuePrefix}${suffix}${location.search}`, { replace: true });
|
||||
navigate(`/${matchedCompany.issuePrefix}${suffix}${location.search}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedCompanyId !== matched.id) {
|
||||
setSelectedCompanyId(matched.id, { source: "route_sync" });
|
||||
if (selectedCompanyId !== matchedCompany.id) {
|
||||
setSelectedCompanyId(matchedCompany.id, { source: "route_sync" });
|
||||
}
|
||||
}, [
|
||||
companyPrefix,
|
||||
companies,
|
||||
companiesLoading,
|
||||
matchedCompany,
|
||||
location.pathname,
|
||||
location.search,
|
||||
navigate,
|
||||
@@ -282,7 +296,14 @@ export function Layout() {
|
||||
className={cn("flex-1 overflow-auto p-4 md:p-6", isMobile && "pb-[calc(5rem+env(safe-area-inset-bottom))]")}
|
||||
onScroll={handleMainScroll}
|
||||
>
|
||||
<Outlet />
|
||||
{hasUnknownCompanyPrefix ? (
|
||||
<NotFoundPage
|
||||
scope="invalid_company_prefix"
|
||||
requestedPrefix={companyPrefix ?? selectedCompany?.issuePrefix}
|
||||
/>
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</main>
|
||||
<PropertiesPanel />
|
||||
</div>
|
||||
|
||||
66
ui/src/pages/NotFound.tsx
Normal file
66
ui/src/pages/NotFound.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useEffect } from "react";
|
||||
import { Link, useLocation } from "@/lib/router";
|
||||
import { AlertTriangle, Compass } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
|
||||
type NotFoundScope = "board" | "invalid_company_prefix" | "global";
|
||||
|
||||
interface NotFoundPageProps {
|
||||
scope?: NotFoundScope;
|
||||
requestedPrefix?: string;
|
||||
}
|
||||
|
||||
export function NotFoundPage({ scope = "global", requestedPrefix }: NotFoundPageProps) {
|
||||
const location = useLocation();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const { companies, selectedCompany } = useCompany();
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([{ label: "Not Found" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const fallbackCompany = selectedCompany ?? companies[0] ?? null;
|
||||
const dashboardHref = fallbackCompany ? `/${fallbackCompany.issuePrefix}/dashboard` : "/";
|
||||
const currentPath = `${location.pathname}${location.search}${location.hash}`;
|
||||
const normalizedPrefix = requestedPrefix?.toUpperCase();
|
||||
|
||||
const title = scope === "invalid_company_prefix" ? "Company not found" : "Page not found";
|
||||
const description =
|
||||
scope === "invalid_company_prefix"
|
||||
? `No company matches prefix "${normalizedPrefix ?? "unknown"}".`
|
||||
: "This route does not exist.";
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl py-10">
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-md border border-destructive/20 bg-destructive/10 p-2">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">{title}</h1>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-md border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||
Requested path: <code className="font-mono">{currentPath}</code>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<Button asChild>
|
||||
<Link to={dashboardHref}>
|
||||
<Compass className="mr-1.5 h-4 w-4" />
|
||||
Open dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/">Go home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user