feat(sdlc-rename): бренд SDLC Hub и ключи хранилища sdlc-hub-* с переносом
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
528c579aa8
commit
ef9ec9f46d
@ -89,7 +89,7 @@ for (const theme of ["light", "dark"] as const) {
|
|||||||
|
|
||||||
test("board-collapsed", async ({ page }) => {
|
test("board-collapsed", async ({ page }) => {
|
||||||
test.skip(w < 640, "свёртка — только на широком экране");
|
test.skip(w < 640, "свёртка — только на широком экране");
|
||||||
await page.addInitScript(() => { localStorage.setItem("jira-hub-sidebar", "collapsed"); });
|
await page.addInitScript(() => { localStorage.setItem("sdlc-hub-sidebar", "collapsed"); });
|
||||||
await login(page);
|
await login(page);
|
||||||
await expect(page.locator(".board .card[data-key]").first()).toBeVisible();
|
await expect(page.locator(".board .card[data-key]").first()).toBeVisible();
|
||||||
await settle(page);
|
await settle(page);
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Хаб Jira</title>
|
<title>SDLC Hub</title>
|
||||||
<link rel="icon" href="data:,">
|
<link rel="icon" href="data:,">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "jira-hub-web",
|
"name": "sdlc-hub-web",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jira-hub-web",
|
"name": "sdlc-hub-web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/ibm-plex-mono": "5.3.0",
|
"@fontsource/ibm-plex-mono": "5.3.0",
|
||||||
"@fontsource/mulish": "5.3.0",
|
"@fontsource/mulish": "5.3.0",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "jira-hub-web",
|
"name": "sdlc-hub-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
31
web/src/api/legacyStorage.test.ts
Normal file
31
web/src/api/legacyStorage.test.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { readMigrated } from "./legacyStorage";
|
||||||
|
|
||||||
|
describe("readMigrated", () => {
|
||||||
|
beforeEach(() => localStorage.clear());
|
||||||
|
afterEach(() => vi.restoreAllMocks());
|
||||||
|
|
||||||
|
it("переносит старый ключ jira-hub-* в sdlc-hub-*", () => {
|
||||||
|
localStorage.setItem("jira-hub-token", "abc");
|
||||||
|
expect(readMigrated("sdlc-hub-token")).toBe("abc");
|
||||||
|
expect(localStorage.getItem("sdlc-hub-token")).toBe("abc");
|
||||||
|
expect(localStorage.getItem("jira-hub-token")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("при наличии обоих возвращает новое и не трогает старое", () => {
|
||||||
|
localStorage.setItem("jira-hub-token", "old");
|
||||||
|
localStorage.setItem("sdlc-hub-token", "new");
|
||||||
|
expect(readMigrated("sdlc-hub-token")).toBe("new");
|
||||||
|
expect(localStorage.getItem("jira-hub-token")).toBe("old");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("без обоих ключей возвращает null и ничего не пишет", () => {
|
||||||
|
expect(readMigrated("sdlc-hub-token")).toBeNull();
|
||||||
|
expect(localStorage.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("исключение хранилища даёт null", () => {
|
||||||
|
vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { throw new Error("closed"); });
|
||||||
|
expect(readMigrated("sdlc-hub-token")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
18
web/src/api/legacyStorage.ts
Normal file
18
web/src/api/legacyStorage.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// разовый перенос ключей localStorage после переименования jira-hub -> sdlc-hub: читает новый ключ,
|
||||||
|
// а если его нет — старый "jira-hub-*", переписывая значение под новым именем. Ошибки хранилища глотаем.
|
||||||
|
const NEW = "sdlc-hub-";
|
||||||
|
const OLD = "jira-hub-";
|
||||||
|
|
||||||
|
export function readMigrated(key: string): string | null {
|
||||||
|
try {
|
||||||
|
const cur = localStorage.getItem(key);
|
||||||
|
if (cur !== null) return cur;
|
||||||
|
if (!key.startsWith(NEW)) return null;
|
||||||
|
const oldKey = OLD + key.slice(NEW.length);
|
||||||
|
const v = localStorage.getItem(oldKey);
|
||||||
|
if (v === null) return null;
|
||||||
|
localStorage.setItem(key, v);
|
||||||
|
localStorage.removeItem(oldKey);
|
||||||
|
return v;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@ describe("вход", () => {
|
|||||||
renderWithProviders(app);
|
renderWithProviders(app);
|
||||||
expect(screen.getByRole("heading", { name: "Вход в хаб" })).toBeInTheDocument();
|
expect(screen.getByRole("heading", { name: "Вход в хаб" })).toBeInTheDocument();
|
||||||
expect(screen.queryByText("страница приложения")).not.toBeInTheDocument();
|
expect(screen.queryByText("страница приложения")).not.toBeInTheDocument();
|
||||||
expect(screen.getByText(/uv run jira-hub tokens/)).toBeInTheDocument();
|
expect(screen.getByText(/uv run sdlc-hub tokens/)).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("Клиентский токен хаба")).toHaveAttribute("type", "password");
|
expect(screen.getByLabelText("Клиентский токен хаба")).toHaveAttribute("type", "password");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ describe("вход", () => {
|
|||||||
await userEvent.type(screen.getByLabelText("Клиентский токен хаба"), "good-token");
|
await userEvent.type(screen.getByLabelText("Клиентский токен хаба"), "good-token");
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Войти" }));
|
await userEvent.click(screen.getByRole("button", { name: "Войти" }));
|
||||||
expect(await screen.findByText("страница приложения")).toBeInTheDocument();
|
expect(await screen.findByText("страница приложения")).toBeInTheDocument();
|
||||||
expect(localStorage.getItem("jira-hub-token")).toBe("good-token");
|
expect(localStorage.getItem("sdlc-hub-token")).toBe("good-token");
|
||||||
expect(fetch.mock.calls[0][0]).toBe("/settings");
|
expect(fetch.mock.calls[0][0]).toBe("/settings");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ describe("вход", () => {
|
|||||||
await userEvent.click(screen.getByRole("button", { name: "Войти" }));
|
await userEvent.click(screen.getByRole("button", { name: "Войти" }));
|
||||||
expect(await screen.findByRole("alert")).toHaveTextContent(/Токен не подошёл/);
|
expect(await screen.findByRole("alert")).toHaveTextContent(/Токен не подошёл/);
|
||||||
expect(input).toHaveValue("wrong");
|
expect(input).toHaveValue("wrong");
|
||||||
expect(localStorage.getItem("jira-hub-token")).toBeNull();
|
expect(localStorage.getItem("sdlc-hub-token")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("кириллица или пробел — «недопустимые символы», запроса нет", async () => {
|
it("кириллица или пробел — «недопустимые символы», запроса нет", async () => {
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import { setToken, TOKEN_RE } from "./token.ts";
|
|||||||
import { useAuth } from "./useAuth.ts";
|
import { useAuth } from "./useAuth.ts";
|
||||||
import "./login.css";
|
import "./login.css";
|
||||||
|
|
||||||
const BAD_TOKEN = "Токен не подошёл. Проверьте его по uv run jira-hub tokens: токен могли пересоздать.";
|
const BAD_TOKEN = "Токен не подошёл. Проверьте его по uv run sdlc-hub tokens: токен могли пересоздать.";
|
||||||
const BAD_CHARS = "В токене недопустимые символы: скопируйте строку после «клиентский токен:» целиком, без пробелов и кириллицы.";
|
const BAD_CHARS = "В токене недопустимые символы: скопируйте строку после «клиентский токен:» целиком, без пробелов и кириллицы.";
|
||||||
const HUB_DOWN = "Хаб не отвечает: проверьте, что jira-hub запущен, и попробуйте снова.";
|
const HUB_DOWN = "Хаб не отвечает: проверьте, что sdlc-hub запущен, и попробуйте снова.";
|
||||||
|
|
||||||
export function LoginScreen() {
|
export function LoginScreen() {
|
||||||
const { reason } = useAuth();
|
const { reason } = useAuth();
|
||||||
@ -40,8 +40,8 @@ export function LoginScreen() {
|
|||||||
<div className="login">
|
<div className="login">
|
||||||
<Panel padding="lg" className="login__panel">
|
<Panel padding="lg" className="login__panel">
|
||||||
<h2 className="login__title">Вход в хаб</h2>
|
<h2 className="login__title">Вход в хаб</h2>
|
||||||
<p className="login__text">Приложению нужен клиентский токен хаба. Его показывает команда <code>uv run jira-hub tokens</code> в каталоге
|
<p className="login__text">Приложению нужен клиентский токен хаба. Его показывает команда <code>uv run sdlc-hub tokens</code> в каталоге
|
||||||
jira-hub на этом Mac. Токен сохранится в этом браузере.</p>
|
sdlc-hub на этом Mac. Токен сохранится в этом браузере.</p>
|
||||||
{error && <Notice tone="error">{error}</Notice>}
|
{error && <Notice tone="error">{error}</Notice>}
|
||||||
<form className="login__form" onSubmit={submit}>
|
<form className="login__form" onSubmit={submit}>
|
||||||
<Field label="Клиентский токен хаба">
|
<Field label="Клиентский токен хаба">
|
||||||
|
|||||||
@ -5,7 +5,7 @@ describe("token", () => {
|
|||||||
afterEach(() => { vi.restoreAllMocks(); });
|
afterEach(() => { vi.restoreAllMocks(); });
|
||||||
|
|
||||||
it("читает токен, сохранённый прежней доской", async () => {
|
it("читает токен, сохранённый прежней доской", async () => {
|
||||||
localStorage.setItem("jira-hub-token", "abc");
|
localStorage.setItem("sdlc-hub-token", "abc");
|
||||||
const t = await import("./token.ts");
|
const t = await import("./token.ts");
|
||||||
expect(t.getToken()).toBe("abc");
|
expect(t.getToken()).toBe("abc");
|
||||||
});
|
});
|
||||||
@ -15,10 +15,10 @@ describe("token", () => {
|
|||||||
const fn = vi.fn();
|
const fn = vi.fn();
|
||||||
t.subscribe(fn);
|
t.subscribe(fn);
|
||||||
t.setToken("xyz");
|
t.setToken("xyz");
|
||||||
expect(localStorage.getItem("jira-hub-token")).toBe("xyz");
|
expect(localStorage.getItem("sdlc-hub-token")).toBe("xyz");
|
||||||
t.clearToken("bad");
|
t.clearToken("bad");
|
||||||
expect(t.getAuth()).toEqual({ token: "", reason: "bad" });
|
expect(t.getAuth()).toEqual({ token: "", reason: "bad" });
|
||||||
expect(localStorage.getItem("jira-hub-token")).toBeNull();
|
expect(localStorage.getItem("sdlc-hub-token")).toBeNull();
|
||||||
expect(fn).toHaveBeenCalledTimes(2);
|
expect(fn).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
// клиентский токен хаба: localStorage["jira-hub-token"] (тот же ключ, что у прежней доски) и копия в памяти на случай
|
// клиентский токен хаба: localStorage["sdlc-hub-token"] (со старого "sdlc-hub-token" переносится при чтении) и копия в памяти на случай
|
||||||
// закрытого хранилища. reason — почему показан экран входа: "bad" — токен не подошёл (ответ 401)
|
// закрытого хранилища. reason — почему показан экран входа: "bad" — токен не подошёл (ответ 401)
|
||||||
export const TOKEN_KEY = "jira-hub-token";
|
import { readMigrated } from "../api/legacyStorage";
|
||||||
|
|
||||||
|
export const TOKEN_KEY = "sdlc-hub-token";
|
||||||
export const TOKEN_RE = /^[\x21-\x7e]+$/; // печатный ASCII без пробелов: иначе fetch бросит TypeError на заголовке
|
export const TOKEN_RE = /^[\x21-\x7e]+$/; // печатный ASCII без пробелов: иначе fetch бросит TypeError на заголовке
|
||||||
|
|
||||||
export type LoginReason = "none" | "bad";
|
export type LoginReason = "none" | "bad";
|
||||||
export interface AuthSnapshot { token: string; reason: LoginReason }
|
export interface AuthSnapshot { token: string; reason: LoginReason }
|
||||||
|
|
||||||
const readStored = (): string => {
|
const readStored = (): string => {
|
||||||
try { return localStorage.getItem(TOKEN_KEY) ?? ""; } catch { return ""; }
|
return readMigrated(TOKEN_KEY) ?? "";
|
||||||
};
|
};
|
||||||
|
|
||||||
let state: AuthSnapshot = { token: readStored(), reason: "none" };
|
let state: AuthSnapshot = { token: readStored(), reason: "none" };
|
||||||
|
|||||||
@ -147,7 +147,7 @@ describe("страница доски", () => {
|
|||||||
await userEvent.click(toggle);
|
await userEvent.click(toggle);
|
||||||
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
||||||
expect(legend()).toBeNull();
|
expect(legend()).toBeNull();
|
||||||
expect(localStorage.getItem("jira-hub-legend")).toBe("0");
|
expect(localStorage.getItem("sdlc-hub-legend")).toBe("0");
|
||||||
r.unmount();
|
r.unmount();
|
||||||
page();
|
page();
|
||||||
await screen.findByText(/задач из 9/);
|
await screen.findByText(/задач из 9/);
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export function BoardPage({ project }: { project: string }) {
|
|||||||
return (
|
return (
|
||||||
<div data-testid="page-board"><Notice tone="error">
|
<div data-testid="page-board"><Notice tone="error">
|
||||||
В настройках хаба нет {d.missing.join(" и ")} — доска не может перевести виды работ и типы связей Jira. Задайте
|
В настройках хаба нет {d.missing.join(" и ")} — доска не может перевести виды работ и типы связей Jira. Задайте
|
||||||
словари в config.example.json и выполните <code>uv run jira-hub init --config …</code> или запишите их через PUT /settings.
|
словари в config.example.json и выполните <code>uv run sdlc-hub init --config …</code> или запишите их через PUT /settings.
|
||||||
</Notice></div>
|
</Notice></div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
// свёрнутость легенды доски: "0" — свёрнута, иначе развёрнута; ошибки хранилища глотаем
|
// свёрнутость легенды доски: "0" — свёрнута, иначе развёрнута; ошибки хранилища глотаем
|
||||||
const KEY = "jira-hub-legend";
|
import { readMigrated } from "../../../api/legacyStorage";
|
||||||
|
|
||||||
|
const KEY = "sdlc-hub-legend";
|
||||||
|
|
||||||
export function readLegendOpen(): boolean {
|
export function readLegendOpen(): boolean {
|
||||||
try { return localStorage.getItem(KEY) !== "0"; } catch { return true; }
|
return readMigrated(KEY) !== "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveLegendOpen(open: boolean): void {
|
export function saveLegendOpen(open: boolean): void {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export function statusText(s: BoardStatus): { text: string; warn: boolean } {
|
|||||||
case "loading": return { text: "загрузка данных хаба…", warn: false };
|
case "loading": return { text: "загрузка данных хаба…", warn: false };
|
||||||
case "hub-down": return {
|
case "hub-down": return {
|
||||||
text: s.fromFile ? "хаб не отвечает — на доске снимок из файла"
|
text: s.fromFile ? "хаб не отвечает — на доске снимок из файла"
|
||||||
: s.hasData ? "хаб не отвечает — показаны прежние данные" : "хаб не отвечает: запустите хаб (jira-hub serve) и нажмите «Перечитать»",
|
: s.hasData ? "хаб не отвечает — показаны прежние данные" : "хаб не отвечает: запустите хаб (sdlc-hub serve) и нажмите «Перечитать»",
|
||||||
warn: true,
|
warn: true,
|
||||||
};
|
};
|
||||||
case "file-error": return { text: `файл не разобран: ${s.reason}`, warn: true };
|
case "file-error": return { text: `файл не разобран: ${s.reason}`, warn: true };
|
||||||
|
|||||||
@ -83,7 +83,7 @@ export function useBoardSource(dicts: Dicts | null, project: string): BoardSourc
|
|||||||
const pending = board.isPending;
|
const pending = board.isPending;
|
||||||
const emptyHub = useMemo(() => emptyModel(pending
|
const emptyHub = useMemo(() => emptyModel(pending
|
||||||
? "Загрузка данных хаба…"
|
? "Загрузка данных хаба…"
|
||||||
: "Нет данных: хаб не отвечает. Запустите хаб (jira-hub serve) и нажмите «Перечитать» или загрузите файл CSV из фильтра Jira."), [pending]);
|
: "Нет данных: хаб не отвечает. Запустите хаб (sdlc-hub serve) и нажмите «Перечитать» или загрузите файл CSV из фильтра Jira."), [pending]);
|
||||||
const model = source.kind === "file" ? source.model : hubModel ?? emptyHub;
|
const model = source.kind === "file" ? source.model : hubModel ?? emptyHub;
|
||||||
const label = source.kind === "file" ? `${source.model.fileName} (из файла)`
|
const label = source.kind === "file" ? `${source.model.fileName} (из файла)`
|
||||||
: board.data ? (ls ? `Хаб · данные на ${fmtStamp(ls.finished_at)}` : "Хаб · полного обновления не было")
|
: board.data ? (ls ? `Хаб · данные на ${fmtStamp(ls.finished_at)}` : "Хаб · полного обновления не было")
|
||||||
|
|||||||
@ -207,7 +207,7 @@ describe("карточка задачи", () => {
|
|||||||
expect(items[0].querySelector(".body br")).not.toBeNull();
|
expect(items[0].querySelector(".body br")).not.toBeNull();
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Скрыть" }));
|
await userEvent.click(screen.getByRole("button", { name: "Скрыть" }));
|
||||||
expect(screen.queryByText("старый")).toBeNull();
|
expect(screen.queryByText("старый")).toBeNull();
|
||||||
expect(localStorage.getItem("jira-hub-comments-hidden")).toBe("1");
|
expect(localStorage.getItem("sdlc-hub-comments-hidden")).toBe("1");
|
||||||
cleanup();
|
cleanup();
|
||||||
open("/issues/AURORA-9001");
|
open("/issues/AURORA-9001");
|
||||||
expect(await screen.findByRole("button", { name: "Показать" })).toBeInTheDocument();
|
expect(await screen.findByRole("button", { name: "Показать" })).toBeInTheDocument();
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const SELECTS = [
|
|||||||
type Name = (typeof SELECTS)[number][0];
|
type Name = (typeof SELECTS)[number][0];
|
||||||
|
|
||||||
// раскрыта ли панель «Фильтры» — на время сессии вкладки; ошибки хранилища глотаем
|
// раскрыта ли панель «Фильтры» — на время сессии вкладки; ошибки хранилища глотаем
|
||||||
const KEY = "jira-hub-issue-filters";
|
const KEY = "sdlc-hub-issue-filters";
|
||||||
const readOpen = () => { try { return sessionStorage.getItem(KEY) === "1"; } catch { return false; } };
|
const readOpen = () => { try { return sessionStorage.getItem(KEY) === "1"; } catch { return false; } };
|
||||||
const saveOpen = (open: boolean) => { try { sessionStorage.setItem(KEY, open ? "1" : "0"); } catch { /* только в памяти */ } };
|
const saveOpen = (open: boolean) => { try { sessionStorage.setItem(KEY, open ? "1" : "0"); } catch { /* только в памяти */ } };
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ describe("карточка окном", () => {
|
|||||||
beforeEach(() => { setToken("tok"); hub(); });
|
beforeEach(() => { setToken("tok"); hub(); });
|
||||||
|
|
||||||
it("из списка: окно поверх списка, фон не пересоздаётся, ✕ возвращает со старым фильтром, «назад» не открывает карточку", async () => {
|
it("из списка: окно поверх списка, фон не пересоздаётся, ✕ возвращает со старым фильтром, «назад» не открывает карточку", async () => {
|
||||||
sessionStorage.setItem("jira-hub-issue-filters", "1"); // панель «Фильтры» раскрыта
|
sessionStorage.setItem("sdlc-hub-issue-filters", "1"); // панель «Фильтры» раскрыта
|
||||||
const router = at(["/app/board", "/app/issues?sprint=AWPCA_Спринт 2"]);
|
const router = at(["/app/board", "/app/issues?sprint=AWPCA_Спринт 2"]);
|
||||||
render(<App router={router} />);
|
render(<App router={router} />);
|
||||||
const table = await screen.findByRole("table");
|
const table = await screen.findByRole("table");
|
||||||
|
|||||||
@ -27,7 +27,7 @@ const issuesCalls = (fetch: ReturnType<typeof hub>) =>
|
|||||||
|
|
||||||
describe("список задач", () => {
|
describe("список задач", () => {
|
||||||
beforeEach(() => { setToken("tok"); sessionStorage.clear(); });
|
beforeEach(() => { setToken("tok"); sessionStorage.clear(); });
|
||||||
const expanded = () => sessionStorage.setItem("jira-hub-issue-filters", "1");
|
const expanded = () => sessionStorage.setItem("sdlc-hub-issue-filters", "1");
|
||||||
|
|
||||||
it("фильтры из адреса уходят в GET /issues; ключ ведёт на карточку", async () => {
|
it("фильтры из адреса уходят в GET /issues; ключ ведёт на карточку", async () => {
|
||||||
const fetch = hub(() => [row(1)]);
|
const fetch = hub(() => [row(1)]);
|
||||||
@ -266,11 +266,11 @@ describe("список задач", () => {
|
|||||||
await userEvent.click(screen.getByRole("button", { name: /Фильтры/ }));
|
await userEvent.click(screen.getByRole("button", { name: /Фильтры/ }));
|
||||||
expect(screen.getByRole("button", { name: /Фильтры/ })).toHaveAttribute("aria-expanded", "true");
|
expect(screen.getByRole("button", { name: /Фильтры/ })).toHaveAttribute("aria-expanded", "true");
|
||||||
for (const n of ["Спринт", "Исполнитель", "Вид работы", "Статус Jira", "Метка", "Эпик"]) expect(screen.getByLabelText(n)).toBeInTheDocument();
|
for (const n of ["Спринт", "Исполнитель", "Вид работы", "Статус Jira", "Метка", "Эпик"]) expect(screen.getByLabelText(n)).toBeInTheDocument();
|
||||||
expect(sessionStorage.getItem("jira-hub-issue-filters")).toBe("1");
|
expect(sessionStorage.getItem("sdlc-hub-issue-filters")).toBe("1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("панель раскрыта по sessionStorage", async () => {
|
it("панель раскрыта по sessionStorage", async () => {
|
||||||
sessionStorage.setItem("jira-hub-issue-filters", "1");
|
sessionStorage.setItem("sdlc-hub-issue-filters", "1");
|
||||||
hub(() => [row(1)]);
|
hub(() => [row(1)]);
|
||||||
renderWithProviders(<IssuesPage project="AWPCA" />, { route: "/issues", path: "/issues" });
|
renderWithProviders(<IssuesPage project="AWPCA" />, { route: "/issues", path: "/issues" });
|
||||||
expect(await screen.findByLabelText("Спринт")).toBeInTheDocument();
|
expect(await screen.findByLabelText("Спринт")).toBeInTheDocument();
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { readMigrated } from "../../../api/legacyStorage";
|
||||||
|
|
||||||
// выбор «комментарии скрыты» — один на все карточки, в браузере (design 19); без localStorage — показаны
|
// выбор «комментарии скрыты» — один на все карточки, в браузере (design 19); без localStorage — показаны
|
||||||
export const COMMENTS_HIDDEN_KEY = "jira-hub-comments-hidden";
|
export const COMMENTS_HIDDEN_KEY = "sdlc-hub-comments-hidden";
|
||||||
const read = (): boolean => { try { return localStorage.getItem(COMMENTS_HIDDEN_KEY) === "1"; } catch { return false; } };
|
const read = (): boolean => readMigrated(COMMENTS_HIDDEN_KEY) === "1";
|
||||||
|
|
||||||
export function useCommentsHidden(): [boolean, (hidden: boolean) => void] {
|
export function useCommentsHidden(): [boolean, (hidden: boolean) => void] {
|
||||||
const [hidden, setHidden] = useState(read);
|
const [hidden, setHidden] = useState(read);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// владелец состояния «Обновить из Jira»: один опрос пакета на всё приложение. Правила — прежние (jira-hub#20):
|
// владелец состояния «Обновить из Jira»: один опрос пакета на всё приложение. Правила — прежние (hub#20):
|
||||||
// одна кнопка и один опрос (M-2), подхват незавершённого пакета после любой загрузки снимка (M-3), остановка опроса
|
// одна кнопка и один опрос (M-2), подхват незавершённого пакета после любой загрузки снимка (M-3), остановка опроса
|
||||||
// (M-4, T10.2), только пакеты с первой командой refresh (T10.6)
|
// (M-4, T10.2), только пакеты с первой командой refresh (T10.6)
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|||||||
@ -149,13 +149,13 @@ describe("боковая панель", () => {
|
|||||||
expect(await within(nav).findByRole("link", { name: "Доска связей" })).toHaveAttribute("aria-current", "page");
|
expect(await within(nav).findByRole("link", { name: "Доска связей" })).toHaveAttribute("aria-current", "page");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("«Свернуть панель» сворачивает и запоминает в jira-hub-sidebar; после перезапуска свёрнута, разделы доступны", async () => {
|
it("«Свернуть панель» сворачивает и запоминает в sdlc-hub-sidebar; после перезапуска свёрнута, разделы доступны", async () => {
|
||||||
hub();
|
hub();
|
||||||
open("/app/board?project=AWPCA");
|
open("/app/board?project=AWPCA");
|
||||||
const btn = await within(bar()).findByRole("button", { name: "Свернуть панель" });
|
const btn = await within(bar()).findByRole("button", { name: "Свернуть панель" });
|
||||||
expect(btn).toHaveAttribute("aria-expanded", "true");
|
expect(btn).toHaveAttribute("aria-expanded", "true");
|
||||||
await userEvent.click(btn);
|
await userEvent.click(btn);
|
||||||
expect(localStorage.getItem("jira-hub-sidebar")).toBe("collapsed");
|
expect(localStorage.getItem("sdlc-hub-sidebar")).toBe("collapsed");
|
||||||
expect(bar()).toHaveClass("is-collapsed");
|
expect(bar()).toHaveClass("is-collapsed");
|
||||||
cleanup();
|
cleanup();
|
||||||
open("/app/board?project=AWPCA");
|
open("/app/board?project=AWPCA");
|
||||||
@ -163,12 +163,12 @@ describe("боковая панель", () => {
|
|||||||
expect(within(bar()).getByRole("button", { name: "Развернуть панель" })).toHaveAttribute("aria-expanded", "false");
|
expect(within(bar()).getByRole("button", { name: "Развернуть панель" })).toHaveAttribute("aria-expanded", "false");
|
||||||
expect(within(bar()).getByRole("link", { name: "Задачи" })).toHaveAttribute("title", "Задачи");
|
expect(within(bar()).getByRole("link", { name: "Задачи" })).toHaveAttribute("title", "Задачи");
|
||||||
await userEvent.click(within(bar()).getByRole("button", { name: "Развернуть панель" }));
|
await userEvent.click(within(bar()).getByRole("button", { name: "Развернуть панель" }));
|
||||||
expect(localStorage.getItem("jira-hub-sidebar")).toBeNull();
|
expect(localStorage.getItem("sdlc-hub-sidebar")).toBeNull();
|
||||||
expect(bar()).not.toHaveClass("is-collapsed");
|
expect(bar()).not.toHaveClass("is-collapsed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("свёрнутая: «Обновить из Jira» значком ставит пакет, событие — в role=status и подсказке", async () => {
|
it("свёрнутая: «Обновить из Jira» значком ставит пакет, событие — в role=status и подсказке", async () => {
|
||||||
localStorage.setItem("jira-hub-sidebar", "collapsed");
|
localStorage.setItem("sdlc-hub-sidebar", "collapsed");
|
||||||
const h = hub();
|
const h = hub();
|
||||||
open("/app/issues?project=AWPCA");
|
open("/app/issues?project=AWPCA");
|
||||||
const btn = await within(bar()).findByRole("button", { name: "Обновить из Jira" });
|
const btn = await within(bar()).findByRole("button", { name: "Обновить из Jira" });
|
||||||
@ -180,7 +180,7 @@ describe("боковая панель", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("свёрнутая: метка проекта видна, щелчок по ней разворачивает панель", async () => {
|
it("свёрнутая: метка проекта видна, щелчок по ней разворачивает панель", async () => {
|
||||||
localStorage.setItem("jira-hub-sidebar", "collapsed");
|
localStorage.setItem("sdlc-hub-sidebar", "collapsed");
|
||||||
hub();
|
hub();
|
||||||
open("/app/board?project=AWPCA");
|
open("/app/board?project=AWPCA");
|
||||||
const tag = await within(bar()).findByRole("button", { name: "Проект AWPCA — развернуть панель" });
|
const tag = await within(bar()).findByRole("button", { name: "Проект AWPCA — развернуть панель" });
|
||||||
@ -201,7 +201,7 @@ describe("боковая панель на узком экране", () => {
|
|||||||
open("/app/board?project=AWPCA");
|
open("/app/board?project=AWPCA");
|
||||||
const top = await screen.findByRole("banner", { name: "Строка приложения" });
|
const top = await screen.findByRole("banner", { name: "Строка приложения" });
|
||||||
expect(within(top).getByRole("button", { name: "Меню" })).toHaveAttribute("aria-expanded", "false");
|
expect(within(top).getByRole("button", { name: "Меню" })).toHaveAttribute("aria-expanded", "false");
|
||||||
expect(within(top).getByText("Хаб Jira")).toBeInTheDocument();
|
expect(within(top).getByText("SDLC Hub")).toBeInTheDocument();
|
||||||
expect(within(top).getByRole("button", { name: "Обновить из Jira" })).toBeInTheDocument();
|
expect(within(top).getByRole("button", { name: "Обновить из Jira" })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("complementary", { name: "Панель приложения" })).toBeNull();
|
expect(screen.queryByRole("complementary", { name: "Панель приложения" })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -40,7 +40,7 @@ function NarrowShell(p: Props) {
|
|||||||
<header className="topbar" aria-label="Строка приложения">
|
<header className="topbar" aria-label="Строка приложения">
|
||||||
<button ref={menuBtn} type="button" className="ui-icon-btn" aria-label="Меню" title="Меню" aria-expanded={open}
|
<button ref={menuBtn} type="button" className="ui-icon-btn" aria-label="Меню" title="Меню" aria-expanded={open}
|
||||||
onClick={() => setOpen(true)}><IconMenu /></button>
|
onClick={() => setOpen(true)}><IconMenu /></button>
|
||||||
<span className="sidebar__brand">Хаб Jira</span>
|
<span className="sidebar__brand">SDLC Hub</span>
|
||||||
<span className="sidebar__spacer" />
|
<span className="sidebar__spacer" />
|
||||||
{MODULES.map((m) => m.SidebarStatus && <m.SidebarStatus key={m.name} project={p.project} compact />)}
|
{MODULES.map((m) => m.SidebarStatus && <m.SidebarStatus key={m.name} project={p.project} compact />)}
|
||||||
</header>
|
</header>
|
||||||
@ -71,7 +71,7 @@ function Panel({ section, project, drawer }: Props & { drawer?: { close(): void;
|
|||||||
return (
|
return (
|
||||||
<aside className={"sidebar" + (collapsed ? " is-collapsed" : "") + (drawer ? " is-drawer" : "")} aria-label="Панель приложения">
|
<aside className={"sidebar" + (collapsed ? " is-collapsed" : "") + (drawer ? " is-drawer" : "")} aria-label="Панель приложения">
|
||||||
<div className="sidebar__head">
|
<div className="sidebar__head">
|
||||||
{!collapsed && <span className="sidebar__brand">Хаб Jira</span>}
|
{!collapsed && <span className="sidebar__brand">SDLC Hub</span>}
|
||||||
{drawer
|
{drawer
|
||||||
? <IconButton label="Закрыть меню" icon={<IconClose />} onClick={drawer.close} />
|
? <IconButton label="Закрыть меню" icon={<IconClose />} onClick={drawer.close} />
|
||||||
: <IconButton label={collapsed ? "Развернуть панель" : "Свернуть панель"} icon={<IconSidebar />}
|
: <IconButton label={collapsed ? "Развернуть панель" : "Свернуть панель"} icon={<IconSidebar />}
|
||||||
|
|||||||
13
web/src/shell/sidebarPref.test.ts
Normal file
13
web/src/shell/sidebarPref.test.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { readSidebar } from "./sidebarPref";
|
||||||
|
|
||||||
|
describe("sidebarPref", () => {
|
||||||
|
beforeEach(() => localStorage.clear());
|
||||||
|
|
||||||
|
it("подхватывает старый ключ jira-hub-sidebar и переносит его", () => {
|
||||||
|
localStorage.setItem("jira-hub-sidebar", "collapsed");
|
||||||
|
expect(readSidebar()).toBe(true);
|
||||||
|
expect(localStorage.getItem("sdlc-hub-sidebar")).toBe("collapsed");
|
||||||
|
expect(localStorage.getItem("jira-hub-sidebar")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,8 +1,10 @@
|
|||||||
// свёртка боковой панели: "collapsed" — свёрнута, без записи — развёрнута; ошибки хранилища глотаем
|
// свёртка боковой панели: "collapsed" — свёрнута, без записи — развёрнута; ошибки хранилища глотаем
|
||||||
const KEY = "jira-hub-sidebar";
|
import { readMigrated } from "../api/legacyStorage";
|
||||||
|
|
||||||
|
const KEY = "sdlc-hub-sidebar";
|
||||||
|
|
||||||
export function readSidebar(): boolean {
|
export function readSidebar(): boolean {
|
||||||
try { return localStorage.getItem(KEY) === "collapsed"; } catch { return false; }
|
return readMigrated(KEY) === "collapsed";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveSidebar(collapsed: boolean): void {
|
export function saveSidebar(collapsed: boolean): void {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user