9148c9819b36
give release articles and notes fields their own id prefixes so no id on a repo's one-page console workspace can collide, completing one tab per repo (v3.0.199)
- time
- 2026-10-03 04:38 UTC · 1 week ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- lightning quick kepr
- parent
- 10e65d3e89e7
↓ kepr-9148c981.tar.gzatlas at this save
files changed4 files
package.json+1 −1src/admin/repos.ts+6 −5tests/console.test.ts+12 −16tests/repoworkspace.test.ts+20 −3package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.198",3+ "version": "3.0.199",44 "private": true,55 "type": "module",66 "scripts": {src/admin/repos.ts+6 −5
@@ -117,7 +117,7 @@117117 const override = notes === release.recordNotes ? null : notes;118118 setRelease(ctx.kepr, repo.name, release.version, fields.hidden === "1", override);119119 return c.redirect(120- `/admin/repos?repo=${repo.name}&saved=1#${repo.name}-${release.version}`,120+ `/admin/repos?repo=${repo.name}&saved=1#release-${repo.name}-${release.version}`,121121 303,122122 );123123 });@@ -189,7 +189,7 @@189189 throw error;190190 }191191 return c.redirect(192- `/admin/repos?repo=${repo.name}&saved=1#${repo.name}-${release.version}`,192+ `/admin/repos?repo=${repo.name}&saved=1#release-${repo.name}-${release.version}`,193193 303,194194 );195195 });@@ -356,7 +356,8 @@356356 357357 function releaseForm(c: Context, repo: string, release: OperatorRelease) {358358 const total = release.downloads.reduce((sum, [, n]) => sum + n, 0);359- const id = `${repo}-${release.version}`;359+ // Its own prefix: a version may be named like a settings field, which uses `<repo>-<name>`.360+ const id = `release-${repo}-${release.version}`;360361 return html`<article class="issue-post" id="${id}">361362 <p class="issue-post-head mono">362363 ${/* this row stays one line: splitting its spans changes the HTML that tests/console.test.ts pins */ ""}${html`<span class="issue-author">${release.version}</span>`}${html`<span>${release.hidden ? "hidden" : "shown"}</span>`}${html`<span>${plural(total, "download")}</span>`}${release.downloads.map(@@ -365,9 +366,9 @@365366 </p>366367 <form class="issues-form" method="post" action="/admin/releases/${repo}/${release.version}">367368 ${formField(c)}368-<label class="issues-label" for="${id}-notes">notes369+<label class="issues-label" for="notes-${repo}-${release.version}">notes369370 ${release.notes !== release.recordNotes ? html` <span class="muted">(edited)</span>` : ""}</label>370-<textarea class="issues-textarea" id="${id}-notes" name="notes" rows="3"371+<textarea class="issues-textarea" id="notes-${repo}-${release.version}" name="notes" rows="3"371372 maxlength="${MAX_CHARS}">${release.notes}</textarea>372373 ${373374 // One line on purpose: Biome folds a hole-by-hole split back together, and breaking thetests/console.test.ts+12 −16
@@ -136,9 +136,7 @@136136 expect(await text("/")).not.toContain("tools");137137 expect(await stat()).toEqual(["1", "1", "0", "0"]);138138 // Settings survive the flip.139- expect(await (await browser.get("/admin/repos")).text()).toContain(140- 'value="tools"',141- );139+ expect(await (await browser.get("/admin/repos")).text()).toContain('value="tools"');142140 });143141 144142 test("sync now reads the bucket at once and records the time", async () => {@@ -288,23 +286,21 @@288286 289287 test("every version is listed with its download counts and notes", async () => {290288 const page = await (await operator.get("/admin/repos?repo=app")).text();291- expect(page).toContain('id="app-1.1.0"');292- expect(page).toContain('id="app-1.0.0"');293- expect(page.indexOf('id="app-1.1.0"')).toBeLessThan(page.indexOf('id="app-1.0.0"'));289+ expect(page).toContain('id="release-app-1.1.0"');290+ expect(page).toContain('id="release-app-1.0.0"');291+ expect(page.indexOf('id="release-app-1.1.0"')).toBeLessThan(292+ page.indexOf('id="release-app-1.0.0"'),293+ );294294 expect(page).toContain('<span>1 download</span><span class="muted">linux-x86_64 1</span>');295295 expect(page).toContain(">notes for 1.1.0</textarea>");296296 });297297 298298 test("edited notes replace the record's everywhere; the record's own text clears the edit", async () => {299299 const saved = await edit("1.1.0", { notes: "Better <notes>." });300- expect(saved.headers.get("location")).toBe(301- "/admin/repos?repo=app&saved=1#app-1.1.0",302- );300+ expect(saved.headers.get("location")).toBe("/admin/repos?repo=app&saved=1#release-app-1.1.0");303301 expect(JSON.parse(await text("/api/app/releases/latest")).notes).toBe("Better <notes>.");304302 expect(await text("/app/releases")).toContain("Better <notes>.");305- expect(await (await operator.get("/admin/repos?repo=app")).text()).toContain(306- "(edited)",307- );303+ expect(await (await operator.get("/admin/repos?repo=app")).text()).toContain("(edited)");308304 expect((await edit("1.1.0", { notes: "notes for 1.1.0" })).status).toBe(303);309305 const row = inst.kepr.query("SELECT notes FROM release_settings WHERE version = '1.1.0'").get();310306 expect(row).toEqual({ notes: null });@@ -319,7 +315,7 @@319315 expect(await text("/app/releases")).not.toContain("1.1.0");320316 expect((await inst.app.request("/app/releases/1.1.0/linux-x86_64")).status).toBe(404);321317 const page = await (await operator.get("/admin/repos?repo=app")).text();322- expect(page).toContain('id="app-1.1.0"');318+ expect(page).toContain('id="release-app-1.1.0"');323319 expect(page).toContain('value="1" checked> hidden');324320 expect((await edit("1.1.0", { notes: "notes for 1.1.0" })).status).toBe(303);325321 expect(await text("/app/releases/latest/version.txt")).toBe("1.1.0");@@ -371,8 +367,8 @@371367 expect(inst.kepr.query("SELECT * FROM downloads WHERE version = '1.1.0'").all()).toEqual([]);372368 await operator.post("/admin/sync", {});373369 const page = await (await operator.get("/admin/repos?repo=app")).text();374- expect(page).not.toContain('id="app-1.1.0"');375- expect(page).toContain('id="app-1.0.0"');370+ expect(page).not.toContain('id="release-app-1.1.0"');371+ expect(page).toContain('id="release-app-1.0.0"');376372 });377373 378374 test("the last build goes, and latest with it", async () => {@@ -418,7 +414,7 @@418414 test("the file leaves the record, the bucket, the counts and the public routes", async () => {419415 const response = await removeFile("macos-aarch64");420416 expect(response.headers.get("location")).toBe(421- "/admin/repos?repo=app&saved=1#app-1.0.0",417+ "/admin/repos?repo=app&saved=1#release-app-1.0.0",422418 );423419 const files = (await record()).files.map((file: { platform: string }) => file.platform);424420 expect(files).toEqual(["linux-x86_64"]);tests/repoworkspace.test.ts+20 −3
@@ -83,9 +83,7 @@8383 });8484 8585 test("edits the description and details; blank hands them back to the README", async () => {86- expect(await workspace()).toMatch(87- /<textarea[^>]*name="description"[^>]*><\/textarea>/,88- );86+ expect(await workspace()).toMatch(/<textarea[^>]*name="description"[^>]*><\/textarea>/);8987 const saved = await save({ description: "Own *words*", details: "## Usage\r\nRun it." });9088 expect(saved.status).toBe(303);9189 const page = await workspace();@@ -184,4 +182,23 @@184182 expect(columns.map((column) => column.name)).not.toContain("install1");185183 expect(columns.map((column) => column.name)).not.toContain("sort_order");186184 expect(db.query("SELECT name, public FROM repos").all()).toEqual([{ name: "app", public: 1 }]);185+});186+187+test("no two elements of a repo's workspace share an id, whatever its versions are called", async () => {188+ const repos = await makeRepos(["app"]);189+ const head = JSON.parse(repos.koh("app", "view", "--json")).head_id as string;190+ for (const version of ["category", "rename", "description", "1.0.0", "1.0.0-notes"]) {191+ await writeRelease(repos.bucketDir, "app", {192+ version,193+ save: head,194+ files: [{ platform: "linux-x86_64", name: "app", bytes: new Uint8Array([1]) }],195+ });196+ }197+ const inst = await instance(repos.bucketDir);198+ const browser = new Browser(inst.app);199+ await browser.signIn();200+ const page = await (await browser.get("/admin/repos?repo=app")).text();201+ const ids = [...page.matchAll(/ id="([^"]+)"/g)].map((match) => match[1]);202+ expect(ids.length).toBeGreaterThan(10);203+ expect(ids.filter((id, i) => ids.indexOf(id) !== i)).toEqual([]);187204 });