diff --git a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx
index 1e850adde..f794d3335 100644
--- a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx
+++ b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx
@@ -1,6 +1,13 @@
'use client';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ Fragment,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { UseFormReturn } from 'react-hook-form';
@@ -72,7 +79,11 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuLabel,
DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
@@ -1543,6 +1554,14 @@ export default function EventBindingsEditor({
),
[dryRunEventOptions],
);
+ const otherEventGroups = useMemo(() => {
+ const commonEventTypes = new Set(
+ behaviorPresets.map((preset) => preset.eventType),
+ );
+ return groupEventPatterns(
+ eventOptions.filter((event) => !commonEventTypes.has(event)),
+ );
+ }, [behaviorPresets, eventOptions]);
const refreshRouteStatuses = useCallback(async () => {
if (!botId) {
@@ -1780,6 +1799,9 @@ export default function EventBindingsEditor({
+
+ {t('bots.commonScenarios')}
+
{behaviorPresets.map((preset) => {
const Icon = preset.icon;
return (
@@ -1799,18 +1821,44 @@ export default function EventBindingsEditor({
);
})}
- addBinding(dryRunEventOptions[0])}
- >
-
-
- {t('bots.behaviorCustom')}
-
- {t('bots.behaviorCustomDescription')}
+
+
+
+
+ {t('bots.behaviorCustom')}
+
+ {t('bots.behaviorCustomDescription')}
+
-
-
+
+
+ {otherEventGroups.map((group, groupIndex) => (
+
+ {groupIndex > 0 && }
+
+ {eventGroupLabel(group.namespace, t)}
+
+ {group.patterns.map((event) => (
+ addBinding(event)}
+ >
+
+ {eventLabel(event, t)}
+
+ {eventDescription(event, t)}
+
+
+
+ ))}
+
+ ))}
+
+
{
});
test.describe('cross-resource flows', () => {
- test('reorders bot event routes with a visible drag preview', async ({
+ test('adds custom bot events and reorders routes with a drag preview', async ({
page,
}) => {
await installLangBotApiMocks(page, {
@@ -887,6 +887,9 @@ test.describe('cross-resource flows', () => {
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
await page.getByRole('button', { name: 'Add behavior' }).click();
+ await expect(
+ page.getByText('Common scenarios', { exact: true }),
+ ).toBeVisible();
await page.getByRole('menuitem', { name: /^Reply to messages/ }).click();
await page.getByRole('button', { name: 'Add behavior' }).click();
await page.getByRole('menuitem', { name: /^Welcome new members/ }).click();
@@ -896,6 +899,25 @@ test.describe('cross-resource flows', () => {
await expect(routeCards.nth(0)).toContainText('Message received');
await expect(routeCards.nth(1)).toContainText('Member joined group');
+ await page.getByRole('button', { name: 'Add behavior' }).click();
+ await page
+ .getByRole('menuitem', { name: /^Configure another event/ })
+ .hover();
+ const eventSubmenu = page.locator(
+ '[data-slot="dropdown-menu-sub-content"]',
+ );
+ await expect(
+ eventSubmenu.getByText('Messages', { exact: true }),
+ ).toBeVisible();
+ await expect(
+ eventSubmenu.getByRole('menuitem', { name: /^Message edited/ }),
+ ).toBeVisible();
+ await eventSubmenu
+ .getByRole('menuitem', { name: /^Message edited/ })
+ .click();
+ await expect(routeCards).toHaveCount(3);
+ await expect(routeCards.nth(2)).toContainText('Message edited');
+
const firstHandle = page.getByRole('button', { name: 'Drag route 1' });
const secondCard = routeCards.nth(1);
const handleBox = await firstHandle.boundingBox();
@@ -930,6 +952,7 @@ test.describe('cross-resource flows', () => {
const savedRouteCards = page.locator('[data-testid^="event-route-"]');
await expect(savedRouteCards.nth(0)).toContainText('Member joined group');
await expect(savedRouteCards.nth(1)).toContainText('Message received');
+ await expect(savedRouteCards.nth(2)).toContainText('Message edited');
});
test('creates a pipeline then binds it to a bot', async ({ page }) => {