import { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate, useRoutes } from "react-router"; import { useProjects } from "../api/queries.ts"; import { MODULES, useOverlays } from "../modules/index.ts"; import { rememberedProject, rememberProject, resolveProject, withProject } from "../projects/current.ts"; import { NewProjectDialog } from "../projects/NewProjectDialog.tsx"; import { ProjectContext, type ProjectCtx, type ProjectState } from "../projects/ProjectContext.ts"; import { ActivityProvider } from "./activity/ActivityProvider.tsx"; import { useTrackBatch } from "./activity/activity.ts"; import { ErrorBoundary } from "./ErrorBoundary.tsx"; import { PAGE_ROUTES } from "./pages.tsx"; import { Sidebar } from "./Sidebar.tsx"; import "./shell.css"; const NAV = MODULES.flatMap((m) => m.nav); const NAV_PATHS: readonly string[] = NAV.map((n) => n.to); export function Layout() { const location = useLocation(); const navigate = useNavigate(); // окно модуля (карточка задачи): первое, вернувшее адрес страницы под собой, определяет и страницу, и окно const overlay = useOverlays().find((o) => o.pageLocation !== null) ?? null; const hasOverlay = overlay !== null; const pageLocation = overlay?.pageLocation ?? location; const page = useRoutes(PAGE_ROUTES, pageLocation); const at = pageLocation.pathname; // текущий проект: адрес страницы (под окном карточки — адрес списка) → запомненный → первый; карточка от него не зависит const projects = useProjects(); const urlLabel = new URLSearchParams(pageLocation.search).get("project"); const state = useMemo(() => projects.isPending ? { kind: "loading" } : projects.error && !projects.data ? { kind: "error", error: projects.error } // ошибка фонового рефетча — остаётся последний список : resolveProject(urlLabel, rememberedProject(), projects.data), [projects.isPending, projects.error, projects.data, urlLabel]); const okLabel = state.kind === "ok" ? state.project.label : null; const fromUrl = state.kind === "ok" && state.fromUrl; const section = NAV_PATHS.includes(at) ? at : null; const onPage = section !== null; useEffect(() => { if (okLabel) rememberProject(okLabel); }, [okLabel]); // выбранный проект записывается в адрес без новой записи истории; с открытой карточкой адрес не трогаем (design 10) useEffect(() => { if (okLabel && !fromUrl && !hasOverlay && onPage) { void navigate(at + withProject(pageLocation.search, okLabel), { replace: true }); } }, [okLabel, fromUrl, hasOverlay, onPage, at, pageLocation.search, navigate]); const [newOpen, setNewOpen] = useState(false); // смена проекта: фильтры доски и списка — проектные и сбрасываются; период и виды ленты от проекта не зависят (R-1) const select = useCallback((label: string) => { void navigate({ pathname: section ?? NAV_PATHS[0], search: withProject(NAV.find((n) => n.to === section)?.keepSearch ? pageLocation.search : "", label) }); }, [navigate, section, pageLocation.search]); const ctx = useMemo(() => ({ state, projects: projects.data ?? [], select, openNew: () => setNewOpen(true) }), [state, projects.data, select]); return (
n.to === section)?.padded ? "" : " page--board")}> {page}
{overlay?.element} {newOpen && setNewOpen(false)} select={select} />}
); } // окно «Новый проект»: пакет, поставленный хабом при создании, отслеживает трекер ядра (модуль берётся из ответа хаба) function NewProject({ onClose, select }: { onClose(): void; select(label: string): void }) { const trackBatch = useTrackBatch(); return { onClose(); void trackBatch(batchId).catch(() => {}); select(p.label); }} />; }