fix(chat,todo): show sent chat messages immediately + kanban drag-and-drop reorder

チャット送信の即時表示とKanbanのDnD並べ替えを修正。

- chat: 送信成功時に返却メッセージを楽観的にローカルへ先頭追加(SSE到着前でも即表示、idで重複排除)。setSending(false) をレスポンス処理後に移動し二重送信窓を縮小。
- todo: TodoService.reorderItems(projectId, columnId, itemIds[]) を追加(1トランザクションでカラム内を再採番、メンバーシップ+同プロジェクト検証、活動ログ/SSEは1件)。POST /todos/items/reorder を追加。
- KanbanBoard: カード単位のドロップターゲット(ポインタYで前/後挿入)+カラム空き領域は末尾追加。楽観的にorderIndex/columnIdを更新しrenderをorderIndexでソート(即時反映)、失敗時はエラー表示+再取得で巻き戻し。ドラッグ直後の誤クリック(ダイアログ誘起)をdragEndタイムスタンプで抑制。空の並べ替えはSSE/ログを出さない。
This commit is contained in:
Ken Yasue
2026-06-25 13:06:05 +02:00
parent 2588c0247b
commit 03cf27a148
6 changed files with 441 additions and 126 deletions

View File

@ -171,4 +171,58 @@ test.describe('todo / kanban', () => {
await expect(page.getByTestId('todo-dialog')).toBeVisible();
await expect(page.getByTestId('attachment-list')).toBeVisible();
});
test('reorder cards within a column by drag and drop and persist', async ({
page,
}) => {
const projectId = await setupOwner(page);
const colsRes = await page.request.get(
`/api/projects/${projectId}/todos/columns`
);
const cols = (
(await colsRes.json()) as { columns: { id: number; name: string }[] }
).columns;
const backlog = cols.find((c) => c.name === 'Backlog')!;
// 3 つのタスクを Backlog に作成(作成順 A, B, C)
const titles = ['Task-A', 'Task-B', 'Task-C'];
const ids: number[] = [];
for (const title of titles) {
const res = await page.request.post(
`/api/projects/${projectId}/todos/items`,
{ data: { title, columnId: backlog.id } }
);
const { item } = (await res.json()) as { item: { id: number } };
ids.push(item.id);
}
const [aId, bId, cId] = ids;
await page.goto(`/projects/${projectId}/todos`);
const col = page.getByTestId(`kanban-column-${backlog.id}`);
async function orderIds(): Promise<number[]> {
return col
.locator('[data-testid^="todo-open-"]')
.evaluateAll((els) =>
els.map((e) => Number(e.getAttribute('data-testid')!.slice(10)))
);
}
await expect(col.getByTestId(`todo-card-${cId}`)).toBeVisible();
// C を A の上にドラッグ(前へ挿入) -> 順序は C, A, B
await page
.getByTestId(`todo-card-${cId}`)
.dragTo(page.getByTestId(`todo-card-${aId}`), {
targetPosition: { x: 10, y: 2 },
});
await expect
.poll(async () => (await orderIds())[0], { timeout: 10_000 })
.toBe(cId);
expect(await orderIds()).toEqual([cId, aId, bId]);
// リロード後も順序が維持される
await page.reload();
expect(await orderIds()).toEqual([cId, aId, bId]);
});
});