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.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 expect(page.locator(".board .card[data-key]").first()).toBeVisible();
|
||||
await settle(page);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Хаб Jira</title>
|
||||
<title>SDLC Hub</title>
|
||||
<link rel="icon" href="data:,">
|
||||
</head>
|
||||
<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,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jira-hub-web",
|
||||
"name": "sdlc-hub-web",
|
||||
"dependencies": {
|
||||
"@fontsource/ibm-plex-mono": "5.3.0",
|
||||
"@fontsource/mulish": "5.3.0",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "jira-hub-web",
|
||||
"name": "sdlc-hub-web",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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);
|
||||
expect(screen.getByRole("heading", { name: "Вход в хаб" })).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");
|
||||
});
|
||||
|
||||
@ -28,7 +28,7 @@ describe("вход", () => {
|
||||
await userEvent.type(screen.getByLabelText("Клиентский токен хаба"), "good-token");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Войти" }));
|
||||
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");
|
||||
});
|
||||
|
||||
@ -40,7 +40,7 @@ describe("вход", () => {
|
||||
await userEvent.click(screen.getByRole("button", { name: "Войти" }));
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent(/Токен не подошёл/);
|
||||
expect(input).toHaveValue("wrong");
|
||||
expect(localStorage.getItem("jira-hub-token")).toBeNull();
|
||||
expect(localStorage.getItem("sdlc-hub-token")).toBeNull();
|
||||
});
|
||||
|
||||
it("кириллица или пробел — «недопустимые символы», запроса нет", async () => {
|
||||
|
||||
@ -6,9 +6,9 @@ import { setToken, TOKEN_RE } from "./token.ts";
|
||||
import { useAuth } from "./useAuth.ts";
|
||||
import "./login.css";
|
||||
|
||||
const BAD_TOKEN = "Токен не подошёл. Проверьте его по uv run jira-hub tokens: токен могли пересоздать.";
|
||||
const BAD_TOKEN = "Токен не подошёл. Проверьте его по uv run sdlc-hub tokens: токен могли пересоздать.";
|
||||
const BAD_CHARS = "В токене недопустимые символы: скопируйте строку после «клиентский токен:» целиком, без пробелов и кириллицы.";
|
||||
const HUB_DOWN = "Хаб не отвечает: проверьте, что jira-hub запущен, и попробуйте снова.";
|
||||
const HUB_DOWN = "Хаб не отвечает: проверьте, что sdlc-hub запущен, и попробуйте снова.";
|
||||
|
||||
export function LoginScreen() {
|
||||
const { reason } = useAuth();
|
||||
@ -40,8 +40,8 @@ export function LoginScreen() {
|
||||
<div className="login">
|
||||
<Panel padding="lg" className="login__panel">
|
||||
<h2 className="login__title">Вход в хаб</h2>
|
||||
<p className="login__text">Приложению нужен клиентский токен хаба. Его показывает команда <code>uv run jira-hub tokens</code> в каталоге
|
||||
jira-hub на этом Mac. Токен сохранится в этом браузере.</p>
|
||||
<p className="login__text">Приложению нужен клиентский токен хаба. Его показывает команда <code>uv run sdlc-hub tokens</code> в каталоге
|
||||
sdlc-hub на этом Mac. Токен сохранится в этом браузере.</p>
|
||||
{error && <Notice tone="error">{error}</Notice>}
|
||||
<form className="login__form" onSubmit={submit}>
|
||||
<Field label="Клиентский токен хаба">
|
||||
|
||||
@ -5,7 +5,7 @@ describe("token", () => {
|
||||
afterEach(() => { vi.restoreAllMocks(); });
|
||||
|
||||
it("читает токен, сохранённый прежней доской", async () => {
|
||||
localStorage.setItem("jira-hub-token", "abc");
|
||||
localStorage.setItem("sdlc-hub-token", "abc");
|
||||
const t = await import("./token.ts");
|
||||
expect(t.getToken()).toBe("abc");
|
||||
});
|
||||
@ -15,10 +15,10 @@ describe("token", () => {
|
||||
const fn = vi.fn();
|
||||
t.subscribe(fn);
|
||||
t.setToken("xyz");
|
||||
expect(localStorage.getItem("jira-hub-token")).toBe("xyz");
|
||||
expect(localStorage.getItem("sdlc-hub-token")).toBe("xyz");
|
||||
t.clearToken("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);
|
||||
});
|
||||
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
// клиентский токен хаба: localStorage["jira-hub-token"] (тот же ключ, что у прежней доски) и копия в памяти на случай
|
||||
// клиентский токен хаба: localStorage["sdlc-hub-token"] (со старого "sdlc-hub-token" переносится при чтении) и копия в памяти на случай
|
||||
// закрытого хранилища. 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 type LoginReason = "none" | "bad";
|
||||
export interface AuthSnapshot { token: string; reason: LoginReason }
|
||||
|
||||
const readStored = (): string => {
|
||||
try { return localStorage.getItem(TOKEN_KEY) ?? ""; } catch { return ""; }
|
||||
return readMigrated(TOKEN_KEY) ?? "";
|
||||
};
|
||||
|
||||
let state: AuthSnapshot = { token: readStored(), reason: "none" };
|
||||
|
||||
@ -147,7 +147,7 @@ describe("страница доски", () => {
|
||||
await userEvent.click(toggle);
|
||||
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
||||
expect(legend()).toBeNull();
|
||||
expect(localStorage.getItem("jira-hub-legend")).toBe("0");
|
||||
expect(localStorage.getItem("sdlc-hub-legend")).toBe("0");
|
||||
r.unmount();
|
||||
page();
|
||||
await screen.findByText(/задач из 9/);
|
||||
|
||||
@ -47,7 +47,7 @@ export function BoardPage({ project }: { project: string }) {
|
||||
return (
|
||||
<div data-testid="page-board"><Notice tone="error">
|
||||
В настройках хаба нет {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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
// свёрнутость легенды доски: "0" — свёрнута, иначе развёрнута; ошибки хранилища глотаем
|
||||
const KEY = "jira-hub-legend";
|
||||
import { readMigrated } from "../../../api/legacyStorage";
|
||||
|
||||
const KEY = "sdlc-hub-legend";
|
||||
|
||||
export function readLegendOpen(): boolean {
|
||||
try { return localStorage.getItem(KEY) !== "0"; } catch { return true; }
|
||||
return readMigrated(KEY) !== "0";
|
||||
}
|
||||
|
||||
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 "hub-down": return {
|
||||
text: s.fromFile ? "хаб не отвечает — на доске снимок из файла"
|
||||
: s.hasData ? "хаб не отвечает — показаны прежние данные" : "хаб не отвечает: запустите хаб (jira-hub serve) и нажмите «Перечитать»",
|
||||
: s.hasData ? "хаб не отвечает — показаны прежние данные" : "хаб не отвечает: запустите хаб (sdlc-hub serve) и нажмите «Перечитать»",
|
||||
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 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 label = source.kind === "file" ? `${source.model.fileName} (из файла)`
|
||||
: board.data ? (ls ? `Хаб · данные на ${fmtStamp(ls.finished_at)}` : "Хаб · полного обновления не было")
|
||||
|
||||
@ -207,7 +207,7 @@ describe("карточка задачи", () => {
|
||||
expect(items[0].querySelector(".body br")).not.toBeNull();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Скрыть" }));
|
||||
expect(screen.queryByText("старый")).toBeNull();
|
||||
expect(localStorage.getItem("jira-hub-comments-hidden")).toBe("1");
|
||||
expect(localStorage.getItem("sdlc-hub-comments-hidden")).toBe("1");
|
||||
cleanup();
|
||||
open("/issues/AURORA-9001");
|
||||
expect(await screen.findByRole("button", { name: "Показать" })).toBeInTheDocument();
|
||||
|
||||
@ -10,7 +10,7 @@ export const SELECTS = [
|
||||
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 saveOpen = (open: boolean) => { try { sessionStorage.setItem(KEY, open ? "1" : "0"); } catch { /* только в памяти */ } };
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ describe("карточка окном", () => {
|
||||
beforeEach(() => { setToken("tok"); hub(); });
|
||||
|
||||
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"]);
|
||||
render(<App router={router} />);
|
||||
const table = await screen.findByRole("table");
|
||||
|
||||
@ -27,7 +27,7 @@ const issuesCalls = (fetch: ReturnType<typeof hub>) =>
|
||||
|
||||
describe("список задач", () => {
|
||||
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 () => {
|
||||
const fetch = hub(() => [row(1)]);
|
||||
@ -266,11 +266,11 @@ describe("список задач", () => {
|
||||
await userEvent.click(screen.getByRole("button", { name: /Фильтры/ }));
|
||||
expect(screen.getByRole("button", { name: /Фильтры/ })).toHaveAttribute("aria-expanded", "true");
|
||||
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 () => {
|
||||
sessionStorage.setItem("jira-hub-issue-filters", "1");
|
||||
sessionStorage.setItem("sdlc-hub-issue-filters", "1");
|
||||
hub(() => [row(1)]);
|
||||
renderWithProviders(<IssuesPage project="AWPCA" />, { route: "/issues", path: "/issues" });
|
||||
expect(await screen.findByLabelText("Спринт")).toBeInTheDocument();
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { readMigrated } from "../../../api/legacyStorage";
|
||||
|
||||
// выбор «комментарии скрыты» — один на все карточки, в браузере (design 19); без localStorage — показаны
|
||||
export const COMMENTS_HIDDEN_KEY = "jira-hub-comments-hidden";
|
||||
const read = (): boolean => { try { return localStorage.getItem(COMMENTS_HIDDEN_KEY) === "1"; } catch { return false; } };
|
||||
export const COMMENTS_HIDDEN_KEY = "sdlc-hub-comments-hidden";
|
||||
const read = (): boolean => readMigrated(COMMENTS_HIDDEN_KEY) === "1";
|
||||
|
||||
export function useCommentsHidden(): [boolean, (hidden: boolean) => void] {
|
||||
const [hidden, setHidden] = useState(read);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// владелец состояния «Обновить из Jira»: один опрос пакета на всё приложение. Правила — прежние (jira-hub#20):
|
||||
// владелец состояния «Обновить из Jira»: один опрос пакета на всё приложение. Правила — прежние (hub#20):
|
||||
// одна кнопка и один опрос (M-2), подхват незавершённого пакета после любой загрузки снимка (M-3), остановка опроса
|
||||
// (M-4, T10.2), только пакеты с первой командой refresh (T10.6)
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
@ -149,13 +149,13 @@ describe("боковая панель", () => {
|
||||
expect(await within(nav).findByRole("link", { name: "Доска связей" })).toHaveAttribute("aria-current", "page");
|
||||
});
|
||||
|
||||
it("«Свернуть панель» сворачивает и запоминает в jira-hub-sidebar; после перезапуска свёрнута, разделы доступны", async () => {
|
||||
it("«Свернуть панель» сворачивает и запоминает в sdlc-hub-sidebar; после перезапуска свёрнута, разделы доступны", async () => {
|
||||
hub();
|
||||
open("/app/board?project=AWPCA");
|
||||
const btn = await within(bar()).findByRole("button", { name: "Свернуть панель" });
|
||||
expect(btn).toHaveAttribute("aria-expanded", "true");
|
||||
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");
|
||||
cleanup();
|
||||
open("/app/board?project=AWPCA");
|
||||
@ -163,12 +163,12 @@ describe("боковая панель", () => {
|
||||
expect(within(bar()).getByRole("button", { name: "Развернуть панель" })).toHaveAttribute("aria-expanded", "false");
|
||||
expect(within(bar()).getByRole("link", { name: "Задачи" })).toHaveAttribute("title", "Задачи");
|
||||
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");
|
||||
});
|
||||
|
||||
it("свёрнутая: «Обновить из Jira» значком ставит пакет, событие — в role=status и подсказке", async () => {
|
||||
localStorage.setItem("jira-hub-sidebar", "collapsed");
|
||||
localStorage.setItem("sdlc-hub-sidebar", "collapsed");
|
||||
const h = hub();
|
||||
open("/app/issues?project=AWPCA");
|
||||
const btn = await within(bar()).findByRole("button", { name: "Обновить из Jira" });
|
||||
@ -180,7 +180,7 @@ describe("боковая панель", () => {
|
||||
});
|
||||
|
||||
it("свёрнутая: метка проекта видна, щелчок по ней разворачивает панель", async () => {
|
||||
localStorage.setItem("jira-hub-sidebar", "collapsed");
|
||||
localStorage.setItem("sdlc-hub-sidebar", "collapsed");
|
||||
hub();
|
||||
open("/app/board?project=AWPCA");
|
||||
const tag = await within(bar()).findByRole("button", { name: "Проект AWPCA — развернуть панель" });
|
||||
@ -201,7 +201,7 @@ describe("боковая панель на узком экране", () => {
|
||||
open("/app/board?project=AWPCA");
|
||||
const top = await screen.findByRole("banner", { name: "Строка приложения" });
|
||||
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(screen.queryByRole("complementary", { name: "Панель приложения" })).toBeNull();
|
||||
});
|
||||
|
||||
@ -40,7 +40,7 @@ function NarrowShell(p: Props) {
|
||||
<header className="topbar" aria-label="Строка приложения">
|
||||
<button ref={menuBtn} type="button" className="ui-icon-btn" aria-label="Меню" title="Меню" aria-expanded={open}
|
||||
onClick={() => setOpen(true)}><IconMenu /></button>
|
||||
<span className="sidebar__brand">Хаб Jira</span>
|
||||
<span className="sidebar__brand">SDLC Hub</span>
|
||||
<span className="sidebar__spacer" />
|
||||
{MODULES.map((m) => m.SidebarStatus && <m.SidebarStatus key={m.name} project={p.project} compact />)}
|
||||
</header>
|
||||
@ -71,7 +71,7 @@ function Panel({ section, project, drawer }: Props & { drawer?: { close(): void;
|
||||
return (
|
||||
<aside className={"sidebar" + (collapsed ? " is-collapsed" : "") + (drawer ? " is-drawer" : "")} aria-label="Панель приложения">
|
||||
<div className="sidebar__head">
|
||||
{!collapsed && <span className="sidebar__brand">Хаб Jira</span>}
|
||||
{!collapsed && <span className="sidebar__brand">SDLC Hub</span>}
|
||||
{drawer
|
||||
? <IconButton label="Закрыть меню" icon={<IconClose />} onClick={drawer.close} />
|
||||
: <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" — свёрнута, без записи — развёрнута; ошибки хранилища глотаем
|
||||
const KEY = "jira-hub-sidebar";
|
||||
import { readMigrated } from "../api/legacyStorage";
|
||||
|
||||
const KEY = "sdlc-hub-sidebar";
|
||||
|
||||
export function readSidebar(): boolean {
|
||||
try { return localStorage.getItem(KEY) === "collapsed"; } catch { return false; }
|
||||
return readMigrated(KEY) === "collapsed";
|
||||
}
|
||||
|
||||
export function saveSidebar(collapsed: boolean): void {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user