Веб: «Сбросить фильтры» на месте, без «Скрыть выполненные» на «Спринте» (#118) #119

Merged
aep merged 1 commits from feat/118-board-filters into main 2026-10-25 21:18:00 +03:00
6 changed files with 31 additions and 19 deletions

View File

@ -852,9 +852,11 @@ Jira» — `POST /refresh {full: true, author: "viewer"}`; после пакет
**Панель фильтров** — над содержимым «Бэклога», «Спринта» и «Kanban»: поиск «Ключ или тема» (подстрока без учёта
регистра в ключе или теме), выбор «Исполнитель» («все», «Без исполнителя», исполнители задач страницы по алфавиту),
переключатель «Скрыть выполненные» (только «Бэклог» и «Спринт»); на «Бэклоге» — кнопка «Эпики» (показать или скрыть
панель эпиков); на «Спринте» — выбор спринтов; на «Спринте» и «Kanban» — меню «Колонки». Если задан хотя бы один фильтр
(поиск, исполнитель, эпик, «Скрыть выполненные») — кнопка «Сбросить фильтры», снимающая их все. Фильтры действуют вместе
переключатель «Скрыть выполненные» (только «Бэклог»; на «Спринте» и «Kanban» выполненное скрывают колонкой, сохранённое
значение там не применяется); на «Бэклоге» — кнопка «Эпики» (показать или скрыть панель эпиков); на «Спринте» — выбор
спринтов; на «Спринте» и «Kanban» — меню «Колонки». Кнопка «Сбросить фильтры» SHALL быть в панели всегда и снимать все
фильтры (поиск, исполнитель, эпик, «Скрыть выполненные»); пока ни один не задан, она неактивна — панель не сдвигается,
когда фильтр появляется или снимается. Фильтры действуют вместе
(И). Сохранённое значение исполнителя или эпика, которого нет в данных, SHALL оставаться выбранным и видимым в списке.
**Запоминание:** свёрнутые секции, выбранный эпик, исполнитель, «Скрыть выполненные», видимость панели эпиков и скрытые
@ -947,6 +949,11 @@ Jira» — `POST /refresh {full: true, author: "viewer"}`; после пакет
- **WHEN** у доски активный спринт 101 и будущий 102; в выборе спринтов отмечен и 102
- **THEN** адрес — `?sprint=101,102`; над доской два заголовка, «осталось N дн.» только у 101; на доске задачи обоих
#### Scenario: Сброс фильтров на месте
- **WHEN** открыт «Спринт» без фильтров, затем выбран исполнитель
- **THEN** «Сбросить фильтры» видна и неактивна до выбора, после — активна на том же месте; флажка «Скрыть выполненные»
на «Спринте» нет
#### Scenario: Скрытая колонка
- **WHEN** на «Спринте» в меню «Колонки» снята отметка «Выполнено», страница перезагружена
- **THEN** колонки «Выполнено» нет, на кнопке «Колонки» — 1; остальные колонки на месте

View File

@ -42,9 +42,9 @@ describe("AgileToolbar", () => {
expect(stored().AWPCA.backlog).not.toHaveProperty("q");
});
it("«Сбросить фильтры» появляется после ввода и снимает всё", () => {
it("«Сбросить фильтры» всегда на месте: неактивна без фильтров, после ввода снимает всё (#118)", () => {
render(<Harness />);
expect(screen.queryByText("Сбросить фильтры")).toBeNull();
expect(screen.getByRole("button", { name: "Сбросить фильтры" })).toBeDisabled();
fireEvent.change(search(), { target: { value: "abc" } });
fireEvent.change(assignee(), { target: { value: "petrov" } });
fireEvent.click(screen.getByLabelText("Скрыть выполненные"));
@ -52,7 +52,7 @@ describe("AgileToolbar", () => {
expect(search().value).toBe("");
expect(assignee().value).toBe("");
expect((screen.getByLabelText("Скрыть выполненные") as HTMLInputElement).checked).toBe(false);
expect(screen.queryByText("Сбросить фильтры")).toBeNull();
expect(screen.getByRole("button", { name: "Сбросить фильтры" })).toBeDisabled();
expect(stored().AWPCA.backlog).toMatchObject({ assignee: "", epic: "", hideDone: false });
});
@ -108,9 +108,9 @@ describe("AgileToolbar", () => {
assignee: "", epic: "", hideDone: false, collapsed: ["E-1"], hiddenColumns: ["Done"], epicsPanel: false });
});
it("showHideDone=false: сохранённый hideDone не показывает «Сбросить фильтры»", () => {
it("showHideDone=false: сохранённый hideDone не делает «Сбросить фильтры» активной", () => {
localStorage.setItem(AGILE_KEY, JSON.stringify({ AWPCA: { backlog: { hideDone: true } } }));
render(<Harness showHideDone={false} />);
expect(screen.queryByText("Сбросить фильтры")).toBeNull();
expect(screen.getByRole("button", { name: "Сбросить фильтры" })).toBeDisabled();
});
});

View File

@ -22,7 +22,8 @@ export function AgileToolbar({ filter, setQ, set, onReset, assignees, showHideDo
<Checkbox label="Скрыть выполненные" checked={filter.hideDone} onChange={(e) => set({ hideDone: e.target.checked })} />
)}
{children}
{hasFilters({ ...filter, hideDone: filter.hideDone && showHideDone }) && <Button variant="ghost" onClick={onReset}>Сбросить фильтры</Button>}
{/* всегда на месте, без фильтров неактивна: панель не сдвигается при первом фильтре (#118) */}
<Button variant="ghost" disabled={!hasFilters({ ...filter, hideDone: filter.hideDone && showHideDone })} onClick={onReset}>Сбросить фильтры</Button>
</Panel>
);
}

View File

@ -139,7 +139,7 @@ describe("«Kanban»", () => {
open("/app/jira/kanban?project=KAN");
const p = await page();
expect(await within(p).findByText("Тема AURORA-4")).toBeInTheDocument();
expect(within(p).queryByRole("button", { name: "Сбросить фильтры" })).toBeNull();
expect(within(p).getByRole("button", { name: "Сбросить фильтры" })).toBeDisabled();
});
it("скрытая колонка: снять «Готово» — колонки нет; имя колонки, которой нет, в хранилище не ломает меню", async () => {

View File

@ -216,16 +216,18 @@ describe("«Спринт»", () => {
expect(await within(b).findByLabelText("Исполнитель")).toHaveValue("");
});
it("«Скрыть выполненные» убирает выполненные карточки", async () => {
it("флажка «Скрыть выполненные» нет, сохранённый hideDone не применяется; «Сбросить фильтры» неактивна (#118)", async () => {
localStorage.setItem("sdlc-hub-agile", JSON.stringify({ AWPCA: { sprint: { hideDone: true } } }));
hub(agile());
open("/app/jira/sprint?project=AWPCA");
const p = await page();
await within(p).findByText("Тема AURORA-2");
await userEvent.click(within(p).getByRole("checkbox", { name: "Скрыть выполненные" }));
expect(within(p).queryByText("Тема AURORA-2")).toBeNull();
expect(within(p).getByText("Тема AURORA-1")).toBeInTheDocument();
const cols = Array.from(p.querySelectorAll(".agile-col"));
expect(cols.map((c) => c.querySelector(".agile-col__count")!.textContent)).toEqual(["1", "0", "1"]);
expect(await within(p).findByText("Тема AURORA-2")).toBeInTheDocument();
expect(within(p).queryByRole("checkbox", { name: "Скрыть выполненные" })).toBeNull();
const reset = within(p).getByRole("button", { name: "Сбросить фильтры" });
expect(reset).toBeDisabled();
await userEvent.type(within(p).getByLabelText("Поиск по ключу или теме"), "AURORA-1");
expect(within(p).getByRole("button", { name: "Сбросить фильтры" })).toBe(reset); // тот же элемент — панель не перестраивается
expect(reset).toBeEnabled();
});
it("заголовки колонок — h3 (T10.3)", async () => {

View File

@ -31,7 +31,9 @@ function SprintHead({ s }: { s: AgileSprint }) {
// «Спринт» (spec web-app): колонки доски выбранных спринтов (по умолчанию все активные); только просмотр
export function SprintPage({ project }: { project: Project }) {
const [params, setParams] = useSearchParams();
const { view, set, setQ, filter, reset } = useAgileView(project.label, "sprint");
const { view, set, setQ, filter: saved, reset } = useAgileView(project.label, "sprint");
// «Скрыть выполненные» здесь нет — то же делает скрытие колонки; сохранённое раньше hideDone не применяется (#118)
const filter = { ...saved, hideDone: false };
return (
<div data-testid="page-sprint" className="agile agile--wide">
<PageHeader title="Спринт" actions={<RefreshButton />} />
@ -48,7 +50,7 @@ export function SprintPage({ project }: { project: Project }) {
const pick = (next: number[]) => setParams((p) => { const n = new URLSearchParams(p); n.set("sprint", next.join(",")); return n; }, { replace: true });
return (
<>
<AgileToolbar filter={filter} setQ={setQ} set={set} onReset={reset} assignees={assigneeOptions(issues, view.assignee)} showHideDone>
<AgileToolbar filter={filter} setQ={setQ} set={set} onReset={reset} assignees={assigneeOptions(issues, view.assignee)} showHideDone={false}>
{selectable.length > 0 && <SprintPicker sprints={selectable} selected={ids} onChange={pick} />}
{chosen.length > 0 && <ColumnsMenu names={allNames} hidden={view.hiddenColumns} onChange={(hiddenColumns) => set({ hiddenColumns })} />}
</AgileToolbar>