fix(bots): restore event route drag sorting

This commit is contained in:
RockChinQ
2026-08-26 00:08:09 +08:00
parent d28b385a9f
commit aaeb9ad178
5 changed files with 96 additions and 5 deletions
@@ -1264,6 +1264,7 @@ function BindingCardContent({
onUpdate,
onRemove,
dragHandleProps,
isOverlay = false,
}: BindingCardProps) {
const { t } = useTranslation();
const isEnabled = binding.enabled ?? true;
@@ -1275,13 +1276,21 @@ function BindingCardContent({
const statusDetail = routeStatusDetail(routeStatus, t);
return (
<div className="rounded-lg border bg-card">
<div
className={`rounded-lg border bg-card ${
isOverlay ? 'pointer-events-none shadow-lg ring-1 ring-primary/20' : ''
}`}
data-drag-overlay={isOverlay ? 'true' : undefined}
>
{/* main row */}
<div className="flex flex-wrap items-center gap-2 p-2.5">
{isEnabled && (
<button
type="button"
className="cursor-grab active:cursor-grabbing shrink-0 text-muted-foreground hover:text-foreground touch-none"
aria-label={t('bots.dragEventRoute', {
index: globalIndex + 1,
})}
{...dragHandleProps}
>
<GripVertical className="h-4 w-4" />
@@ -1435,15 +1444,32 @@ function BindingCardContent({
// ── sortable wrapper ──────────────────────────────────────────────────────────
function SortableBindingCard(props: BindingCardProps) {
const { attributes, listeners, setNodeRef, transform, isDragging } =
useSortable({ id: props.binding.id ?? props.globalIndex });
interface SortableBindingCardProps extends BindingCardProps {
sortableId: string;
}
function SortableBindingCard({
sortableId,
...props
}: SortableBindingCardProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: sortableId });
return (
<div
ref={setNodeRef}
data-testid={`event-route-${sortableId}`}
style={{
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.3 : undefined,
position: 'relative',
zIndex: isDragging ? 1 : undefined,
}}
>
<BindingCardContent
@@ -1686,6 +1712,7 @@ export default function EventBindingsEditor({
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={() => setActiveId(null)}
>
<SortableContext
items={idsRef.current}
@@ -1702,6 +1729,7 @@ export default function EventBindingsEditor({
return (
<SortableBindingCard
key={idsRef.current[sortIdx]}
sortableId={idsRef.current[sortIdx]}
binding={binding}
globalIndex={globalIdx}
routeStatus={
@@ -1720,7 +1748,7 @@ export default function EventBindingsEditor({
})}
</div>
</SortableContext>
<DragOverlay dropAnimation={null}>
<DragOverlay adjustScale={false} dropAnimation={null}>
{activeBinding && activeGlobalIdx >= 0 ? (
<BindingCardContent
binding={activeBinding}
+1
View File
@@ -392,6 +392,7 @@ const enUS = {
eventBindings: 'Event Routes',
addEventBinding: 'Add Route',
addBehavior: 'Add behavior',
dragEventRoute: 'Drag route {{index}}',
behaviorReplyMessages: 'Reply to messages',
behaviorReplyMessagesDescription:
'Send incoming messages to an Agent or Pipeline.',
+1
View File
@@ -398,6 +398,7 @@ const jaJP = {
eventBindings: 'イベントルート',
addEventBinding: 'ルートを追加',
addBehavior: '動作を追加',
dragEventRoute: 'ルート {{index}} をドラッグ',
behaviorReplyMessages: '受信メッセージに返信',
behaviorReplyMessagesDescription:
'受信メッセージを Agent または Pipeline で処理します。',
+1
View File
@@ -375,6 +375,7 @@ const zhHans = {
eventBindings: '事件路由',
addEventBinding: '添加路由',
addBehavior: '添加行为',
dragEventRoute: '拖动第 {{index}} 条路由',
behaviorReplyMessages: '回复收到的消息',
behaviorReplyMessagesDescription:
'把收到的消息交给 Agent 或 Pipeline 处理。',
+60
View File
@@ -872,6 +872,66 @@ test.describe('agent and pipeline save concurrency', () => {
});
test.describe('cross-resource flows', () => {
test('reorders bot event routes with a visible drag preview', async ({
page,
}) => {
await installLangBotApiMocks(page, {
authenticated: true,
withAdapterEvents: true,
});
await page.goto('/home/bots?id=new');
await selectPlaywrightAdapter(page);
await page.locator('input[name="name"]').fill('Routing Bot');
await submit(page);
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
await page.getByRole('button', { name: 'Add behavior' }).click();
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();
const routeCards = page.locator('[data-testid^="event-route-"]');
await expect(routeCards).toHaveCount(2);
await expect(routeCards.nth(0)).toContainText('Message received');
await expect(routeCards.nth(1)).toContainText('Member joined group');
const firstHandle = page.getByRole('button', { name: 'Drag route 1' });
const secondCard = routeCards.nth(1);
const handleBox = await firstHandle.boundingBox();
const targetBox = await secondCard.boundingBox();
expect(handleBox).not.toBeNull();
expect(targetBox).not.toBeNull();
await page.mouse.move(
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
);
await page.mouse.down();
await page.mouse.move(
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2 + 10,
{ steps: 4 },
);
await expect(page.locator('[data-drag-overlay="true"]')).toBeVisible();
await page.mouse.move(
targetBox!.x + targetBox!.width / 2,
targetBox!.y + targetBox!.height - 4,
{ steps: 12 },
);
await page.mouse.up();
await expect(page.locator('[data-drag-overlay="true"]')).toHaveCount(0);
await expect(routeCards.nth(0)).toContainText('Member joined group');
await expect(routeCards.nth(1)).toContainText('Message received');
await save(page);
await page.reload();
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');
});
test('creates a pipeline then binds it to a bot', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });