From 0e9f697c1b45cf7f1227725164fcb2b318188b64 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Date: Tue, 7 Jul 2026 20:42:36 -0300 Subject: [PATCH] refactor: centralize wall-link id template in wallLinkKey The '${contestId}-notice' / '${contestId}-exam' id template was duplicated in WallTab and the Seeder. Extract wallLinkKey(contestId, kind) in the Wall module, import it at both call sites, and add three tests covering format stability (regression for the 'second save replaces first save' behaviour the form relies on). --- sample-vault/.obsidian/plugins/leif/main.js | 11 +++++++---- src/domain/entities/Wall.ts | 14 ++++++++++++++ src/infrastructure/persistence/Seeder.ts | 5 +++-- src/ui/view/components/WallTab.ts | 5 +++-- tests/domain/EntityClasses.test.ts | 16 +++++++++++++++- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/sample-vault/.obsidian/plugins/leif/main.js b/sample-vault/.obsidian/plugins/leif/main.js index dd596db..05ee5ce 100644 --- a/sample-vault/.obsidian/plugins/leif/main.js +++ b/sample-vault/.obsidian/plugins/leif/main.js @@ -1086,6 +1086,9 @@ var Wall = class { this.notes = notes; } }; +function wallLinkKey(contestId, kind) { + return `${contestId}-${kind}`; +} // src/domain/entities/Contest.ts var Contest = class { @@ -2244,10 +2247,10 @@ async function seedTceSpDemo(dataStore) { contestId: contest.id, wall: { noticeLinks: [ - { id: `${contest.id}-notice`, label: contestSpec.wall.noticeLabel, url: contestSpec.wall.noticeUrl } + { id: wallLinkKey(contest.id, "notice"), label: contestSpec.wall.noticeLabel, url: contestSpec.wall.noticeUrl } ], examLinks: [ - { id: `${contest.id}-exam`, label: contestSpec.wall.examLabel, url: contestSpec.wall.examUrl } + { id: wallLinkKey(contest.id, "exam"), label: contestSpec.wall.examLabel, url: contestSpec.wall.examUrl } ], subjectSnapshots: seededSubjects.map((subject, index) => ({ subjectId: subject.id, @@ -4280,14 +4283,14 @@ var WallTab = class { try { const noticeLink = noticeUrl.value ? [ { - id: `${activeContest.id}-notice`, + id: wallLinkKey(activeContest.id, "notice"), label: noticeLabel.value || "Edital", url: noticeUrl.value } ] : []; const examLink = examUrl.value ? [ { - id: `${activeContest.id}-exam`, + id: wallLinkKey(activeContest.id, "exam"), label: examLabel.value || "Prova", url: examUrl.value } diff --git a/src/domain/entities/Wall.ts b/src/domain/entities/Wall.ts index e1b7a4f..e56bcdc 100644 --- a/src/domain/entities/Wall.ts +++ b/src/domain/entities/Wall.ts @@ -41,4 +41,18 @@ export class Wall { public readonly subjectSnapshots: WallSubjectSnapshot[] = [], public readonly notes?: string ) {} +} + +/** + * The kinds of single-link slots the wall form manages. + */ +export type WallLinkKind = "notice" | "exam"; + +/** + * Builds the stable identifier used for the wall's single notice/exam link. + * The wall form overwrites the link on every save, so a deterministic id + * keeps the array at length 0 or 1 instead of appending duplicates. + */ +export function wallLinkKey(contestId: string, kind: WallLinkKind): string { + return `${contestId}-${kind}`; } \ No newline at end of file diff --git a/src/infrastructure/persistence/Seeder.ts b/src/infrastructure/persistence/Seeder.ts index 9cc26f0..d0647ca 100644 --- a/src/infrastructure/persistence/Seeder.ts +++ b/src/infrastructure/persistence/Seeder.ts @@ -11,6 +11,7 @@ import type { StudyItem } from "@/domain/entities/StudyItem"; import { StudySessionType } from "@/domain/entities/StudySession"; import type { Subject } from "@/domain/entities/Subject"; import type { Topic } from "@/domain/entities/Topic"; +import { wallLinkKey } from "@/domain/entities/Wall"; import { EntityRepositoryFactory } from "@/infrastructure/persistence/EntityRepositoryFactory"; export interface SeededContest { @@ -469,10 +470,10 @@ export async function seedTceSpDemo(dataStore: PluginDataStore): Promise ({ subjectId: subject.id, diff --git a/src/ui/view/components/WallTab.ts b/src/ui/view/components/WallTab.ts index 963ee1a..e2f3498 100644 --- a/src/ui/view/components/WallTab.ts +++ b/src/ui/view/components/WallTab.ts @@ -1,6 +1,7 @@ import type { PluginDataStore } from "@/application/ports/PluginDataStore"; import { UpdateContestWallUseCase } from "@/application/use-cases/UpdateContestWallUseCase"; import type { LeifPluginData } from "@/domain/types/LeifPluginData"; +import { wallLinkKey } from "@/domain/entities/Wall"; import { DomHelpers } from "@/ui/view/shared/DomHelpers"; import { EntityRepositoryFactory } from "@/infrastructure/persistence/EntityRepositoryFactory"; @@ -71,7 +72,7 @@ export class WallTab { const noticeLink = noticeUrl.value ? [ { - id: `${activeContest.id}-notice`, + id: wallLinkKey(activeContest.id, "notice"), label: noticeLabel.value || "Edital", url: noticeUrl.value } @@ -81,7 +82,7 @@ export class WallTab { const examLink = examUrl.value ? [ { - id: `${activeContest.id}-exam`, + id: wallLinkKey(activeContest.id, "exam"), label: examLabel.value || "Prova", url: examUrl.value } diff --git a/tests/domain/EntityClasses.test.ts b/tests/domain/EntityClasses.test.ts index e0ec21b..bb3382b 100644 --- a/tests/domain/EntityClasses.test.ts +++ b/tests/domain/EntityClasses.test.ts @@ -8,7 +8,7 @@ import { Topic } from "@/domain/entities/Topic"; import { StudySession } from "@/domain/entities/StudySession"; import { QuestionNotebook } from "@/domain/entities/QuestionNotebook"; import { ResourceReference } from "@/domain/entities/ResourceReference"; -import { WallLink, WallSubjectSnapshot, Wall } from "@/domain/entities/Wall"; +import { WallLink, WallSubjectSnapshot, Wall, wallLinkKey } from "@/domain/entities/Wall"; import { ValidationError } from "@/domain/errors/DomainErrors"; describe("Entity Classes", () => { @@ -142,6 +142,20 @@ describe("Entity Classes", () => { }); }); + describe("wallLinkKey", () => { + it("builds a stable notice-link id from the contest id", () => { + expect(wallLinkKey("c-1", "notice")).toBe("c-1-notice"); + }); + + it("builds a stable exam-link id from the contest id", () => { + expect(wallLinkKey("c-1", "exam")).toBe("c-1-exam"); + }); + + it("returns the same id for the same contest + kind", () => { + expect(wallLinkKey("c-1", "notice")).toBe(wallLinkKey("c-1", "notice")); + }); + }); + describe("ContestState", () => { it("creates a valid state", () => { const state = new ContestState("c-1", "s-1", "i-1");