From 59ff392433d1b9f5f7d41c803644cc402181558f Mon Sep 17 00:00:00 2001 From: Joshua Date: Sat, 26 Sep 2026 08:10:39 -0400 Subject: [PATCH] Admin: validate raw emoji input instead of trimming it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend rejects whitespace in emoji keys, so an entry like " ๐Ÿถ " must fail the picker's validation rather than be saved as emoji:๐Ÿถ. Only an exactly empty field clears the icon; whitespace-only text now shows the invalid-emoji error. Co-Authored-By: Claude Opus 5.5 --- admin/src/definitions/DefinitionsTab.test.tsx | 7 +++---- admin/src/definitions/DefinitionsTab.tsx | 6 +++--- admin/src/definitions/definitionIcons.tsx | 9 +++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/admin/src/definitions/DefinitionsTab.test.tsx b/admin/src/definitions/DefinitionsTab.test.tsx index b2b3c51..2f6d818 100644 --- a/admin/src/definitions/DefinitionsTab.test.tsx +++ b/admin/src/definitions/DefinitionsTab.test.tsx @@ -1282,10 +1282,9 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => { it("builds and validates emoji keys like the registry", () => { expect(emojiIconKey("๐Ÿถ")).toBe("emoji:๐Ÿถ"); - expect(emojiIconKey(" ๐Ÿถ ")).toBe("emoji:๐Ÿถ"); expect(emojiIconKey("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ")).toBe("emoji:๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"); expect(emojiIconKey("โญ".repeat(8))).toBe(`emoji:${"โญ".repeat(8)}`); - for (const bad of ["", " ", "โญ".repeat(9), "a b", "", "a&b", "๐Ÿถ\u0007"]) { + for (const bad of ["", " ", " ๐Ÿถ ", "๐Ÿถ ", "\t๐Ÿถ", "โญ".repeat(9), "a b", "", "a&b", "๐Ÿถ\u0007"]) { expect(emojiIconKey(bad), JSON.stringify(bad)).toBeNull(); } expect(faIconName("fa:dog")).toBe("dog"); @@ -1335,7 +1334,7 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => { fireEvent.click(screen.getByRole("button", { name: "New" })); const s = sheet(); fillNewTask(s); - fireEvent.change(emojiInput(s), { target: { value: " ๐Ÿถ " } }); + fireEvent.change(emojiInput(s), { target: { value: "๐Ÿถ" } }); expect(radio(s, "Icon", "No icon").getAttribute("aria-checked")).toBe("false"); await save(s); @@ -1358,7 +1357,7 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => { expect(rowGlyph(await screen.findByTestId("definition-99"))).toBe("lucide:dog"); }); - for (const bad of ["a b", "", "a&b", "โญ".repeat(9)]) { + for (const bad of [" ๐Ÿถ ", "๐Ÿถ ", " ", "a b", "", "a&b", "โญ".repeat(9)]) { it(`an invalid emoji entry ${JSON.stringify(bad)} is not saved`, async () => { await renderReady([{ ...BRUSH, id: 50, icon: "lucide:dog" }]); fireEvent.click(screen.getByTestId("definition-50")); diff --git a/admin/src/definitions/DefinitionsTab.tsx b/admin/src/definitions/DefinitionsTab.tsx index a1e58f9..fcd174a 100644 --- a/admin/src/definitions/DefinitionsTab.tsx +++ b/admin/src/definitions/DefinitionsTab.tsx @@ -249,7 +249,7 @@ function buildBody(draft: Draft, keepsAssignees = false): DefinitionCreateBody | if (!title) return "Give the task a title."; if (draft.assigneeIds.length === 0 && !keepsAssignees) return "Assign at least one child."; if (draft.windows.length === 0) return "Pick at least one window."; - if (draft.emoji.trim() && draft.icon === null) { + if (draft.emoji && draft.icon === null) { return "Enter a single emoji (no spaces or < > &), or clear the emoji field."; } const rule = buildRule(draft); @@ -590,10 +590,10 @@ function EditSheet({