Admin: validate raw emoji input instead of trimming it

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 08:10:39 -04:00
co-authored by Claude Opus 5.5
parent 7f2f903fa9
commit 59ff392433
3 changed files with 11 additions and 11 deletions
@@ -1281,11 +1281,10 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => {
}); });
it("builds and validates emoji keys like the registry", () => { it("builds and validates emoji keys like the registry", () => {
expect(emojiIconKey("🐶")).toBe("emoji:🐶");
expect(emojiIconKey("🐶")).toBe("emoji:🐶"); expect(emojiIconKey("🐶")).toBe("emoji:🐶");
expect(emojiIconKey("👨‍👩‍👧‍👦")).toBe("emoji:👨‍👩‍👧‍👦"); expect(emojiIconKey("👨‍👩‍👧‍👦")).toBe("emoji:👨‍👩‍👧‍👦");
expect(emojiIconKey("⭐".repeat(8))).toBe(`emoji:${"⭐".repeat(8)}`); expect(emojiIconKey("⭐".repeat(8))).toBe(`emoji:${"⭐".repeat(8)}`);
for (const bad of ["", " ", "⭐".repeat(9), "a b", "<b>", "a&b", "🐶\u0007"]) { for (const bad of ["", " ", " 🐶 ", "🐶 ", "\t🐶", "⭐".repeat(9), "a b", "<b>", "a&b", "🐶\u0007"]) {
expect(emojiIconKey(bad), JSON.stringify(bad)).toBeNull(); expect(emojiIconKey(bad), JSON.stringify(bad)).toBeNull();
} }
expect(faIconName("fa:dog")).toBe("dog"); expect(faIconName("fa:dog")).toBe("dog");
@@ -1358,7 +1357,7 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => {
expect(rowGlyph(await screen.findByTestId("definition-99"))).toBe("lucide:dog"); expect(rowGlyph(await screen.findByTestId("definition-99"))).toBe("lucide:dog");
}); });
for (const bad of ["a b", "<b>", "a&b", "⭐".repeat(9)]) { for (const bad of [" 🐶 ", "🐶 ", " ", "a b", "<b>", "a&b", "⭐".repeat(9)]) {
it(`an invalid emoji entry ${JSON.stringify(bad)} is not saved`, async () => { it(`an invalid emoji entry ${JSON.stringify(bad)} is not saved`, async () => {
await renderReady([{ ...BRUSH, id: 50, icon: "lucide:dog" }]); await renderReady([{ ...BRUSH, id: 50, icon: "lucide:dog" }]);
fireEvent.click(screen.getByTestId("definition-50")); fireEvent.click(screen.getByTestId("definition-50"));
+3 -3
View File
@@ -249,7 +249,7 @@ function buildBody(draft: Draft, keepsAssignees = false): DefinitionCreateBody |
if (!title) return "Give the task a title."; if (!title) return "Give the task a title.";
if (draft.assigneeIds.length === 0 && !keepsAssignees) return "Assign at least one child."; 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.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."; return "Enter a single emoji (no spaces or < > &), or clear the emoji field.";
} }
const rule = buildRule(draft); const rule = buildRule(draft);
@@ -590,10 +590,10 @@ function EditSheet({
<button <button
type="button" type="button"
role="radio" role="radio"
aria-checked={draft.icon === null && !draft.emoji.trim()} aria-checked={draft.icon === null && !draft.emoji}
aria-label="No icon" aria-label="No icon"
className={ className={
draft.icon === null && !draft.emoji.trim() ? "defs-icon defs-icon--on" : "defs-icon" draft.icon === null && !draft.emoji ? "defs-icon defs-icon--on" : "defs-icon"
} }
onClick={() => update({ icon: null, emoji: "" })} onClick={() => update({ icon: null, emoji: "" })}
> >
+5 -4
View File
@@ -32,12 +32,13 @@ export function faIconKey(name: string): string {
} }
/** /**
* The stored key for typed emoji text, e.g. `emoji:🐶`. Edge whitespace is * The stored key for typed emoji text, e.g. `emoji:🐶`. The raw text — not
* trimmed; the rest must pass the registry's emoji check (1-8 code points, * trimmed, as the backend rejects any whitespace — must pass the registry's
* no whitespace, control characters or `<`, `>`, `&`), else null. * emoji check (1-8 code points, no whitespace, control characters or `<`,
* `>`, `&`), else null. Only an empty field means "no icon".
*/ */
export function emojiIconKey(text: string): string | null { export function emojiIconKey(text: string): string | null {
const key = `${EMOJI_PREFIX}${text.trim()}`; const key = `${EMOJI_PREFIX}${text}`;
return resolveIcon(key).kind === "emoji" ? key : null; return resolveIcon(key).kind === "emoji" ? key : null;
} }