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:
@@ -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: "",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user