Compare commits

..

No commits in common. "1095d41bd5a82ab25ec46e6ceb8f6ab7c7ae1093" and "61704893e90bb4a4d00a45b2cdbad712cd73e347" have entirely different histories.

6 changed files with 19 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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