diff --git a/web/src/modules/jira/agile/agilePref.test.ts b/web/src/modules/jira/agile/agilePref.test.ts index ccf2d4c..f0514ee 100644 --- a/web/src/modules/jira/agile/agilePref.test.ts +++ b/web/src/modules/jira/agile/agilePref.test.ts @@ -37,6 +37,12 @@ describe("agilePref", () => { expect(readView("AWPCA", "backlog")).toEqual(DEFAULT_VIEW); }); + it("испорченный JSON: saveView перезаписывает мусор, readView возвращает сохранённое", () => { + localStorage.setItem(AGILE_KEY, "oops"); + saveView("AWPCA", "backlog", view({ hideDone: true, epic: "AURORA-10" })); + expect(readView("AWPCA", "backlog")).toEqual(view({ hideDone: true, epic: "AURORA-10" })); + }); + it("мусор: «[1]» → DEFAULT_VIEW", () => { localStorage.setItem(AGILE_KEY, "[1]"); expect(readView("AWPCA", "backlog")).toEqual(DEFAULT_VIEW); diff --git a/web/src/modules/jira/agile/agilePref.ts b/web/src/modules/jira/agile/agilePref.ts index b0cd0c8..cb9aa86 100644 --- a/web/src/modules/jira/agile/agilePref.ts +++ b/web/src/modules/jira/agile/agilePref.ts @@ -41,7 +41,12 @@ export function readView(label: string, page: AgilePage): AgileView { export function saveView(label: string, page: AgilePage, v: AgileView): void { try { - const root = readRoot(); + let root: Obj; + try { + root = readRoot(); + } catch { + root = {}; // испорченный JSON перезаписывается, иначе вид не сохранится никогда + } const project = isObj(root[label]) ? root[label] : {}; const view: AgileView = { assignee: v.assignee, epic: v.epic, hideDone: v.hideDone, diff --git a/web/src/modules/jira/agile/filters.test.ts b/web/src/modules/jira/agile/filters.test.ts index 58e4500..c86123f 100644 --- a/web/src/modules/jira/agile/filters.test.ts +++ b/web/src/modules/jira/agile/filters.test.ts @@ -202,7 +202,15 @@ describe("parseSprintIds", () => { ]; it("id из строки, только среди selectableSprints", () => { expect(parseSprintIds("102,999", sprints)).toEqual([102]); - expect(parseSprintIds("101", sprints)).toEqual([]); + expect(parseSprintIds("999,103", sprints)).toEqual([103]); + }); + it("если не осталось ни одного допустимого id — все активные", () => { + expect(parseSprintIds("999", sprints)).toEqual([102]); + expect(parseSprintIds("101", sprints)).toEqual([102]); + }); + it("токены — только десятичные цифры (1e2, 0x10, 1.0 отбрасываются)", () => { + expect(parseSprintIds("1e2,0x66,102.0", sprints)).toEqual([102]); + expect(parseSprintIds("1e2,103", sprints)).toEqual([103]); }); it("порядок — как в selectableSprints, не как в строке", () => { expect(parseSprintIds("103,102", sprints)).toEqual([102, 103]); diff --git a/web/src/modules/jira/agile/filters.ts b/web/src/modules/jira/agile/filters.ts index 74af4a8..5039c48 100644 --- a/web/src/modules/jira/agile/filters.ts +++ b/web/src/modules/jira/agile/filters.ts @@ -77,14 +77,15 @@ export function selectableSprints(sprints: AgileSprint[]): AgileSprint[] { return [...active, ...future.sort((a, b) => startMs(a) - startMs(b))]; } -// id из «101,102» среди selectableSprints, в их порядке; пусто или без годных чисел — все активные +// id из «101,102» среди selectableSprints, в их порядке; недопустимые отбрасываются; если не осталось ни одного — все активные export function parseSprintIds(param: string | null, sprints: AgileSprint[]): number[] { const selectable = selectableSprints(sprints); const ids = new Set( - (param ?? "").split(",").map((x) => x.trim()).filter((x) => x !== "").map(Number).filter((n) => Number.isInteger(n)), + (param ?? "").split(",").map((x) => x.trim()).filter((x) => /^\d+$/.test(x)).map(Number), ); - if (ids.size === 0) return selectable.filter((s) => s.state === "active").map((s) => s.id); - return selectable.filter((s) => ids.has(s.id)).map((s) => s.id); + const picked = selectable.filter((s) => ids.has(s.id)).map((s) => s.id); + if (picked.length > 0) return picked; + return selectable.filter((s) => s.state === "active").map((s) => s.id); } // строки-эпики из всех спринтов и бэклога (по rank, без повторов), затем ключи из epic_key без строки (по ключу);