import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { matchPath, useLocation, useNavigate, useRoutes } from "react-router"; import { useProjects } from "../api/queries.ts"; import { CloseAbortContext } from "../issues/closeAbort.ts"; import { IssueModal } from "../issues/IssueModal.tsx"; import { cardState, LIST_BACKGROUND } from "../issues/modal.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 { AppBar } from "./AppBar.tsx"; import { ErrorBoundary } from "./ErrorBoundary.tsx"; import { PAGE_ROUTES } from "./pages.tsx"; import { useJiraRefresh } from "./refresh.ts"; import "./shell.css"; export function Layout() { const location = useLocation(); const navigate = useNavigate(); const issueKey = matchPath("/issues/:key", location.pathname)?.params.key ?? null; const card = issueKey ? cardState(location.state) : null; const bgPath = issueKey ? (card?.background ?? LIST_BACKGROUND).pathname : null; const bgSearch = issueKey ? (card?.background ?? LIST_BACKGROUND).search : ""; // адрес страницы под окном; передаётся в useRoutes всегда (и без окна), чтобы дерево страницы не менялось при // открытии окна и useSearchParams внутри неё видел её собственный адрес (design 10) const bgLocation = useMemo(() => bgPath === null ? null : { pathname: bgPath, search: bgSearch, hash: "", state: null, key: "card-background" }, [bgPath, bgSearch]); const pageLocation = bgLocation ?? location; const page = useRoutes(PAGE_ROUTES, pageLocation); // прямой адрес карточки: запись истории получает state окна поверх списка задач (design 11) useEffect(() => { if (issueKey && !card) { void navigate(location.pathname + location.search, { replace: true, state: { background: LIST_BACKGROUND, depth: 0, direct: true } }); } }, [issueKey, card, location.pathname, location.search, navigate]); // закрытие (design 12): назад на depth записей; запись прямого адреса, на которую вернулись, заменяется списком const closingDirect = useRef(false); useEffect(() => { if (closingDirect.current && card?.direct && card.depth === 0) { closingDirect.current = false; void navigate("/issues", { replace: true }); } }, [card, navigate]); const close = useCallback(() => { if (!card || card.depth === 0) { void navigate("/issues", { replace: true }); return; } closingDirect.current = card.direct; void navigate(-card.depth); }, [card, navigate]); const abortClose = useCallback(() => { closingDirect.current = false; }, []); 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 onPage = at === "/board" || at === "/issues"; useEffect(() => { if (okLabel) rememberProject(okLabel); }, [okLabel]); // выбранный проект записывается в адрес без новой записи истории; с открытой карточкой адрес не трогаем (design 10) useEffect(() => { if (okLabel && !fromUrl && issueKey === null && onPage) { void navigate(at + withProject(pageLocation.search, okLabel), { replace: true }); } }, [okLabel, fromUrl, issueKey, onPage, at, pageLocation.search, navigate]); const [newOpen, setNewOpen] = useState(false); const section = at === "/issues" || at === "/board" ? at : null; const select = useCallback((label: string) => { void navigate({ pathname: section ?? "/board", search: withProject("", label) }); }, [navigate, section]); const ctx = useMemo(() => ({ state, projects: projects.data ?? [], select, openNew: () => setNewOpen(true) }), [state, projects.data, select]); const refresh = useJiraRefresh(); return (
{page}
{issueKey && } {newOpen && setNewOpen(false)} onCreated={(p, batchId) => { setNewOpen(false); refresh.track(batchId); select(p.label); }} />}
); }