feat(dashboard): add drag-to-delete and mobile delete actions
This commit is contained in:
parent
0fd1fa377b
commit
2d2a4388e2
6 changed files with 137 additions and 16 deletions
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* @file dashboard/KanbanBoard.tsx
|
||||
* @description Client component wrapping the kanban columns grid, tracking individual task update states, and handling asynchronous status mutations via API.
|
||||
* @description Client component wrapping the kanban columns grid, tracking individual task update/deletion states, and handling asynchronous mutations via API.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
|
@ -13,7 +13,7 @@ import KanbanBoardHeader from "./components/KanbanBoardHeader";
|
|||
|
||||
/**
|
||||
* Renders the responsive grid container of kanban columns, coordinating state tracking
|
||||
* for active task updates and triggering status mutation API requests.
|
||||
* for active task updates/deletions and triggering mutation API requests.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {Task[]} props.tasks - The array of task items displayed across the board.
|
||||
|
|
@ -61,10 +61,39 @@ export default function KanbanBoard({ tasks }: { tasks: Task[] }) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a specific task by sending a DELETE request to the API,
|
||||
* managing loading states, and refreshing the router upon success.
|
||||
*
|
||||
* @param {string} taskId - The unique identifier of the task to delete.
|
||||
*/
|
||||
const deleteTask = (taskId: string) => {
|
||||
setUpdatingTaskIds((prev) => new Set(prev).add(taskId));
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to delete task");
|
||||
await router.refresh();
|
||||
} catch (error) {
|
||||
console.error("Error during task deletion:", error);
|
||||
} finally {
|
||||
setUpdatingTaskIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(taskId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Workspace Header */}
|
||||
<KanbanBoardHeader />
|
||||
<KanbanBoardHeader onTaskDelete={deleteTask} />
|
||||
|
||||
{/* Kanban Columns Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 items-start">
|
||||
|
|
@ -80,6 +109,7 @@ export default function KanbanBoard({ tasks }: { tasks: Task[] }) {
|
|||
color={col.color}
|
||||
updatingTaskIds={updatingTaskIds}
|
||||
onTaskMove={updateTaskStatus}
|
||||
onTaskDelete={deleteTask}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default function KanbanCard({
|
|||
task,
|
||||
isUpdating = false,
|
||||
onStatusChange,
|
||||
onDelete,
|
||||
}: KanbanCardProps) {
|
||||
/**
|
||||
* Initiates the drag action on a task card if not currently updating, storing its ID and status payload.
|
||||
|
|
@ -88,7 +89,11 @@ export default function KanbanCard({
|
|||
creator={task.creator}
|
||||
assignees={task.assignees}
|
||||
/>
|
||||
<KanbanCardActions currentStatus={task.status} onMove={handleMove} />
|
||||
<KanbanCardActions
|
||||
currentStatus={task.status}
|
||||
onMove={handleMove}
|
||||
onDelete={() => onDelete?.(task.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export default function KanbanColumn(props: KanbanColumnProps) {
|
|||
task={task}
|
||||
isUpdating={props.updatingTaskIds?.has(task.id)}
|
||||
onStatusChange={props.onTaskMove}
|
||||
onDelete={props.onTaskDelete}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,45 @@
|
|||
/**
|
||||
* @file dashboard/components/KanbanBoardHeader.tsx
|
||||
* @description Component rendering the top title header and action buttons for the dashboard view.
|
||||
* @description Component rendering the top title header with an integrated delete drop zone that appears during drag-and-drop interactions.
|
||||
*/
|
||||
|
||||
import { Plus, Sparkles } from "lucide-react";
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Plus, Sparkles, Trash2 } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Renders the dashboard header section including title, subtitle, workspace indicator, and action triggers.
|
||||
* Renders the dashboard header featuring a workspace title, subtitle, new task action button,
|
||||
* and a dynamic drop zone for deleting tasks during drag operations.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered dashboard header component.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {(taskId: string) => void} props.onTaskDelete - Callback function invoked when a task is dropped into the delete zone.
|
||||
* @returns {JSX.Element} The rendered kanban board header component.
|
||||
*/
|
||||
export default function KanbanBoardHeader() {
|
||||
export default function KanbanBoardHeader({
|
||||
onTaskDelete,
|
||||
}: {
|
||||
onTaskDelete: (taskId: string) => void;
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isOver, setIsOver] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleDragStart = () => setIsDragging(true);
|
||||
const handleDragEnd = () => {
|
||||
setIsDragging(false);
|
||||
setIsOver(false);
|
||||
};
|
||||
|
||||
window.addEventListener("dragstart", handleDragStart);
|
||||
window.addEventListener("dragend", handleDragEnd);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("dragstart", handleDragStart);
|
||||
window.removeEventListener("dragend", handleDragEnd);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
|
|
@ -25,10 +54,38 @@ export default function KanbanBoardHeader() {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="inline-flex items-center gap-2 px-3.5 py-2.5 rounded-xl font-medium text-sm text-black hover:text-foreground bg-primary hover:bg-primary-hover active:scale-95 transition-colors duration-200 cursor-pointer">
|
||||
<Plus size={16} />
|
||||
New Task
|
||||
</button>
|
||||
{isDragging ? (
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
setIsOver(true);
|
||||
}}
|
||||
onDragLeave={() => setIsOver(false)}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOver(false);
|
||||
setIsDragging(false);
|
||||
const taskId = e.dataTransfer.getData("text/plain");
|
||||
if (taskId) {
|
||||
onTaskDelete(taskId);
|
||||
}
|
||||
}}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl font-medium text-sm border-2 border-dashed cursor-pointer select-none transition-all duration-300 ease-out transform ${
|
||||
isOver
|
||||
? "bg-destructive border-destructive scale-110 shadow-lg animate-pulse"
|
||||
: "bg-destructive/10 border-destructive/40 text-destructive scale-100 opacity-90 hover:opacity-100 animate-in fade-in zoom-in-95 duration-200"
|
||||
}`}
|
||||
>
|
||||
<Trash2 size={16} className={isOver ? "animate-bounce" : ""} />
|
||||
<span>Drop to Delete</span>
|
||||
</div>
|
||||
) : (
|
||||
<button className="inline-flex items-center gap-2 px-3.5 py-2.5 rounded-xl font-medium text-sm text-black hover:text-foreground bg-primary hover:bg-primary-hover active:scale-95 transition-colors duration-200 cursor-pointer">
|
||||
<Plus size={16} />
|
||||
New Task
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
/**
|
||||
* @file dashboard/components/KanbanCardActions.tsx
|
||||
* @description Client component rendering the mobile status transition dropdown for a kanban card.
|
||||
* @description Client component rendering the mobile status transition and deletion dropdown for a kanban card.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { MoreHorizontal, CornerDownRight } from "lucide-react";
|
||||
import { MoreHorizontal, CornerDownRight, Trash2 } from "lucide-react";
|
||||
import { KANBAN_COLUMNS, TaskStatus } from "@/types/tasks";
|
||||
|
||||
/**
|
||||
* Renders a mobile-only action menu component allowing users to move a task
|
||||
* between different kanban columns via a dropdown interface.
|
||||
* between different kanban columns or delete it entirely via a dropdown interface.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {TaskStatus} props.currentStatus - The current status category of the task.
|
||||
* @param {(newStatus: TaskStatus) => void} props.onMove - Callback function triggered when a new status column is selected.
|
||||
* @param {() => void} props.onDelete - Callback function triggered when the delete action is selected.
|
||||
* @returns {JSX.Element} The rendered mobile card actions component.
|
||||
*/
|
||||
export default function KanbanCardActions({
|
||||
currentStatus,
|
||||
onMove,
|
||||
onDelete,
|
||||
}: {
|
||||
currentStatus: TaskStatus;
|
||||
onMove: (newStatus: TaskStatus) => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const [showMobileActions, setShowMobileActions] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -59,6 +62,17 @@ export default function KanbanCardActions({
|
|||
setShowMobileActions(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles clicking the delete action item to trigger task deletion.
|
||||
*
|
||||
* @param {React.MouseEvent} e - The mouse event object.
|
||||
*/
|
||||
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
setShowMobileActions(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative sm:hidden" ref={dropdownRef}>
|
||||
<button
|
||||
|
|
@ -99,6 +113,18 @@ export default function KanbanCardActions({
|
|||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="h-px bg-border my-1" />
|
||||
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
onClick={handleDeleteClick}
|
||||
className="w-full text-left px-3 py-2 rounded-lg hover:bg-destructive hover:text-foreground flex items-center justify-between text-destructive group/del transition-colors cursor-pointer font-medium"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Trash2 size={12} />
|
||||
Delete Task
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,12 +46,14 @@ export interface KanbanColumnProps extends KanbanColumnConfig {
|
|||
tasks: Task[];
|
||||
updatingTaskIds?: Set<string>;
|
||||
onTaskMove?: (taskId: string, targetStatus: TaskStatus) => void;
|
||||
onTaskDelete?: (taskId: string) => void;
|
||||
}
|
||||
|
||||
export interface KanbanCardProps {
|
||||
task: Task;
|
||||
isUpdating?: boolean;
|
||||
onStatusChange?: (taskId: string, newStatus: TaskStatus) => void;
|
||||
onDelete?: (taskId: string) => void;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue