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'; '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 { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
import { UseFormReturn } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form';
@@ -72,7 +79,11 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { import {
@@ -1543,6 +1554,14 @@ export default function EventBindingsEditor({
), ),
[dryRunEventOptions], [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 () => { const refreshRouteStatuses = useCallback(async () => {
if (!botId) { if (!botId) {
@@ -1780,6 +1799,9 @@ export default function EventBindingsEditor({
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[300px] max-w-[90vw]"> <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) => { {behaviorPresets.map((preset) => {
const Icon = preset.icon; const Icon = preset.icon;
return ( return (
@@ -1799,18 +1821,44 @@ export default function EventBindingsEditor({
); );
})} })}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuSub>
className="items-start gap-2 py-2" <DropdownMenuSubTrigger
onClick={() => addBinding(dryRunEventOptions[0])} 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"> <Workflow className="mt-0.5 h-4 w-4 shrink-0" />
<span>{t('bots.behaviorCustom')}</span> <span className="flex min-w-0 flex-col gap-0.5 pr-2">
<span className="text-xs text-muted-foreground"> <span>{t('bots.behaviorCustom')}</span>
{t('bots.behaviorCustomDescription')} <span className="text-xs font-normal text-muted-foreground">
{t('bots.behaviorCustomDescription')}
</span>
</span> </span>
</span> </DropdownMenuSubTrigger>
</DropdownMenuItem> <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> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<RouteDryRunDialog <RouteDryRunDialog
+1
View File
@@ -392,6 +392,7 @@ const enUS = {
eventBindings: 'Event Routes', eventBindings: 'Event Routes',
addEventBinding: 'Add Route', addEventBinding: 'Add Route',
addBehavior: 'Add behavior', addBehavior: 'Add behavior',
commonScenarios: 'Common scenarios',
dragEventRoute: 'Drag route {{index}}', dragEventRoute: 'Drag route {{index}}',
behaviorReplyMessages: 'Reply to messages', behaviorReplyMessages: 'Reply to messages',
behaviorReplyMessagesDescription: behaviorReplyMessagesDescription:
+1
View File
@@ -398,6 +398,7 @@ const jaJP = {
eventBindings: 'イベントルート', eventBindings: 'イベントルート',
addEventBinding: 'ルートを追加', addEventBinding: 'ルートを追加',
addBehavior: '動作を追加', addBehavior: '動作を追加',
commonScenarios: 'よく使うシーン',
dragEventRoute: 'ルート {{index}} をドラッグ', dragEventRoute: 'ルート {{index}} をドラッグ',
behaviorReplyMessages: '受信メッセージに返信', behaviorReplyMessages: '受信メッセージに返信',
behaviorReplyMessagesDescription: behaviorReplyMessagesDescription:
+1
View File
@@ -375,6 +375,7 @@ const zhHans = {
eventBindings: '事件路由', eventBindings: '事件路由',
addEventBinding: '添加路由', addEventBinding: '添加路由',
addBehavior: '添加行为', addBehavior: '添加行为',
commonScenarios: '常用场景',
dragEventRoute: '拖动第 {{index}} 条路由', dragEventRoute: '拖动第 {{index}} 条路由',
behaviorReplyMessages: '回复收到的消息', behaviorReplyMessages: '回复收到的消息',
behaviorReplyMessagesDescription: behaviorReplyMessagesDescription:
+24 -1
View File
@@ -872,7 +872,7 @@ test.describe('agent and pipeline save concurrency', () => {
}); });
test.describe('cross-resource flows', () => { 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, page,
}) => { }) => {
await installLangBotApiMocks(page, { await installLangBotApiMocks(page, {
@@ -887,6 +887,9 @@ test.describe('cross-resource flows', () => {
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/); await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
await page.getByRole('button', { name: 'Add behavior' }).click(); 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('menuitem', { name: /^Reply to messages/ }).click();
await page.getByRole('button', { name: 'Add behavior' }).click(); await page.getByRole('button', { name: 'Add behavior' }).click();
await page.getByRole('menuitem', { name: /^Welcome new members/ }).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(0)).toContainText('Message received');
await expect(routeCards.nth(1)).toContainText('Member joined group'); 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 firstHandle = page.getByRole('button', { name: 'Drag route 1' });
const secondCard = routeCards.nth(1); const secondCard = routeCards.nth(1);
const handleBox = await firstHandle.boundingBox(); const handleBox = await firstHandle.boundingBox();
@@ -930,6 +952,7 @@ test.describe('cross-resource flows', () => {
const savedRouteCards = page.locator('[data-testid^="event-route-"]'); const savedRouteCards = page.locator('[data-testid^="event-route-"]');
await expect(savedRouteCards.nth(0)).toContainText('Member joined group'); await expect(savedRouteCards.nth(0)).toContainText('Member joined group');
await expect(savedRouteCards.nth(1)).toContainText('Message received'); 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 }) => { test('creates a pipeline then binds it to a bot', async ({ page }) => {