fix(web): scope Suspense to page body; guard formatConfig against null config

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 16:12:21 +07:00
parent 8c35aed8f2
commit 7256adf637
3 changed files with 22 additions and 16 deletions
+3 -1
View File
@@ -24,7 +24,9 @@ test("renders navigation and redirects to domains", async () => {
)
// Sidebar nav also renders a "Domains" link label, so scope the assertion
// to the routed page content to unambiguously confirm the redirect + page.
// Suspense is now scoped inside <main>, so <main> mounts with the loading
// fallback first — await the lazy chunk resolving to the actual page text.
const main = await screen.findByRole("main")
expect(within(main).getByText("Domains")).toBeInTheDocument()
expect(await within(main).findByText("Domains")).toBeInTheDocument()
expect(screen.getByRole("link", { name: /domains/i })).toBeInTheDocument()
})
+18 -14
View File
@@ -17,28 +17,32 @@ const ChannelsPage = lazy(() => import("@/pages/ChannelsPage").then((m) => ({ de
// Every non-auth route shares the same guard + chrome; wrapping here keeps
// each <Route> below a one-liner instead of repeating both on every page.
// Suspense is scoped to just the page body (not Layout) so lazy-loading a
// route doesn't collapse the sidebar/header chrome to the fallback on nav.
function Protected({ children }: { children: ReactNode }) {
return (
<ProtectedRoute>
<Layout>{children}</Layout>
<Layout>
<Suspense fallback={<div className="p-6 text-muted-foreground">Загрузка</div>}>
{children}
</Suspense>
</Layout>
</ProtectedRoute>
)
}
export function App() {
return (
<Suspense fallback={<div className="p-6 text-muted-foreground">Загрузка</div>}>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/" element={<Navigate to="/domains" replace />} />
<Route path="/domains" element={<Protected><DomainsPage /></Protected>} />
<Route path="/domains/:id" element={<Protected><DomainDiffPage /></Protected>} />
<Route path="/accounts" element={<Protected><AccountsPage /></Protected>} />
<Route path="/templates" element={<Protected><TemplatesPage /></Protected>} />
<Route path="/schedule" element={<Protected><SchedulePage /></Protected>} />
<Route path="/channels" element={<Protected><ChannelsPage /></Protected>} />
</Routes>
</Suspense>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/" element={<Navigate to="/domains" replace />} />
<Route path="/domains" element={<Protected><DomainsPage /></Protected>} />
<Route path="/domains/:id" element={<Protected><DomainDiffPage /></Protected>} />
<Route path="/accounts" element={<Protected><AccountsPage /></Protected>} />
<Route path="/templates" element={<Protected><TemplatesPage /></Protected>} />
<Route path="/schedule" element={<Protected><SchedulePage /></Protected>} />
<Route path="/channels" element={<Protected><ChannelsPage /></Protected>} />
</Routes>
)
}
+1 -1
View File
@@ -74,7 +74,7 @@ const EMPTY_FORM: ChannelForm = { type: "telegram", chatId: "", botToken: "", ur
// (Object.entries по всему config печатал бы любое поле, включая случайно
// сохранённый секрет).
function formatConfig(type: string, config: object): string {
const c = config as Record<string, unknown>
const c = (config ?? {}) as Record<string, unknown>
if (type === "telegram") return c.chat_id ? `chat_id: ${String(c.chat_id)}` : ""
if (type === "webhook") return c.url ? `url: ${String(c.url)}` : ""
return ""