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
@@ -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", "<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(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", "<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 () => {
await renderReady([{ ...BRUSH, id: 50, icon: "lucide:dog" }]);
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 (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({
<button
type="button"
role="radio"
aria-checked={draft.icon === null && !draft.emoji.trim()}
aria-checked={draft.icon === null && !draft.emoji}
aria-label="No icon"
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: "" })}
>
+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
* trimmed; the rest must pass the registry's emoji check (1-8 code points,
* no whitespace, control characters or `<`, `>`, `&`), else null.
* The stored key for typed emoji text, e.g. `emoji:🐶`. The raw text — not
* trimmed, as the backend rejects any whitespace — must pass the registry's
* 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 {
const key = `${EMOJI_PREFIX}${text.trim()}`;
const key = `${EMOJI_PREFIX}${text}`;
return resolveIcon(key).kind === "emoji" ? key : null;
}