ui: add company-aware not found handling

This commit is contained in:
Dotta
2026-03-10 16:38:46 -05:00
parent 50db379db2
commit d62b89cadd
3 changed files with 105 additions and 15 deletions

View File

@@ -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 />

View File

@@ -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
View 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>
);
}