mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-26 12:17:14 +00:00
fix(bots): restore event route drag sorting
This commit is contained in:
@@ -1264,6 +1264,7 @@ function BindingCardContent({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
onRemove,
|
onRemove,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
|
isOverlay = false,
|
||||||
}: BindingCardProps) {
|
}: BindingCardProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isEnabled = binding.enabled ?? true;
|
const isEnabled = binding.enabled ?? true;
|
||||||
@@ -1275,13 +1276,21 @@ function BindingCardContent({
|
|||||||
const statusDetail = routeStatusDetail(routeStatus, t);
|
const statusDetail = routeStatusDetail(routeStatus, t);
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* main row */}
|
||||||
<div className="flex flex-wrap items-center gap-2 p-2.5">
|
<div className="flex flex-wrap items-center gap-2 p-2.5">
|
||||||
{isEnabled && (
|
{isEnabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="cursor-grab active:cursor-grabbing shrink-0 text-muted-foreground hover:text-foreground touch-none"
|
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}
|
{...dragHandleProps}
|
||||||
>
|
>
|
||||||
<GripVertical className="h-4 w-4" />
|
<GripVertical className="h-4 w-4" />
|
||||||
@@ -1435,15 +1444,32 @@ function BindingCardContent({
|
|||||||
|
|
||||||
// ── sortable wrapper ──────────────────────────────────────────────────────────
|
// ── sortable wrapper ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SortableBindingCard(props: BindingCardProps) {
|
interface SortableBindingCardProps extends BindingCardProps {
|
||||||
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
sortableId: string;
|
||||||
useSortable({ id: props.binding.id ?? props.globalIndex });
|
}
|
||||||
|
|
||||||
|
function SortableBindingCard({
|
||||||
|
sortableId,
|
||||||
|
...props
|
||||||
|
}: SortableBindingCardProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: sortableId });
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
|
data-testid={`event-route-${sortableId}`}
|
||||||
style={{
|
style={{
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
opacity: isDragging ? 0.3 : undefined,
|
opacity: isDragging ? 0.3 : undefined,
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: isDragging ? 1 : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BindingCardContent
|
<BindingCardContent
|
||||||
@@ -1686,6 +1712,7 @@ export default function EventBindingsEditor({
|
|||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragCancel={() => setActiveId(null)}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={idsRef.current}
|
items={idsRef.current}
|
||||||
@@ -1702,6 +1729,7 @@ export default function EventBindingsEditor({
|
|||||||
return (
|
return (
|
||||||
<SortableBindingCard
|
<SortableBindingCard
|
||||||
key={idsRef.current[sortIdx]}
|
key={idsRef.current[sortIdx]}
|
||||||
|
sortableId={idsRef.current[sortIdx]}
|
||||||
binding={binding}
|
binding={binding}
|
||||||
globalIndex={globalIdx}
|
globalIndex={globalIdx}
|
||||||
routeStatus={
|
routeStatus={
|
||||||
@@ -1720,7 +1748,7 @@ export default function EventBindingsEditor({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
<DragOverlay dropAnimation={null}>
|
<DragOverlay adjustScale={false} dropAnimation={null}>
|
||||||
{activeBinding && activeGlobalIdx >= 0 ? (
|
{activeBinding && activeGlobalIdx >= 0 ? (
|
||||||
<BindingCardContent
|
<BindingCardContent
|
||||||
binding={activeBinding}
|
binding={activeBinding}
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ const enUS = {
|
|||||||
eventBindings: 'Event Routes',
|
eventBindings: 'Event Routes',
|
||||||
addEventBinding: 'Add Route',
|
addEventBinding: 'Add Route',
|
||||||
addBehavior: 'Add behavior',
|
addBehavior: 'Add behavior',
|
||||||
|
dragEventRoute: 'Drag route {{index}}',
|
||||||
behaviorReplyMessages: 'Reply to messages',
|
behaviorReplyMessages: 'Reply to messages',
|
||||||
behaviorReplyMessagesDescription:
|
behaviorReplyMessagesDescription:
|
||||||
'Send incoming messages to an Agent or Pipeline.',
|
'Send incoming messages to an Agent or Pipeline.',
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ const jaJP = {
|
|||||||
eventBindings: 'イベントルート',
|
eventBindings: 'イベントルート',
|
||||||
addEventBinding: 'ルートを追加',
|
addEventBinding: 'ルートを追加',
|
||||||
addBehavior: '動作を追加',
|
addBehavior: '動作を追加',
|
||||||
|
dragEventRoute: 'ルート {{index}} をドラッグ',
|
||||||
behaviorReplyMessages: '受信メッセージに返信',
|
behaviorReplyMessages: '受信メッセージに返信',
|
||||||
behaviorReplyMessagesDescription:
|
behaviorReplyMessagesDescription:
|
||||||
'受信メッセージを Agent または Pipeline で処理します。',
|
'受信メッセージを Agent または Pipeline で処理します。',
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ const zhHans = {
|
|||||||
eventBindings: '事件路由',
|
eventBindings: '事件路由',
|
||||||
addEventBinding: '添加路由',
|
addEventBinding: '添加路由',
|
||||||
addBehavior: '添加行为',
|
addBehavior: '添加行为',
|
||||||
|
dragEventRoute: '拖动第 {{index}} 条路由',
|
||||||
behaviorReplyMessages: '回复收到的消息',
|
behaviorReplyMessages: '回复收到的消息',
|
||||||
behaviorReplyMessagesDescription:
|
behaviorReplyMessagesDescription:
|
||||||
'把收到的消息交给 Agent 或 Pipeline 处理。',
|
'把收到的消息交给 Agent 或 Pipeline 处理。',
|
||||||
|
|||||||
@@ -872,6 +872,66 @@ 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 ({
|
||||||
|
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 }) => {
|
test('creates a pipeline then binds it to a bot', async ({ page }) => {
|
||||||
await installLangBotApiMocks(page, { authenticated: true });
|
await installLangBotApiMocks(page, { authenticated: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user