diff --git a/web/src/modules/jira/agile/BacklogPage.test.tsx b/web/src/modules/jira/agile/BacklogPage.test.tsx index a2be107..f00effe 100644 --- a/web/src/modules/jira/agile/BacklogPage.test.tsx +++ b/web/src/modules/jira/agile/BacklogPage.test.tsx @@ -144,6 +144,27 @@ describe("«Бэклог»", () => { await waitFor(() => expect(router.state.location.pathname).toBe("/app/jira/backlog")); }); + it("выделение вне строки не мешает открыть карточку", async () => { + hub(); + const router = open("/app/jira/backlog?project=AWPCA"); + const p = await loaded(); + window.getSelection()!.selectAllChildren(within(p).getByRole("heading", { name: "AWPCA_Sprint_4" })); + await userEvent.click(within(p).getByText("Тема AURORA-1")); + await screen.findByRole("dialog"); + expect(router.state.location.pathname).toBe("/app/jira/issues/AURORA-1"); + window.getSelection()!.removeAllRanges(); + }); + + it("щелчок по ссылке ключа открывает карточку один раз", async () => { + hub(); + const router = open("/app/jira/backlog?project=AWPCA"); + const p = await loaded(); + await userEvent.click(within(p).getByRole("link", { name: "AURORA-1" })); + await screen.findByRole("dialog"); + await userEvent.click(within(screen.getByRole("dialog")).getByRole("button", { name: "Закрыть" })); + await waitFor(() => expect(router.state.location.pathname).toBe("/app/jira/backlog")); // одна запись истории, а не две + }); + it("нет данных доски — «Нужен агент v15 и обновление из Jira» и «Обновить из Jira»", async () => { hub({ data: { board: null, sprints: [], backlog: null, board_issues: null } }); open("/app/jira/backlog?project=AWPCA"); diff --git a/web/src/modules/jira/agile/IssueRow.tsx b/web/src/modules/jira/agile/IssueRow.tsx index a8e6f7a..9c38c4f 100644 --- a/web/src/modules/jira/agile/IssueRow.tsx +++ b/web/src/modules/jira/agile/IssueRow.tsx @@ -9,7 +9,8 @@ export function AgileIssueRow({ issue, mark }: { issue: AgileIssue; mark?: strin const open = useOpenIssue(); const click = (e: MouseEvent) => { if ((e.target as HTMLElement).closest("a")) return; - if (window.getSelection()?.toString()) return; + const sel = window.getSelection(); + if (sel && !sel.isCollapsed && sel.containsNode(e.currentTarget, true)) return; // выделен текст этой строки open(issue.key); }; return ( diff --git a/web/src/modules/jira/agile/agile.css b/web/src/modules/jira/agile/agile.css index ffff67e..0f189f9 100644 --- a/web/src/modules/jira/agile/agile.css +++ b/web/src/modules/jira/agile/agile.css @@ -1,8 +1,7 @@ /* экраны доски (бэклог, спринт, Kanban): только токены дизайн-системы */ .agile{display:flex;flex-direction:column;gap:var(--space-4);padding-top:var(--space-6)} .agile .ui-page-header{margin-bottom:0} -.agile__gate{padding-top:var(--space-6)} -.agile .agile__gate{padding-top:0} +.agile__gate{min-width:0} .agile__link{color:var(--color-action-primary);font-weight:600;text-decoration:none} .agile__link:hover{text-decoration:underline} .agile__empty{margin:0;color:var(--color-text-secondary)} @@ -25,7 +24,7 @@ .agile-row__sp{flex:0 0 32px;text-align:right;color:var(--color-text-secondary)} .agile-row__mark{flex:0 0 auto;font-size:var(--fs-caption);color:var(--color-status-warning)} .agile-type{display:inline-flex;flex:0 0 auto;color:var(--color-text-secondary)} -.agile-type--epic{color:var(--color-cat-1)} +.agile-type--epic{color:var(--k-ui)} .agile-type--bug{color:var(--color-status-error)} .agile-type--task{color:var(--color-status-success)} .agile-past{display:flex;flex-direction:column;gap:var(--space-2)} diff --git a/web/src/modules/jira/api/types.ts b/web/src/modules/jira/api/types.ts index ec5951a..f093aea 100644 --- a/web/src/modules/jira/api/types.ts +++ b/web/src/modules/jira/api/types.ts @@ -71,7 +71,6 @@ export interface IssueDetail extends IssueRow { changes?: ChangeEvent[]; // хаб до журнала изменений (#70) поля не шлёт } - // ответ GET /agile (доска проекта, спринты, бэклог); поля — как отдаёт хаб export interface AgileIssue { key: string; summary: string | null; issue_type: string | null; status: string | null; priority: string | null; diff --git a/web/src/styles/tokens.test.ts b/web/src/styles/tokens.test.ts index 50a45d4..afdae9b 100644 --- a/web/src/styles/tokens.test.ts +++ b/web/src/styles/tokens.test.ts @@ -64,6 +64,25 @@ describe("токены AURA", () => { }); }); +describe("использованные токены определены", () => { + const defs = (css: string) => new Set([...stripComments(css).matchAll(/(--[\w-]+)\s*:/g)].map((m) => m[1])); + // переменные, которые выставляет код страницы (setProperty), а не css + const SET_BY_JS = new Set(["--head-h"]); + const tokenDefs = defs(readFileSync(TOKENS, "utf8")); + it("каждый var(--…) в css определён в tokens.css или в том же файле", () => { + const missing: string[] = []; + for (const f of cssFiles(SRC)) { + if (f === TOKENS) continue; + const css = readFileSync(f, "utf8"); + const local = defs(css); + for (const m of stripComments(css).matchAll(/var\(\s*(--[\w-]+)/g)) { + if (!tokenDefs.has(m[1]) && !local.has(m[1]) && !SET_BY_JS.has(m[1])) missing.push(`${relative(SRC, f)}: ${m[1]}`); + } + } + expect([...new Set(missing)]).toEqual([]); + }); +}); + describe("доска без чередования дорожек (#69, design 9)", () => { it("токена --lane-alt нет, правила чётной дорожки нет", () => { expect(readFileSync(TOKENS, "utf8")).not.toContain("--lane-alt");