From 8d7a4601af11aff89092d154d010fb486dfbb6e4 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Wed, 12 Aug 2026 11:43:04 +0200 Subject: [PATCH] fix(dashboard): restrict task deletion dropzone and mobile actions to creators --- app/(app)/dashboard/card/Card.tsx | 2 + app/(app)/dashboard/card/CardActions.tsx | 26 ++++++---- app/(app)/dashboard/header/DeleteDropZone.tsx | 49 ++++++++++++++----- app/(app)/dashboard/header/Header.tsx | 36 +++++++++----- app/(app)/dashboard/page.tsx | 16 ++---- app/(app)/trash/page.tsx | 21 ++++++-- types/tasks.ts | 2 +- 7 files changed, 99 insertions(+), 53 deletions(-) diff --git a/app/(app)/dashboard/card/Card.tsx b/app/(app)/dashboard/card/Card.tsx index db0503a..300b602 100644 --- a/app/(app)/dashboard/card/Card.tsx +++ b/app/(app)/dashboard/card/Card.tsx @@ -37,6 +37,7 @@ export default function Card({ } e.dataTransfer.setData("text/plain", task.id); e.dataTransfer.setData("sourceStatus", task.status); + e.dataTransfer.setData("isCreator", String(task.isCreator ?? false)); e.dataTransfer.effectAllowed = "move"; }; @@ -88,6 +89,7 @@ export default function Card({ onDelete?.(task.id)} /> diff --git a/app/(app)/dashboard/card/CardActions.tsx b/app/(app)/dashboard/card/CardActions.tsx index 05d23ac..20cfc9c 100644 --- a/app/(app)/dashboard/card/CardActions.tsx +++ b/app/(app)/dashboard/card/CardActions.tsx @@ -21,10 +21,12 @@ import { COLUMNS, TaskStatus } from "@/types/tasks"; */ export default function CardActions({ currentStatus, + isCreator, onMove, onDelete, }: { currentStatus: TaskStatus; + isCreator: boolean; onMove: (newStatus: TaskStatus) => void; onDelete: () => void; }) { @@ -113,18 +115,22 @@ export default function CardActions({ ); })} -
{/* Delete Button */} - + {isCreator && ( + <> +
+ + + )}
); diff --git a/app/(app)/dashboard/header/DeleteDropZone.tsx b/app/(app)/dashboard/header/DeleteDropZone.tsx index 995fdd5..2114ae5 100644 --- a/app/(app)/dashboard/header/DeleteDropZone.tsx +++ b/app/(app)/dashboard/header/DeleteDropZone.tsx @@ -1,6 +1,6 @@ /** * @file dashboard/header/DeleteDropZone.tsx - * @description Client component rendering an interactive drop zone for deleting tasks during drag-and-drop. + * @description Client component rendering an interactive drop zone for deleting tasks during drag-and-drop operations, managing drag state and drop actions. */ "use client"; @@ -9,26 +9,49 @@ import { useState, useEffect } from "react"; import { Trash2 } from "lucide-react"; /** - * Renders a drop target zone that appears dynamically during drag-and-drop operations, - * allowing users to delete tasks by dragging them onto the designated area. + * Properties for the DeleteDropZone component. * - * @param {Object} props - The component props. - * @param {(taskId: string) => void} props.onTaskDelete - Callback triggered when a task is dropped into the delete zone. - * @returns {JSX.Element | null} The rendered delete drop zone component, or null if no drag operation is active. + * @interface DeleteDropZoneProps + * @property {(taskId: string) => void} onTaskDelete - Callback triggered when a valid task is dropped onto the delete zone. + * @property {(isDragging: boolean) => void} onDragStateChange - Callback notified when drag status and permissions change globally. + */ +interface DeleteDropZoneProps { + onTaskDelete: (taskId: string) => void; + onDragStateChange: (isDragging: boolean) => void; +} + +/** + * Renders a conditional delete drop zone during active drag events, enabling task deletion + * when dropped onto the target area if authorized. + * + * @param {DeleteDropZoneProps} props - The component props. + * @returns {JSX.Element | null} The rendered drop zone component or null if no authorized drag is active. */ export default function DeleteDropZone({ onTaskDelete, -}: { - onTaskDelete: (taskId: string) => void; -}) { + onDragStateChange, +}: DeleteDropZoneProps) { const [isDragging, setIsDragging] = useState(false); + const [isAllowed, setIsAllowed] = useState(false); const [isOver, setIsOver] = useState(false); useEffect(() => { - const handleDragStart = () => setIsDragging(true); + const handleDragStart = (e: DragEvent) => { + const isCreatorString = e.dataTransfer?.getData("isCreator"); + const allowed = isCreatorString === "true"; + + if (allowed) { + setIsDragging(true); + setIsAllowed(true); + onDragStateChange(true); + } + }; + const handleDragEnd = () => { setIsDragging(false); + setIsAllowed(false); setIsOver(false); + onDragStateChange(false); }; window.addEventListener("dragstart", handleDragStart); @@ -38,9 +61,9 @@ export default function DeleteDropZone({ window.removeEventListener("dragstart", handleDragStart); window.removeEventListener("dragend", handleDragEnd); }; - }, []); + }, [onDragStateChange]); - if (!isDragging) return null; + if (!isDragging || !isAllowed) return null; return (
void} onTaskDelete - Callback function triggered when a task is dropped into the delete zone. + */ +interface HeaderProps { + onTaskDelete: (taskId: string) => void; +} /** * Renders the dashboard header section featuring title text, a task deletion drop zone, - * a new task action button, and a link to the trash bin. + * and conditionally displays the new task action button and trash link based on the drag state. * * @param {HeaderProps} props - The component props. * @returns {JSX.Element} The rendered dashboard header component. */ -export default function Header({ - onTaskDelete, -}: { - onTaskDelete: (taskId: string) => void; -}) { +export default function Header({ onTaskDelete }: HeaderProps) { + const [isDraggingActive, setIsDraggingActive] = useState(false); + return (
@@ -36,11 +45,16 @@ export default function Header({
- -
- - -
+ + {!isDraggingActive && ( +
+ + +
+ )}
); diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 17707aa..541290d 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -4,17 +4,8 @@ */ import { db } from "@/db"; -import { tasksTable, taskAssigneesTable, usersTable } from "@/db/schema"; -import { - and, - count, - eq, - inArray, - isNotNull, - isNull, - or, - exists, -} from "drizzle-orm"; +import { taskAssigneesTable, usersTable } from "@/db/schema"; +import { eq, inArray } from "drizzle-orm"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; import Board from "./Board"; @@ -65,8 +56,7 @@ export default async function Dashboard() { ...task, creator: creatorEmail, assignees: assigneesMap.get(task.id) || [], - tags: [], - commentsCount: 0, + isCreator: task.userId === currentUserId, })); return ( diff --git a/app/(app)/trash/page.tsx b/app/(app)/trash/page.tsx index f9091b4..0243c7e 100644 --- a/app/(app)/trash/page.tsx +++ b/app/(app)/trash/page.tsx @@ -4,7 +4,7 @@ */ import { db } from "@/db"; -import { tasksTable } from "@/db/schema"; +import { Task, tasksTable, usersTable } from "@/db/schema"; import { eq, and, isNotNull } from "drizzle-orm"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; @@ -22,20 +22,31 @@ export default async function TrashPage() { const session = await auth(); if (!session?.user?.id) redirect("/login"); - const trashedTasks = await db - .select() + const currentUserId = session.user.id; + + const rawTrashedTasks = await db + .select({ + task: tasksTable, + creatorEmail: usersTable.email, + }) .from(tasksTable) + .innerJoin(usersTable, eq(tasksTable.userId, usersTable.id)) .where( and( - eq(tasksTable.userId, session.user.id), + eq(tasksTable.userId, currentUserId), isNotNull(tasksTable.deletedAt), ), ); + const tasks: Task[] = rawTrashedTasks.map(({ task }) => ({ + ...task, + isCreator: task.userId === currentUserId, + })); + return (
- +
); } diff --git a/types/tasks.ts b/types/tasks.ts index fbeb282..6e0adbb 100644 --- a/types/tasks.ts +++ b/types/tasks.ts @@ -21,9 +21,9 @@ export type TaskPriority = (typeof taskPriorityEnum.enumValues)[number]; // ========================================== export interface Task extends Omit { - dueDate?: Date | null; assignees?: string[]; creator?: string; + isCreator?: boolean; } export interface RouteContext {