feat(kanban): implement task reordering and improve task movement logic

feat(share): add documentType prop to ShareModal for dynamic URL generation
This commit is contained in:
M1ngdaXie
2026-02-08 16:38:02 -08:00
parent 3179ead0a5
commit f319e8ec75
5 changed files with 87 additions and 27 deletions

View File

@@ -7,7 +7,6 @@ import type { KanbanColumn, Task } from "./KanbanBoard.tsx";
interface ColumnProps { interface ColumnProps {
column: KanbanColumn; column: KanbanColumn;
onAddTask: (task: Task) => void; onAddTask: (task: Task) => void;
onMoveTask: (taskId: string, toColumnId: string) => void;
} }
const Column = ({ column, onAddTask }: ColumnProps) => { const Column = ({ column, onAddTask }: ColumnProps) => {
@@ -21,7 +20,7 @@ const Column = ({ column, onAddTask }: ColumnProps) => {
const handleAddTask = () => { const handleAddTask = () => {
if (newTaskTitle.trim()) { if (newTaskTitle.trim()) {
onAddTask({ onAddTask({
id: `task-${Date.now()}`, id: `task-${crypto.randomUUID()}`,
title: newTaskTitle, title: newTaskTitle,
description: "", description: "",
}); });

View File

@@ -6,6 +6,7 @@ import {
useSensor, useSensor,
useSensors, useSensors,
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable';
import type { YjsProviders } from "../../lib/yjs"; import type { YjsProviders } from "../../lib/yjs";
import Column from "./Column.tsx"; import Column from "./Column.tsx";
@@ -71,17 +72,44 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => {
if (columnIndex !== -1) { if (columnIndex !== -1) {
providers.ydoc.transact(() => { providers.ydoc.transact(() => {
const column = cols[columnIndex] as KanbanColumn; 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.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 = ( const moveTask = (
fromColumnId: string, fromColumnId: string,
toColumnId: string, toColumnId: string,
taskId: string taskId: string,
overTaskId?: string
) => { ) => {
const yarray = providers.ydoc.getArray("kanban-columns"); const yarray = providers.ydoc.getArray("kanban-columns");
const cols = yarray.toArray(); const cols = yarray.toArray();
@@ -91,18 +119,30 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => {
if (fromIndex !== -1 && toIndex !== -1) { if (fromIndex !== -1 && toIndex !== -1) {
providers.ydoc.transact(() => { providers.ydoc.transact(() => {
const fromCol = { ...(cols[fromIndex] as KanbanColumn) }; const fromCol = cols[fromIndex] as KanbanColumn;
const toCol = { ...(cols[toIndex] 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) { if (taskIndex !== -1) {
const [task] = fromCol.tasks.splice(taskIndex, 1); const [task] = nextFromTasks.splice(taskIndex, 1);
toCol.tasks.push(task); const insertIndex =
overTaskId && overTaskId !== toColumnId
? nextToTasks.findIndex((t: Task) => t.id === overTaskId)
: -1;
yarray.delete(fromIndex, 1); if (insertIndex >= 0) {
yarray.insert(fromIndex, [fromCol]); nextToTasks.splice(insertIndex, 0, task);
yarray.delete(toIndex, 1); } else {
yarray.insert(toIndex, [toCol]); 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; if (!over) return;
const taskId = active.id as string; 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 // Find which column the task is currently in
const fromColumn = columns.find(col => const fromColumn = findColumnByTaskId(taskId);
col.tasks.some(task => task.id === taskId) if (!fromColumn) return;
);
if (fromColumn && fromColumn.id !== targetColumnId) { const overColumn =
moveTask(fromColumn.id, targetColumnId, taskId); 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 ( return (
@@ -134,9 +186,6 @@ const KanbanBoard = ({ providers }: KanbanBoardProps) => {
key={column.id} key={column.id}
column={column} column={column}
onAddTask={(task) => addTask(column.id, task)} onAddTask={(task) => addTask(column.id, task)}
onMoveTask={(taskId, toColumnId) =>
moveTask(column.id, toColumnId, taskId)
}
/> />
))} ))}
</div> </div>

View File

@@ -6,12 +6,19 @@ import './ShareModal.css';
interface ShareModalProps { interface ShareModalProps {
documentId: string; documentId: string;
documentType?: 'editor' | 'kanban';
onClose: () => void; onClose: () => void;
currentPermission?: string; currentPermission?: string;
currentRole?: 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 [activeTab, setActiveTab] = useState<'users' | 'link'>('users');
const [shares, setShares] = useState<DocumentShareWithUser[]>([]); const [shares, setShares] = useState<DocumentShareWithUser[]>([]);
const [shareLink, setShareLink] = useState<ShareLink | null>(null); const [shareLink, setShareLink] = useState<ShareLink | null>(null);
@@ -138,7 +145,7 @@ function ShareModal({ documentId, onClose, currentPermission, currentRole }: Sha
const handleCopyLink = () => { const handleCopyLink = () => {
if (!shareLink) return; 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); navigator.clipboard.writeText(url);
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
@@ -278,7 +285,7 @@ function ShareModal({ documentId, onClose, currentPermission, currentRole }: Sha
<div className="link-box"> <div className="link-box">
<input <input
type="text" type="text"
value={`${window.location.origin}/editor/${documentId}?share=${shareLink.token}`} value={`${window.location.origin}/${documentType}/${documentId}?share=${shareLink.token}`}
readOnly readOnly
className="link-input" className="link-input"
/> />

View File

@@ -58,6 +58,7 @@ const EditorPage = () => {
{showShareModal && ( {showShareModal && (
<ShareModal <ShareModal
documentId={id!} documentId={id!}
documentType="editor"
onClose={() => setShowShareModal(false)} onClose={() => setShowShareModal(false)}
currentPermission={permission || undefined} currentPermission={permission || undefined}
currentRole={role || undefined} currentRole={role || undefined}

View File

@@ -42,7 +42,11 @@ const KanbanPage = () => {
</div> </div>
{showShareModal && ( {showShareModal && (
<ShareModal documentId={id!} onClose={() => setShowShareModal(false)} /> <ShareModal
documentId={id!}
documentType="kanban"
onClose={() => setShowShareModal(false)}
/>
)} )}
</div> </div>
); );