sdlc-hub/web/src/shell/Layout.tsx
2026-10-24 15:30:12 +04:00

76 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ProjectState>(() => 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<ProjectCtx>(() => ({ state, projects: projects.data ?? [], select, openNew: () => setNewOpen(true) }),
[state, projects.data, select]);
return (
<ProjectContext value={ctx}>
<ActivityProvider project={okLabel}>
<div className="shell">
<Sidebar section={section} project={okLabel} />
<main className={"page" + (NAV.find((n) => n.to === section)?.padded ? "" : " page--board")}>
<ErrorBoundary key={at}>{page}</ErrorBoundary>
</main>
</div>
{overlay?.element}
{newOpen && <NewProject onClose={() => setNewOpen(false)} select={select} />}
</ActivityProvider>
</ProjectContext>
);
}
// окно «Новый проект»: пакет, поставленный хабом при создании, отслеживает трекер ядра (модуль берётся из ответа хаба)
function NewProject({ onClose, select }: { onClose(): void; select(label: string): void }) {
const trackBatch = useTrackBatch();
return <NewProjectDialog onClose={onClose} onCreated={(p, batchId) => { onClose(); void trackBatch(batchId).catch(() => {}); select(p.label); }} />;
}