refactor(bots): group event route creation menu

This commit is contained in:
RockChinQ
2026-08-26 00:10:51 +08:00
parent aaeb9ad178
commit 532d1b73d2
5 changed files with 87 additions and 13 deletions
@@ -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({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[300px] max-w-[90vw]">
<DropdownMenuLabel className="px-2 pb-1 pt-1.5 text-xs font-normal text-muted-foreground">
{t('bots.commonScenarios')}
</DropdownMenuLabel>
{behaviorPresets.map((preset) => {
const Icon = preset.icon;
return (
@@ -1799,18 +1821,44 @@ export default function EventBindingsEditor({
);
})}
<DropdownMenuSeparator />
<DropdownMenuItem
className="items-start gap-2 py-2"
onClick={() => addBinding(dryRunEventOptions[0])}
>
<Workflow className="mt-0.5 h-4 w-4 shrink-0" />
<span className="flex min-w-0 flex-col gap-0.5">
<span>{t('bots.behaviorCustom')}</span>
<span className="text-xs text-muted-foreground">
{t('bots.behaviorCustomDescription')}
<DropdownMenuSub>
<DropdownMenuSubTrigger
className="items-start gap-2 py-2"
disabled={otherEventGroups.length === 0}
>
<Workflow className="mt-0.5 h-4 w-4 shrink-0" />
<span className="flex min-w-0 flex-col gap-0.5 pr-2">
<span>{t('bots.behaviorCustom')}</span>
<span className="text-xs font-normal text-muted-foreground">
{t('bots.behaviorCustomDescription')}
</span>
</span>
</span>
</DropdownMenuItem>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="max-h-[min(70vh,32rem)] w-[320px] max-w-[90vw] overflow-y-auto">
{otherEventGroups.map((group, groupIndex) => (
<Fragment key={group.namespace}>
{groupIndex > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel className="px-2 py-1 text-xs font-normal text-muted-foreground">
{eventGroupLabel(group.namespace, t)}
</DropdownMenuLabel>
{group.patterns.map((event) => (
<DropdownMenuItem
key={event}
className="items-start py-2"
onClick={() => addBinding(event)}
>
<span className="flex min-w-0 flex-col gap-0.5">
<span>{eventLabel(event, t)}</span>
<span className="text-xs text-muted-foreground">
{eventDescription(event, t)}
</span>
</span>
</DropdownMenuItem>
))}
</Fragment>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
<RouteDryRunDialog
+1
View File
@@ -392,6 +392,7 @@ const enUS = {
eventBindings: 'Event Routes',
addEventBinding: 'Add Route',
addBehavior: 'Add behavior',
commonScenarios: 'Common scenarios',
dragEventRoute: 'Drag route {{index}}',
behaviorReplyMessages: 'Reply to messages',
behaviorReplyMessagesDescription:
+1
View File
@@ -398,6 +398,7 @@ const jaJP = {
eventBindings: 'イベントルート',
addEventBinding: 'ルートを追加',
addBehavior: '動作を追加',
commonScenarios: 'よく使うシーン',
dragEventRoute: 'ルート {{index}} をドラッグ',
behaviorReplyMessages: '受信メッセージに返信',
behaviorReplyMessagesDescription:
+1
View File
@@ -375,6 +375,7 @@ const zhHans = {
eventBindings: '事件路由',
addEventBinding: '添加路由',
addBehavior: '添加行为',
commonScenarios: '常用场景',
dragEventRoute: '拖动第 {{index}} 条路由',
behaviorReplyMessages: '回复收到的消息',
behaviorReplyMessagesDescription:
+24 -1
View File
@@ -872,7 +872,7 @@ test.describe('agent and pipeline save concurrency', () => {
});
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 }) => {