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:
@@ -1282,10 +1282,9 @@ 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");
|
||||||
@@ -1335,7 +1334,7 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => {
|
|||||||
fireEvent.click(screen.getByRole("button", { name: "New" }));
|
fireEvent.click(screen.getByRole("button", { name: "New" }));
|
||||||
const s = sheet();
|
const s = sheet();
|
||||||
fillNewTask(s);
|
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");
|
expect(radio(s, "Icon", "No icon").getAttribute("aria-checked")).toBe("false");
|
||||||
|
|
||||||
await save(s);
|
await save(s);
|
||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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: "" })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user