From f319e8ec75a104ba17f8aa482657224871139335 Mon Sep 17 00:00:00 2001 From: M1ngdaXie <156019134+M1ngdaXie@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:38:02 -0800 Subject: [PATCH] feat(kanban): implement task reordering and improve task movement logic feat(share): add documentType prop to ShareModal for dynamic URL generation --- frontend/src/components/Kanban/Column.tsx | 3 +- .../src/components/Kanban/KanbanBoard.tsx | 91 ++++++++++++++----- frontend/src/components/Share/ShareModal.tsx | 13 ++- frontend/src/pages/EditorPage.tsx | 1 + frontend/src/pages/KanbanPage.tsx | 6 +- 5 files changed, 87 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/Kanban/Column.tsx b/frontend/src/components/Kanban/Column.tsx index 5229cdb..8352306 100644 --- a/frontend/src/components/Kanban/Column.tsx +++ b/frontend/src/components/Kanban/Column.tsx @@ -7,7 +7,6 @@ import type { KanbanColumn, Task } from "./KanbanBoard.tsx"; interface ColumnProps { column: KanbanColumn; onAddTask: (task: Task) => void; - onMoveTask: (taskId: string, toColumnId: string) => void; } const Column = ({ column, onAddTask }: ColumnProps) => { @@ -21,7 +20,7 @@ const Column = ({ column, onAddTask }: ColumnProps) => { const handleAddTask = () => { if (newTaskTitle.trim()) { onAddTask({ - id: `task-${Date.now()}`, + id: `task-${crypto.randomUUID()}`, title: newTaskTitle, description: "", }); diff --git a/frontend/src/components/Kanban/KanbanBoard.tsx b/frontend/src/components/Kanban/KanbanBoard.tsx index 709c284..ed01cc4 100644 --- a/frontend/src/components/Kanban/KanbanBoard.tsx +++ b/frontend/src/components/Kanban/KanbanBoard.tsx @@ -6,6 +6,7 @@ import { useSensor, useSensors, } from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; import type { YjsProviders } from "../../lib/yjs"; import Column from "./Column.tsx"; @@ -71,17 +72,44 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => { if (columnIndex !== -1) { providers.ydoc.transact(() => { const column = cols[columnIndex] as KanbanColumn; - column.tasks.push(task); + const nextTasks = [...column.tasks, task]; + const nextColumn = { ...column, tasks: nextTasks }; yarray.delete(columnIndex, 1); - yarray.insert(columnIndex, [column]); + yarray.insert(columnIndex, [nextColumn]); }); } }; + const replaceColumn = (index: number, column: KanbanColumn) => { + const yarray = providers.ydoc.getArray("kanban-columns"); + yarray.delete(index, 1); + yarray.insert(index, [column]); + }; + + const findColumnByTaskId = (taskId: string) => + columns.find((col) => col.tasks.some((task) => task.id === taskId)); + + const reorderTask = (columnId: string, fromIndex: number, toIndex: number) => { + if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return; + const yarray = providers.ydoc.getArray("kanban-columns"); + const cols = yarray.toArray(); + const columnIndex = cols.findIndex((col: any) => col.id === columnId); + if (columnIndex === -1) return; + + const column = cols[columnIndex] as KanbanColumn; + const nextTasks = arrayMove(column.tasks, fromIndex, toIndex); + const nextColumn = { ...column, tasks: nextTasks }; + + providers.ydoc.transact(() => { + replaceColumn(columnIndex, nextColumn); + }); + }; + const moveTask = ( fromColumnId: string, toColumnId: string, - taskId: string + taskId: string, + overTaskId?: string ) => { const yarray = providers.ydoc.getArray("kanban-columns"); const cols = yarray.toArray(); @@ -91,18 +119,30 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => { if (fromIndex !== -1 && toIndex !== -1) { providers.ydoc.transact(() => { - const fromCol = { ...(cols[fromIndex] as KanbanColumn) }; - const toCol = { ...(cols[toIndex] as KanbanColumn) }; + const fromCol = cols[fromIndex] as KanbanColumn; + const toCol = cols[toIndex] as KanbanColumn; + const nextFromTasks = [...fromCol.tasks]; + const nextToTasks = fromIndex === toIndex ? nextFromTasks : [...toCol.tasks]; - const taskIndex = fromCol.tasks.findIndex((t: Task) => t.id === taskId); + const taskIndex = nextFromTasks.findIndex((t: Task) => t.id === taskId); if (taskIndex !== -1) { - const [task] = fromCol.tasks.splice(taskIndex, 1); - toCol.tasks.push(task); + const [task] = nextFromTasks.splice(taskIndex, 1); + const insertIndex = + overTaskId && overTaskId !== toColumnId + ? nextToTasks.findIndex((t: Task) => t.id === overTaskId) + : -1; - yarray.delete(fromIndex, 1); - yarray.insert(fromIndex, [fromCol]); - yarray.delete(toIndex, 1); - yarray.insert(toIndex, [toCol]); + if (insertIndex >= 0) { + nextToTasks.splice(insertIndex, 0, task); + } else { + nextToTasks.push(task); + } + + const nextFromCol = { ...fromCol, tasks: nextFromTasks }; + const nextToCol = { ...toCol, tasks: nextToTasks }; + + replaceColumn(fromIndex, nextFromCol); + replaceColumn(toIndex, nextToCol); } }); } @@ -114,16 +154,28 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => { if (!over) return; const taskId = active.id as string; - const targetColumnId = over.id as string; + const overId = over.id as string; // Find which column the task is currently in - const fromColumn = columns.find(col => - col.tasks.some(task => task.id === taskId) - ); + const fromColumn = findColumnByTaskId(taskId); + if (!fromColumn) return; - if (fromColumn && fromColumn.id !== targetColumnId) { - moveTask(fromColumn.id, targetColumnId, taskId); + const overColumn = + columns.find((col) => col.id === overId) || findColumnByTaskId(overId); + if (!overColumn) return; + + if (fromColumn.id === overColumn.id) { + // Reorder within the same column + const oldIndex = fromColumn.tasks.findIndex((task) => task.id === taskId); + const newIndex = fromColumn.tasks.findIndex((task) => task.id === overId); + if (newIndex !== -1 && oldIndex !== -1) { + reorderTask(fromColumn.id, oldIndex, newIndex); + } + return; } + + // Move to a different column + moveTask(fromColumn.id, overColumn.id, taskId, overId); }; return ( @@ -134,9 +186,6 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => { key={column.id} column={column} onAddTask={(task) => addTask(column.id, task)} - onMoveTask={(taskId, toColumnId) => - moveTask(column.id, toColumnId, taskId) - } /> ))} diff --git a/frontend/src/components/Share/ShareModal.tsx b/frontend/src/components/Share/ShareModal.tsx index f08183e..ebc0c2a 100644 --- a/frontend/src/components/Share/ShareModal.tsx +++ b/frontend/src/components/Share/ShareModal.tsx @@ -6,12 +6,19 @@ import './ShareModal.css'; interface ShareModalProps { documentId: string; + documentType?: 'editor' | 'kanban'; onClose: () => void; currentPermission?: string; currentRole?: string; } -function ShareModal({ documentId, onClose, currentPermission, currentRole }: ShareModalProps) { +function ShareModal({ + documentId, + documentType = 'editor', + onClose, + currentPermission, + currentRole, +}: ShareModalProps) { const [activeTab, setActiveTab] = useState<'users' | 'link'>('users'); const [shares, setShares] = useState([]); const [shareLink, setShareLink] = useState(null); @@ -138,7 +145,7 @@ function ShareModal({ documentId, onClose, currentPermission, currentRole }: Sha const handleCopyLink = () => { if (!shareLink) return; - const url = `${window.location.origin}/editor/${documentId}?share=${shareLink.token}`; + const url = `${window.location.origin}/${documentType}/${documentId}?share=${shareLink.token}`; navigator.clipboard.writeText(url); setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -278,7 +285,7 @@ function ShareModal({ documentId, onClose, currentPermission, currentRole }: Sha
diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index c02928a..5e590b1 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -58,6 +58,7 @@ const EditorPage = () => { {showShareModal && ( setShowShareModal(false)} currentPermission={permission || undefined} currentRole={role || undefined} diff --git a/frontend/src/pages/KanbanPage.tsx b/frontend/src/pages/KanbanPage.tsx index b4a9493..997dc8b 100644 --- a/frontend/src/pages/KanbanPage.tsx +++ b/frontend/src/pages/KanbanPage.tsx @@ -42,7 +42,11 @@ const KanbanPage = () => {
{showShareModal && ( - setShowShareModal(false)} /> + setShowShareModal(false)} + /> )} );