From 3cf827b844a6460eccaa9effb77ff9f631a44181 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Wed, 12 Aug 2026 20:44:53 -0700 Subject: [PATCH] Use emoji picker across workflow fields Signed-off-by: Taylor Ho --- .../workflows/ui/WorkflowConditionBuilder.tsx | 46 +++++++---- .../workflows/ui/WorkflowEmojiField.tsx | 82 +++++++++++++++++++ .../workflows/ui/WorkflowFormBuilder.tsx | 81 +++--------------- .../workflows/ui/WorkflowStepCard.tsx | 10 +-- desktop/tests/e2e/workflows.spec.ts | 60 ++++++++++++++ 5 files changed, 185 insertions(+), 94 deletions(-) create mode 100644 desktop/src/features/workflows/ui/WorkflowEmojiField.tsx diff --git a/desktop/src/features/workflows/ui/WorkflowConditionBuilder.tsx b/desktop/src/features/workflows/ui/WorkflowConditionBuilder.tsx index 26c681497..ffd1c8619 100644 --- a/desktop/src/features/workflows/ui/WorkflowConditionBuilder.tsx +++ b/desktop/src/features/workflows/ui/WorkflowConditionBuilder.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { cn } from "@/shared/lib/cn"; import { Input } from "@/shared/ui/input"; +import { WorkflowEmojiField } from "./WorkflowEmojiField"; import { FieldLabel, FormSelect } from "./workflowFormPrimitives"; import { buildConditionExpression, @@ -68,8 +69,6 @@ function valuePlaceholder(field: string): string { return "Paste a channel UUID"; case "trigger_message_id": return "Paste a message event ID"; - case "trigger_emoji": - return "e.g. 👍"; case "trigger_timestamp": return "e.g. 1723507200"; default: @@ -265,21 +264,34 @@ export function WorkflowConditionBuilder({ ? "Value" : valueLabel(editor.field)} - - emitEditor({ ...editor, value: event.target.value }) - } - placeholder={ - editor.field === "webhook_field" - ? "Value to match" - : valuePlaceholder(editor.field) - } - value={editor.value} - /> + {editor.field === "trigger_emoji" ? ( + + emitEditor({ ...editor, value: emoji ?? "" }) + } + value={editor.value} + /> + ) : ( + + emitEditor({ ...editor, value: event.target.value }) + } + placeholder={ + editor.field === "webhook_field" + ? "Value to match" + : valuePlaceholder(editor.field) + } + value={editor.value} + /> + )} ) : null} diff --git a/desktop/src/features/workflows/ui/WorkflowEmojiField.tsx b/desktop/src/features/workflows/ui/WorkflowEmojiField.tsx new file mode 100644 index 000000000..fcf698bac --- /dev/null +++ b/desktop/src/features/workflows/ui/WorkflowEmojiField.tsx @@ -0,0 +1,82 @@ +import { SmilePlus, X } from "lucide-react"; +import * as React from "react"; + +import { EmojiPicker } from "@/features/custom-emoji/ui/EmojiPicker"; +import { StatusEmoji } from "@/features/user-status/ui/StatusEmoji"; +import { emojiDisplayName } from "@/shared/lib/emojiName"; +import { Button } from "@/shared/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; + +type WorkflowEmojiFieldProps = { + ariaLabel: string; + clearAriaLabel?: string; + disabled?: boolean; + id: string; + onChange: (emoji: string | undefined) => void; + value?: string; +}; + +export function WorkflowEmojiField({ + ariaLabel, + clearAriaLabel, + disabled, + id, + onChange, + value, +}: WorkflowEmojiFieldProps) { + const [pickerOpen, setPickerOpen] = React.useState(false); + + return ( +
+ + + + + + { + onChange(emoji); + setPickerOpen(false); + }} + /> + + + {value && clearAriaLabel ? ( + + ) : null} +
+ ); +} diff --git a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx index 7f677696d..b7c6c77fe 100644 --- a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx +++ b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx @@ -1,21 +1,10 @@ -import { - Check, - ChevronDown, - Plus, - SmilePlus, - Trash2, - X, - Zap, -} from "lucide-react"; +import { Check, ChevronDown, Plus, Trash2, X, Zap } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import * as React from "react"; import { createPortal } from "react-dom"; -import { EmojiPicker } from "@/features/custom-emoji/ui/EmojiPicker"; -import { StatusEmoji } from "@/features/user-status/ui/StatusEmoji"; import { Button } from "@/shared/ui/button"; import { cn } from "@/shared/lib/cn"; -import { emojiDisplayName } from "@/shared/lib/emojiName"; import { DropdownMenu, DropdownMenuContent, @@ -23,10 +12,10 @@ import { DropdownMenuTrigger, } from "@/shared/ui/dropdown-menu"; import { Input } from "@/shared/ui/input"; -import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { Switch } from "@/shared/ui/switch"; import { Textarea } from "@/shared/ui/textarea"; import { WorkflowConditionBuilder } from "./WorkflowConditionBuilder"; +import { WorkflowEmojiField } from "./WorkflowEmojiField"; import { WorkflowScheduleFields } from "./WorkflowScheduleFields"; import { WorkflowStepCard } from "./WorkflowStepCard"; import { FieldLabel } from "./workflowFormPrimitives"; @@ -57,8 +46,6 @@ function TriggerConfigFields({ trigger: TriggerConfig; onUpdate: (trigger: TriggerConfig) => void; }) { - const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false); - switch (trigger.on) { case "message_posted": case "diff_posted": @@ -80,62 +67,14 @@ function TriggerConfigFields({ Emoji filter (optional) -
- - - - - - { - onUpdate({ ...trigger, emoji }); - setEmojiPickerOpen(false); - }} - /> - - - {trigger.emoji ? ( - - ) : null} -
+ onUpdate({ ...trigger, emoji })} + value={trigger.emoji} + /> ); case "webhook": diff --git a/desktop/src/features/workflows/ui/WorkflowStepCard.tsx b/desktop/src/features/workflows/ui/WorkflowStepCard.tsx index f4e18f8c0..3b65647d4 100644 --- a/desktop/src/features/workflows/ui/WorkflowStepCard.tsx +++ b/desktop/src/features/workflows/ui/WorkflowStepCard.tsx @@ -5,6 +5,7 @@ import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; import { Textarea } from "@/shared/ui/textarea"; import { WorkflowConditionBuilder } from "./WorkflowConditionBuilder"; +import { WorkflowEmojiField } from "./WorkflowEmojiField"; import { FieldLabel, FormSelect } from "./workflowFormPrimitives"; import { WorkflowWebhookHeadersEditor } from "./WorkflowWebhookHeadersEditor"; import type { StepFormState, TriggerType } from "./workflowFormTypes"; @@ -255,14 +256,11 @@ function StepConfigFields({ return (
Emoji - - onUpdate({ ...step, emoji: event.target.value }) - } - placeholder="e.g. thumbsup" + onChange={(emoji) => onUpdate({ ...step, emoji })} value={step.emoji ?? ""} />
diff --git a/desktop/tests/e2e/workflows.spec.ts b/desktop/tests/e2e/workflows.spec.ts index b531d5b26..4f8ce6906 100644 --- a/desktop/tests/e2e/workflows.spec.ts +++ b/desktop/tests/e2e/workflows.spec.ts @@ -292,6 +292,66 @@ test("chooses and clears a reaction trigger with the app emoji picker", async ({ ).toContainText("Choose a reaction"); }); +test("chooses an add-reaction step emoji with the app emoji picker", async ({ + page, +}) => { + await navigateToWorkflows(page); + + await page.getByRole("button", { name: "Create Workflow" }).click(); + const dialog = page.getByRole("dialog"); + await dialog.getByRole("button", { name: "Add step" }).click(); + await page.getByRole("menuitem", { name: "Add Reaction" }).click(); + + const inspector = dialog.getByTestId("workflow-node-inspector"); + const stepEmoji = inspector.getByRole("button", { + name: "Choose reaction emoji", + }); + await expect(stepEmoji).toContainText("Choose a reaction"); + await stepEmoji.click(); + + const picker = page.locator("em-emoji-picker"); + await picker.locator("input[type='search']").fill("buzz"); + await picker.getByRole("button", { name: ":buzz:" }).first().click(); + + await expect(stepEmoji).toContainText(":buzz:"); + await dialog.getByRole("tab", { name: "YAML" }).click(); + await expect(dialog.getByLabel("Workflow YAML")).toHaveValue( + /action: add_reaction[\s\S]*emoji: ":buzz:"/, + ); +}); + +test("chooses a reaction emoji condition with the app emoji picker", async ({ + page, +}) => { + await navigateToWorkflows(page); + + await page.getByRole("button", { name: "Create Workflow" }).click(); + const dialog = page.getByRole("dialog"); + await dialog.getByRole("button", { name: /^Trigger:/ }).click(); + let inspector = dialog.getByTestId("workflow-node-inspector"); + await inspector.getByLabel("Trigger event").click(); + await page.getByRole("menuitem", { name: "Reaction Added" }).click(); + + await dialog.getByRole("button", { name: "Add step" }).click(); + await page.getByRole("menuitem", { name: "Delay" }).click(); + inspector = dialog.getByTestId("workflow-node-inspector"); + await inspector.getByRole("button", { name: "Reaction emoji" }).click(); + + const conditionEmoji = inspector.getByRole("button", { + name: "Choose condition emoji", + }); + await conditionEmoji.click(); + const picker = page.locator("em-emoji-picker"); + await picker.locator("input[type='search']").fill("buzz"); + await picker.getByRole("button", { name: ":buzz:" }).first().click(); + + await expect(conditionEmoji).toContainText(":buzz:"); + await dialog.getByRole("tab", { name: "YAML" }).click(); + await expect(dialog.getByLabel("Workflow YAML")).toHaveValue( + /trigger_emoji[\s\S]*:buzz:/, + ); +}); + test("switches an empty workflow between form and YAML modes", async ({ page, }) => {