fix(web-modules): липкая шапка «Задач», порядок вики по ширине контейнера, «, просрочено»
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
46a1857706
commit
77ff12d54d
@ -146,8 +146,11 @@ type HubModule = {
|
||||
| 42 | 24.10.2026 | Task 5: поле поиска вики на «Обзоре» не ведёт на пустой запрос (Enter при пустой строке ничего не делает); `q` в «Вики» — только начальное значение поля, выбор страницы убирает `q` из адреса | пустой поиск = страница «Вики» без `q`, для неё есть ссылка «Вики»; `hrefOf` страницы по контракту Task 4 не несёт поиск | пользователь, вставший на `?q=` и открывший страницу, теряет в адресе строку поиска (в поле она остаётся) |
|
||||
| 43 | 24.10.2026 | Task 6: номер доски проверяется в окне сам (`/^\d+$/`, ≥ 1, текст «нужно целое число»), поле — `type="text"` с `inputMode="numeric"`, форма `noValidate` | `type="number"` даёт браузерную подсказку вместо нашего текста и пропускает `51.5`/`1e3` по-разному в браузерах; T4.1 | поле без стрелок счётчика; невалидное значение ловится только по «Создать» |
|
||||
| 44 | 24.10.2026 | Task 6: в ядре `useTrackModuleBatch()` (activity.ts) — отслеживание пакета по имени модуля; окно создания вынесено в `shell/NewProject.tsx`; `useTrackBatch` остаётся для пакетов без известного модуля | пакеты нового проекта привязываются каждому своему модулю по `batches[].module` (M-14, T5.3 #68); Layout не обязан знать модули | пакет неизвестного модуля молча не отслеживается |
|
||||
| 45 | 24.10.2026 | Task 7: узкий экран «Вики» (страница выше дерева) — через `matchMedia("(max-width: 759.98px)")` и порядок в разметке (`WikiPage`), а не CSS `order`; дерево при поиске скрыто (`hidden`), а не размонтировано | порядок Tab совпадает с видимым; раскрытые вручную узлы переживают поиск (T10.2, T10.6); порог окна — приближение порога контейнера страницы | CSS `order` и размонтирование дерева |
|
||||
| 45 | 24.10.2026 | Task 7: узкий экран «Вики» (страница выше дерева) — через `matchMedia("(max-width: 759.98px)")` и порядок в разметке (`WikiPage`), а не CSS `order`; дерево при поиске скрыто (`hidden`), а не размонтировано | порядок Tab совпадает с видимым; раскрытые вручную узлы переживают поиск (T10.2, T10.6); порог окна — приближение порога контейнера страницы (заменено решением 51: мерится ширина контейнера) | CSS `order` и размонтирование дерева |
|
||||
| 46 | 24.10.2026 | Task 7: заголовки текста страницы вики сдвинуты ниже заголовка страницы: `#`/`##` → h3, `###` → h4 … не глубже h6 | порядок h1 → h2 → h3 без скачков и без второго h1 (T10.3) | оставить h1–h4 как есть |
|
||||
| 47 | 24.10.2026 | Task 7: `.issue-table-wrap{overflow-x:auto}` на любой ширине (раньше — только `@container page < 1052px`) | на 1440 px девять столбцов шире панели; цена — липкая шапка таблицы липнет к панели, а не к окну | вернуть условие и сжать столбцы |
|
||||
| 47 | 24.10.2026 | Task 7: `.issue-table-wrap{overflow-x:auto}` на любой ширине (раньше — только `@container page < 1052px`) | на 1440 px девять столбцов шире панели; цена указана неверно: обёртка-скроллер выключает липкость шапки совсем — см. решение 50 | вернуть условие и сжать столбцы |
|
||||
| 48 | 24.10.2026 | Task 7: проверка T5.2 «ядро не грузит /board и /wiki/status» идёт на странице вне модулей (`/app/nowhere`), а не на `/wiki?project=…` | страница «Вики» сама запрашивает `/wiki/status` (это модуль, не ядро) | проверять на странице вики с исключением статуса |
|
||||
| 49 | 24.10.2026 | Task 7: `scrollbar-gutter: stable` — на `html` (прокручивается окно, отдельной области страницы нет); «Обновить из Jira» в шапках — общий `modules/jira/RefreshButton.tsx` | страницы разной высоты не двигаются по ширине; одна реализация кнопки на три страницы | прокручиваемый контейнер страницы; кнопка в каждой странице |
|
||||
| 50 | 24.10.2026 | Task 7, fix T7.I1: обёртка таблицы «Задач» при высоте окна ≥ 700 px получает `max-height: calc(100vh - 220px)` и `overflow-y:auto`, шапка липнет к обёртке; на низких окнах ограничения нет — шапка уходит со страницей | при `overflow-x:auto` обёртка — скроллер, и `position:sticky` шапки внутри неё без собственной вертикальной прокрутки не работает (решение 47 ошибочно считало, что шапка липнет к панели); на длинных списках шапка должна оставаться на виду; цена — вложенная вертикальная прокрутка (на телефонах высотой < 700 px её нет) | снять sticky/z-index и принять уходящую шапку |
|
||||
| 51 | 24.10.2026 | Task 7, fix T7.I2: порядок «страница/дерево» в `WikiPage` определяется `ResizeObserver` на `.wiki__body` с порогом `≤ 759.98` px — тем же, что `@container page` в `wiki.css`; `matchMedia` убран | окно и контейнер страницы (рядом боковая панель) расходятся, на окне ≈ 760–1050 px CSS уже в одну колонку, а DOM ещё нет; до первого замера (и без `ResizeObserver`) — широкая раскладка; ключи панелей сохраняют состояние дерева | вернуть `matchMedia` с запасом по ширине панели |
|
||||
| 52 | 24.10.2026 | Task 7, fix T7.I2: `useWikiDownload` возвращает объект через `useMemo` | перерисовка `WikiPage` (замер ширины) пересоздавала `{start,status}` → набор компонентов Markdown → ссылки-вложения перемонтировались (тест M-15 обнаружил); так же ломали бы их ввод в поиск и опрос статуса | не трогать хук и убрать состояние замера |
|
||||
|
||||
@ -175,7 +175,7 @@ describe("страница доски", () => {
|
||||
expect(screen.getByRole("button", { name: "Перечитать" })).toBeInTheDocument(); // меню действительно открыто
|
||||
expect(screen.queryByRole("button", { name: /^Тема/ })).not.toBeInTheDocument();
|
||||
});
|
||||
it("заголовок страницы — h1, в шапке label; строка состояния — только события доски", async () => {
|
||||
it("заголовок страницы — h1; строка состояния — только события доски", async () => {
|
||||
hub();
|
||||
page();
|
||||
await screen.findByText(/задач из 9/);
|
||||
|
||||
@ -17,7 +17,7 @@ function rowClick(e: MouseEvent<HTMLTableRowElement>, key: string, onOpen: (key:
|
||||
function DueCell({ due, late }: { due: string | null; late: boolean }) {
|
||||
return (
|
||||
<td className={late ? "overdue" : undefined} title={late ? "просрочено" : undefined}>
|
||||
{due ? fmtDate(due) : ""}{late && <span className="visually-hidden">просрочено</span>}
|
||||
{due ? fmtDate(due) : ""}{late && <span className="visually-hidden">, просрочено</span>}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@ -365,11 +365,12 @@ describe("список задач", () => {
|
||||
renderWithProviders(<IssuesPage project="AWPCA" />, { route: "/issues?closed=1", path: "/issues" });
|
||||
await screen.findByRole("link", { name: "AURORA-9001" });
|
||||
const cell = await waitFor(() => {
|
||||
const c = within(screen.getByRole("row", { name: /AURORA-9001/ })).getByText("просрочено").closest("td")!;
|
||||
const c = within(screen.getByRole("row", { name: /AURORA-9001/ })).getByText(", просрочено").closest("td")!;
|
||||
expect(c).toHaveClass("overdue");
|
||||
return c;
|
||||
});
|
||||
expect(within(cell).getByText("просрочено")).toHaveClass("visually-hidden");
|
||||
expect(within(cell).getByText(", просрочено")).toHaveClass("visually-hidden");
|
||||
expect(cell.textContent).toBe("01.01.2000, просрочено");
|
||||
expect(cell).toHaveAttribute("title", "просрочено");
|
||||
expect(cell).toHaveTextContent("01.01.2000");
|
||||
});
|
||||
|
||||
@ -9,8 +9,11 @@
|
||||
.issue-filters__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-3) var(--space-4)}
|
||||
@container page (max-width:851.98px){.issue-filters__grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@container page (max-width:607.98px){.issue-filters__grid{grid-template-columns:minmax(0,1fr)}.issue-filters__search{max-width:none}}
|
||||
/* таблица прокручивается внутри своей панели на любой ширине (девять столбцов шире контейнера и на 1440 px); обёртка не шире контейнера страницы. Цена: липкая шапка таблицы липнет к панели, а не к окну (R-5 Task 7) */
|
||||
/* таблица прокручивается внутри своей панели на любой ширине (девять столбцов шире контейнера и на 1440 px); обёртка не шире контейнера страницы.
|
||||
Обёртка — скроллер, поэтому липкая шапка липнет к ней, а не к окну: чтобы шапка оставалась на виду, высота обёртки ограничена высотой окна
|
||||
и длинный список прокручивается внутри панели (на низких окнах — меньше 700 px — ограничения нет, шапка уходит вместе со страницей) */
|
||||
.issue-table-wrap{overflow-x:auto;max-width:100%;min-width:0}
|
||||
@media (min-height:700px){.issue-table-wrap{max-height:calc(100vh - 220px);overflow-y:auto}}
|
||||
.issue-table th:first-child{border-top-left-radius:calc(var(--radius-xl) - 1px)}
|
||||
.issue-table th:last-child{border-top-right-radius:calc(var(--radius-xl) - 1px)}
|
||||
.issue-table tbody tr:last-child td:first-child{border-bottom-left-radius:calc(var(--radius-xl) - 1px)}
|
||||
|
||||
@ -109,14 +109,31 @@ describe("страница «Вики»", () => {
|
||||
levels.slice(1).forEach((l, i) => { expect(l).toBeGreaterThan(1); expect(l - (levels[i] ?? 0)).toBeLessThanOrEqual(1); });
|
||||
});
|
||||
|
||||
it("на узком экране страница выше дерева", async () => {
|
||||
vi.stubGlobal("matchMedia", (q: string) => ({ matches: q.includes("max-width"), media: q, addEventListener() {}, removeEventListener() {} }));
|
||||
// jsdom без вёрстки: ResizeObserver подменён, ширину страницы сообщает тест
|
||||
const withWidth = (width: number) => {
|
||||
class RO { cb: (e: unknown[]) => void; constructor(cb: (e: unknown[]) => void) { this.cb = cb; } observe() { this.cb([{ contentRect: { width } }]); } disconnect() {} unobserve() {} }
|
||||
vi.stubGlobal("ResizeObserver", RO);
|
||||
};
|
||||
|
||||
it("на узкой странице (ширина контейнера ≤ 759.98) страница выше дерева", async () => {
|
||||
withWidth(700);
|
||||
try {
|
||||
hub();
|
||||
open("/wiki?project=AWPCA&page=DOC-2");
|
||||
const article = await screen.findByRole("article");
|
||||
const tree = await screen.findByRole("tree");
|
||||
expect(article.compareDocumentPosition(tree) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
await waitFor(() => expect(article.compareDocumentPosition(tree) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy());
|
||||
} finally { vi.unstubAllGlobals(); }
|
||||
});
|
||||
|
||||
it("на странице шире порога дерево выше страницы", async () => {
|
||||
withWidth(900);
|
||||
try {
|
||||
hub();
|
||||
open("/wiki?project=AWPCA&page=DOC-2");
|
||||
const article = await screen.findByRole("article");
|
||||
const tree = await screen.findByRole("tree");
|
||||
expect(tree.compareDocumentPosition(article) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
} finally { vi.unstubAllGlobals(); }
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { ApiError } from "../../api/client.ts";
|
||||
import { Button, IconRefresh, Notice, PageHeader, Panel } from "../../ui/index.ts";
|
||||
@ -13,15 +13,23 @@ import "./wiki.css";
|
||||
|
||||
const Q_DELAY = 300; // поиск — после паузы ввода
|
||||
|
||||
// узкий экран: страница выше дерева (порядок в разметке, чтобы совпадал с порядком обхода Tab); порог = порог одной колонки в wiki.css
|
||||
const NARROW = "(max-width: 759.98px)";
|
||||
const subscribeNarrow = (cb: () => void) => {
|
||||
if (typeof matchMedia !== "function") return () => {};
|
||||
const m = matchMedia(NARROW);
|
||||
m.addEventListener?.("change", cb);
|
||||
return () => m.removeEventListener?.("change", cb);
|
||||
};
|
||||
const isNarrow = () => typeof matchMedia === "function" && matchMedia(NARROW).matches;
|
||||
// узкая страница: она выше дерева (порядок в разметке — чтобы совпадал с порядком обхода Tab). Мерится ширина самой страницы
|
||||
// (.wiki__body = контейнер `page` в wiki.css), порог тот же, что у @container page (max-width:759.98px): DOM и сетка переключаются вместе
|
||||
const NARROW_PX = 759.98;
|
||||
function useNarrowBody() {
|
||||
const [el, setEl] = useState<HTMLDivElement | null>(null);
|
||||
const [narrow, setNarrow] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!el || typeof ResizeObserver !== "function") return;
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const w = entries[entries.length - 1]?.contentRect.width;
|
||||
if (typeof w === "number") setNarrow(w <= NARROW_PX);
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [el]);
|
||||
return [narrow, setEl] as const;
|
||||
}
|
||||
|
||||
export function WikiPage({ project }: { project: string }) {
|
||||
const [params] = useSearchParams();
|
||||
@ -44,7 +52,7 @@ export function WikiPage({ project }: { project: string }) {
|
||||
}, [q]);
|
||||
const search = useWikiSearch(project, dq);
|
||||
const searching = q.trim() !== "";
|
||||
const narrow = useSyncExternalStore(subscribeNarrow, isNarrow, () => false);
|
||||
const [narrow, bodyRef] = useNarrowBody();
|
||||
|
||||
if (status.isPending) return <div className="wiki"><p className="wiki-hint">Загрузка…</p></div>;
|
||||
if (noRoot) return <div className="wiki"><PageHeader title="Вики" /><RootForm project={project} /></div>;
|
||||
@ -79,7 +87,7 @@ export function WikiPage({ project }: { project: string }) {
|
||||
{!synced ? (
|
||||
<Notice tone="info">{running ? "Вики синхронизируется…" : "Вики ещё не синхронизирована. Нажмите «Обновить вики»."}</Notice>
|
||||
) : (
|
||||
<div className="wiki__body">
|
||||
<div className="wiki__body" ref={bodyRef}>
|
||||
{narrow ? <>{content}{nav}</> : <>{nav}{content}</>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { api, ApiError } from "../../api/client.ts";
|
||||
import type { WikiAttachmentState } from "./api/queries.ts";
|
||||
|
||||
@ -72,5 +72,6 @@ export function useWikiDownload(project: string, scope: string) {
|
||||
}, [put]);
|
||||
|
||||
const status = useCallback((id: string): DownloadStatus => map[id] ?? IDLE, [map]);
|
||||
return { start, status };
|
||||
// объект стабилен между перерисовками родителя: от него зависит набор компонентов Markdown, смена ссылки перемонтирует вложения
|
||||
return useMemo(() => ({ start, status }), [start, status]);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user