mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-26 20:27:13 +00:00
refactor(bots): group event route creation menu
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -398,6 +398,7 @@ const jaJP = {
|
||||
eventBindings: 'イベントルート',
|
||||
addEventBinding: 'ルートを追加',
|
||||
addBehavior: '動作を追加',
|
||||
commonScenarios: 'よく使うシーン',
|
||||
dragEventRoute: 'ルート {{index}} をドラッグ',
|
||||
behaviorReplyMessages: '受信メッセージに返信',
|
||||
behaviorReplyMessagesDescription:
|
||||
|
||||
@@ -375,6 +375,7 @@ const zhHans = {
|
||||
eventBindings: '事件路由',
|
||||
addEventBinding: '添加路由',
|
||||
addBehavior: '添加行为',
|
||||
commonScenarios: '常用场景',
|
||||
dragEventRoute: '拖动第 {{index}} 条路由',
|
||||
behaviorReplyMessages: '回复收到的消息',
|
||||
behaviorReplyMessagesDescription:
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user