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 }) => {