76 lines
4.5 KiB
TypeScript
76 lines
4.5 KiB
TypeScript
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); }} />;
|
||
}
|