mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Use emoji picker across workflow fields
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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)}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
disabled={disabled}
|
||||
id={`${idPrefix}-value`}
|
||||
onChange={(event) =>
|
||||
emitEditor({ ...editor, value: event.target.value })
|
||||
}
|
||||
placeholder={
|
||||
editor.field === "webhook_field"
|
||||
? "Value to match"
|
||||
: valuePlaceholder(editor.field)
|
||||
}
|
||||
value={editor.value}
|
||||
/>
|
||||
{editor.field === "trigger_emoji" ? (
|
||||
<WorkflowEmojiField
|
||||
ariaLabel="Choose condition emoji"
|
||||
clearAriaLabel="Clear condition emoji"
|
||||
disabled={disabled}
|
||||
id={`${idPrefix}-value`}
|
||||
onChange={(emoji) =>
|
||||
emitEditor({ ...editor, value: emoji ?? "" })
|
||||
}
|
||||
value={editor.value}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
disabled={disabled}
|
||||
id={`${idPrefix}-value`}
|
||||
onChange={(event) =>
|
||||
emitEditor({ ...editor, value: event.target.value })
|
||||
}
|
||||
placeholder={
|
||||
editor.field === "webhook_field"
|
||||
? "Value to match"
|
||||
: valuePlaceholder(editor.field)
|
||||
}
|
||||
value={editor.value}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex gap-2">
|
||||
<Popover onOpenChange={setPickerOpen} open={pickerOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
aria-label={ariaLabel}
|
||||
className="flex-1 justify-start px-3 font-normal"
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{value ? (
|
||||
<>
|
||||
<StatusEmoji className="h-5 w-5 text-base" value={value} />
|
||||
<span>{emojiDisplayName(value)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SmilePlus className="text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Choose a reaction</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-auto overflow-hidden rounded-2xl border-0 bg-transparent p-0 shadow-none"
|
||||
sideOffset={4}
|
||||
>
|
||||
<EmojiPicker
|
||||
autoFocus
|
||||
onSelect={(emoji) => {
|
||||
onChange(emoji);
|
||||
setPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{value && clearAriaLabel ? (
|
||||
<Button
|
||||
aria-label={clearAriaLabel}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(undefined)}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
<FieldLabel htmlFor="wf-trigger-emoji">
|
||||
Emoji filter (optional)
|
||||
</FieldLabel>
|
||||
<div className="flex gap-2">
|
||||
<Popover onOpenChange={setEmojiPickerOpen} open={emojiPickerOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
aria-label="Choose emoji filter"
|
||||
className="flex-1 justify-start px-3 font-normal"
|
||||
disabled={disabled}
|
||||
id="wf-trigger-emoji"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{trigger.emoji ? (
|
||||
<>
|
||||
<StatusEmoji
|
||||
className="h-5 w-5 text-base"
|
||||
value={trigger.emoji}
|
||||
/>
|
||||
<span>{emojiDisplayName(trigger.emoji)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SmilePlus className="text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
Choose a reaction
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-auto overflow-hidden rounded-2xl border-0 bg-transparent p-0 shadow-none"
|
||||
sideOffset={4}
|
||||
>
|
||||
<EmojiPicker
|
||||
autoFocus
|
||||
onSelect={(emoji) => {
|
||||
onUpdate({ ...trigger, emoji });
|
||||
setEmojiPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{trigger.emoji ? (
|
||||
<Button
|
||||
aria-label="Clear emoji filter"
|
||||
disabled={disabled}
|
||||
onClick={() => onUpdate({ ...trigger, emoji: undefined })}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<WorkflowEmojiField
|
||||
ariaLabel="Choose emoji filter"
|
||||
clearAriaLabel="Clear emoji filter"
|
||||
disabled={disabled}
|
||||
id="wf-trigger-emoji"
|
||||
onChange={(emoji) => onUpdate({ ...trigger, emoji })}
|
||||
value={trigger.emoji}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "webhook":
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel htmlFor={`${prefix}-emoji`}>Emoji</FieldLabel>
|
||||
<Input
|
||||
autoCapitalize="off"
|
||||
<WorkflowEmojiField
|
||||
ariaLabel="Choose reaction emoji"
|
||||
disabled={disabled}
|
||||
id={`${prefix}-emoji`}
|
||||
onChange={(event) =>
|
||||
onUpdate({ ...step, emoji: event.target.value })
|
||||
}
|
||||
placeholder="e.g. thumbsup"
|
||||
onChange={(emoji) => onUpdate({ ...step, emoji })}
|
||||
value={step.emoji ?? ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user