feat(dashboard): add individual task loading states and optimize client-server component architecture
This commit is contained in:
parent
68e5070d78
commit
c4989155b3
5 changed files with 135 additions and 83 deletions
82
app/(app)/dashboard/KanbanBoard.tsx
Normal file
82
app/(app)/dashboard/KanbanBoard.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
/**
|
||||||
|
* @file dashboard/KanbanBoard.tsx
|
||||||
|
* @description Client component wrapping the kanban columns grid, tracking individual task update states, and handling asynchronous status mutations via API.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import KanbanColumn from "./KanbanColumn";
|
||||||
|
import { KANBAN_COLUMNS, Task, TaskStatus } from "@/types/tasks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the responsive grid container of kanban columns, coordinating state tracking
|
||||||
|
* for active task updates and triggering status mutation API requests.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The component props.
|
||||||
|
* @param {Task[]} props.tasks - The array of task items displayed across the board.
|
||||||
|
* @returns {JSX.Element} The rendered kanban board component.
|
||||||
|
*/
|
||||||
|
export default function KanbanBoard({ tasks }: { tasks: Task[] }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [, startTransition] = useTransition();
|
||||||
|
const [updatingTaskIds, setUpdatingTaskIds] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the status of a specific task by sending a PATCH request to the API,
|
||||||
|
* managing loading states, and refreshing the router upon success.
|
||||||
|
*
|
||||||
|
* @param {string} taskId - The unique identifier of the task to update.
|
||||||
|
* @param {TaskStatus} targetStatus - The new target status for the task.
|
||||||
|
*/
|
||||||
|
const updateTaskStatus = (taskId: string, targetStatus: TaskStatus) => {
|
||||||
|
setUpdatingTaskIds((prev) => new Set(prev).add(taskId));
|
||||||
|
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/tasks/${taskId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ status: targetStatus }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to update task status");
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during task status update:", error);
|
||||||
|
} finally {
|
||||||
|
setUpdatingTaskIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(taskId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 items-start">
|
||||||
|
{KANBAN_COLUMNS.map((col) => {
|
||||||
|
const columnTasks = tasks.filter((t) => t.status === col.id);
|
||||||
|
return (
|
||||||
|
<KanbanColumn
|
||||||
|
key={col.id}
|
||||||
|
id={col.id}
|
||||||
|
title={col.title}
|
||||||
|
count={columnTasks.length}
|
||||||
|
tasks={columnTasks}
|
||||||
|
color={col.color}
|
||||||
|
updatingTaskIds={updatingTaskIds}
|
||||||
|
onTaskMove={updateTaskStatus}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/KanbanCard.tsx
|
* @file dashboard/KanbanCard.tsx
|
||||||
* @description Client component rendering a single kanban card container with native drag-and-drop source handlers and modular sub-components.
|
* @description Client component rendering a single kanban card container with individual loading states and modular sub-components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { KanbanCardProps, TaskStatus } from "@/types/tasks";
|
import { KanbanCardProps, TaskStatus } from "@/types/tasks";
|
||||||
import KanbanCardActions from "./components/KanbanCardActions";
|
import KanbanCardActions from "./components/KanbanCardActions";
|
||||||
import KanbanCardAvatars from "./components/KanbanCardAvatars";
|
import KanbanCardAvatars from "./components/KanbanCardAvatars";
|
||||||
|
|
@ -12,47 +13,64 @@ import KanbanCardDueDate from "./components/KanbanCardDueDate";
|
||||||
import KanbanCardPriority from "./components/KanbanCardPriority";
|
import KanbanCardPriority from "./components/KanbanCardPriority";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an interactive kanban card container holding title, description,
|
* Renders an interactive kanban card container handling drag-and-drop actions, loading states,
|
||||||
* modular priority badges, deadline elements, assignees, and action triggers.
|
* and assembling modular sub-components for priorities, due dates, avatars, and actions.
|
||||||
*
|
*
|
||||||
* @param {KanbanCardProps} props - The component props containing the task object and status change handler.
|
* @param {KanbanCardProps} props - The component props containing the task object, updating status flag, and status change handler.
|
||||||
* @returns {JSX.Element} The rendered kanban card container component.
|
* @returns {JSX.Element} The rendered kanban card component.
|
||||||
*/
|
*/
|
||||||
export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
|
export default function KanbanCard({
|
||||||
|
task,
|
||||||
|
isUpdating = false,
|
||||||
|
onStatusChange,
|
||||||
|
}: KanbanCardProps) {
|
||||||
/**
|
/**
|
||||||
* Initiates the drag action on a task card, storing its ID and current status in the data transfer payload.
|
* Initiates the drag action on a task card if not currently updating, storing its ID and status payload.
|
||||||
*
|
*
|
||||||
* @param {React.DragEvent<HTMLDivElement>} e - The drag event object.
|
* @param {React.DragEvent<HTMLDivElement>} e - The drag event object.
|
||||||
*/
|
*/
|
||||||
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
|
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
if (isUpdating) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.dataTransfer.setData("text/plain", task.id);
|
e.dataTransfer.setData("text/plain", task.id);
|
||||||
e.dataTransfer.setData("sourceStatus", task.status);
|
e.dataTransfer.setData("sourceStatus", task.status);
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = "move";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers the status change callback with the target task ID and new status.
|
* Triggers the status change callback with the target task ID and new status if updates are permitted.
|
||||||
*
|
*
|
||||||
* @param {TaskStatus} newStatus - The target task status to transition to.
|
* @param {TaskStatus} newStatus - The target task status to transition to.
|
||||||
*/
|
*/
|
||||||
const handleMove = (newStatus: TaskStatus) => {
|
const handleMove = (newStatus: TaskStatus) => {
|
||||||
if (onStatusChange) {
|
if (!onStatusChange || isUpdating) return;
|
||||||
onStatusChange(task.id, newStatus);
|
onStatusChange(task.id, newStatus);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
draggable
|
draggable={!isUpdating}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
className="group relative bg-card/40 border border-border/80 hover:border-primary/60 p-4 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 space-y-3 cursor-grab active:cursor-grabbing hover:-translate-y-0.5 flex flex-col h-full"
|
className={`group relative bg-card/40 border border-border/80 hover:border-primary/60 p-4 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 space-y-3 flex flex-col h-full ${
|
||||||
|
isUpdating
|
||||||
|
? "opacity-50 pointer-events-none cursor-wait bg-primary/5 border-primary/40 animate-pulse"
|
||||||
|
: "cursor-grab active:cursor-grabbing hover:-translate-y-0.5"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
|
{/* Loading Spinner */}
|
||||||
|
{isUpdating && (
|
||||||
|
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/25 backdrop-blur-[0.5px] rounded-2xl">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* --- Card Header --- */}
|
{/* --- Card Header --- */}
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<h3 className="font-semibold leading-snug group-hover/card:text-primary transition-colors line-clamp-2">
|
<h3 className="font-semibold leading-snug group-hover/card:text-primary transition-colors line-clamp-2">
|
||||||
{task.title}
|
{task.title}
|
||||||
</h3>
|
</h3>
|
||||||
{/* Card Priority */}
|
|
||||||
<KanbanCardPriority priority={task.priority} />
|
<KanbanCardPriority priority={task.priority} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -63,10 +81,8 @@ export default function KanbanCard({ task, onStatusChange }: KanbanCardProps) {
|
||||||
|
|
||||||
{/* --- Card Footer --- */}
|
{/* --- Card Footer --- */}
|
||||||
<div className="flex items-center justify-between pt-3 mt-auto text-xs border-t border-border">
|
<div className="flex items-center justify-between pt-3 mt-auto text-xs border-t border-border">
|
||||||
{/* Date Badge Component */}
|
|
||||||
<KanbanCardDueDate task={task} />
|
<KanbanCardDueDate task={task} />
|
||||||
|
|
||||||
{/* Avatars & Mobile Switcher */}
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<KanbanCardAvatars
|
<KanbanCardAvatars
|
||||||
creator={task.creator}
|
creator={task.creator}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,27 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/KanbanColumn.tsx
|
* @file dashboard/KanbanColumn.tsx
|
||||||
* @description Client component rendering a single kanban column container supporting drag-and-drop drop targets and status updates.
|
* @description Client component rendering a single kanban column container supporting drag-and-drop drop targets, dynamic status updates, and task lists.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useTransition } from "react";
|
import { useState } from "react";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import KanbanCard from "./KanbanCard";
|
import KanbanCard from "./KanbanCard";
|
||||||
import { KanbanColumnProps, TaskStatus } from "@/types/tasks";
|
import { KanbanColumnProps, TaskStatus } from "@/types/tasks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a kanban column with title indicators, task counters, drag-and-drop event handlers,
|
* Renders an individual kanban column with an indicator, title, item counter,
|
||||||
* and lists of nested KanbanCard items.
|
* add-task button, and drag-and-drop target zone containing its associated task cards.
|
||||||
*
|
*
|
||||||
* @param {KanbanColumnProps} props - The component props including column ID, title, count, tasks, and color configuration.
|
* @param {KanbanColumnProps} props - The component props containing column configurations, tasks, and event callbacks.
|
||||||
* @returns {JSX.Element} The rendered kanban column component.
|
* @returns {JSX.Element} The rendered kanban column component.
|
||||||
*/
|
*/
|
||||||
export default function KanbanColumn(props: KanbanColumnProps) {
|
export default function KanbanColumn(props: KanbanColumnProps) {
|
||||||
const router = useRouter();
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||||
const indicatorColor = props.color ?? "bg-primary";
|
const indicatorColor = props.color ?? "bg-primary";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the status of a specific task via a PATCH API request and refreshes the router state.
|
* Handles the drag-over event to allow items to be dropped into the column.
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {string} taskId - The unique identifier of the task to update.
|
|
||||||
* @param {TaskStatus} targetStatus - The destination status to apply to the task.
|
|
||||||
*/
|
|
||||||
const updateTaskStatus = (taskId: string, targetStatus: TaskStatus) => {
|
|
||||||
startTransition(async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/tasks/${taskId}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ status: targetStatus }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to update task status");
|
|
||||||
}
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during task status update:", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the drag-over event to allow dropping items onto the column.
|
|
||||||
*
|
*
|
||||||
* @param {React.DragEvent<HTMLDivElement>} e - The drag event object.
|
* @param {React.DragEvent<HTMLDivElement>} e - The drag event object.
|
||||||
*/
|
*/
|
||||||
|
|
@ -63,14 +32,14 @@ export default function KanbanColumn(props: KanbanColumnProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the drag-leave event when a dragged item leaves the column bounds.
|
* Resets the drag-over state when the dragged element leaves the column boundary.
|
||||||
*/
|
*/
|
||||||
const handleDragLeave = () => {
|
const handleDragLeave = () => {
|
||||||
setIsDraggingOver(false);
|
setIsDraggingOver(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles dropping a task card onto the column, triggering status updates if valid.
|
* Handles dropping a task card onto the column, extracting task metadata and triggering the move action.
|
||||||
*
|
*
|
||||||
* @param {React.DragEvent<HTMLDivElement>} e - The drop event object.
|
* @param {React.DragEvent<HTMLDivElement>} e - The drop event object.
|
||||||
*/
|
*/
|
||||||
|
|
@ -83,7 +52,7 @@ export default function KanbanColumn(props: KanbanColumnProps) {
|
||||||
|
|
||||||
if (!taskId || sourceStatus === props.id) return;
|
if (!taskId || sourceStatus === props.id) return;
|
||||||
|
|
||||||
updateTaskStatus(taskId, props.id);
|
props.onTaskMove?.(taskId, props.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -95,7 +64,7 @@ export default function KanbanColumn(props: KanbanColumnProps) {
|
||||||
isDraggingOver
|
isDraggingOver
|
||||||
? "border-primary/80 bg-primary/5 shadow-lg ring-4 ring-primary/10"
|
? "border-primary/80 bg-primary/5 shadow-lg ring-4 ring-primary/10"
|
||||||
: "border-border/60 shadow-sm"
|
: "border-border/60 shadow-sm"
|
||||||
} ${isPending ? "opacity-60 pointer-events-none" : ""}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Column Header */}
|
{/* Column Header */}
|
||||||
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border/40">
|
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border/40">
|
||||||
|
|
@ -126,7 +95,8 @@ export default function KanbanColumn(props: KanbanColumnProps) {
|
||||||
<KanbanCard
|
<KanbanCard
|
||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
onStatusChange={updateTaskStatus}
|
isUpdating={props.updatingTaskIds?.has(task.id)}
|
||||||
|
onStatusChange={props.onTaskMove}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @file dashboard/page.tsx
|
* @file dashboard/page.tsx
|
||||||
* @description Server component rendering the main dashboard page, handling authentication, fetching user-related tasks and assignees, and displaying them across kanban columns.
|
* @description Server component rendering the main dashboard page, handling authentication, fetching user-related tasks and assignees, and passing them to the board.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
|
|
@ -8,12 +8,12 @@ import { tasksTable, taskAssigneesTable, usersTable } from "@/db/schema";
|
||||||
import { eq, inArray, or } from "drizzle-orm";
|
import { eq, inArray, or } from "drizzle-orm";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import KanbanColumn from "./KanbanColumn";
|
import KanbanBoard from "./KanbanBoard";
|
||||||
import { KANBAN_COLUMNS, Task } from "@/types/tasks";
|
import { Task } from "@/types/tasks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the dashboard page layout with user session validation, task queries, assignee mapping,
|
* Renders the dashboard page component with user session validation,
|
||||||
* and distributes the tasks into respective kanban columns.
|
* database queries for relevant tasks and team assignees, and passes the structured dataset to the Kanban board container.
|
||||||
*
|
*
|
||||||
* @async
|
* @async
|
||||||
* @returns {Promise<JSX.Element>} The rendered dashboard page component.
|
* @returns {Promise<JSX.Element>} The rendered dashboard page component.
|
||||||
|
|
@ -26,7 +26,6 @@ export default async function Dashboard() {
|
||||||
|
|
||||||
const currentUserId = session.user.id;
|
const currentUserId = session.user.id;
|
||||||
|
|
||||||
// Determine Assigned Task IDs
|
|
||||||
const assignedTaskRows = await db
|
const assignedTaskRows = await db
|
||||||
.select({ taskId: taskAssigneesTable.taskId })
|
.select({ taskId: taskAssigneesTable.taskId })
|
||||||
.from(taskAssigneesTable)
|
.from(taskAssigneesTable)
|
||||||
|
|
@ -34,7 +33,6 @@ export default async function Dashboard() {
|
||||||
|
|
||||||
const assignedTaskIds = assignedTaskRows.map((r) => r.taskId);
|
const assignedTaskIds = assignedTaskRows.map((r) => r.taskId);
|
||||||
|
|
||||||
// Load tasks, including their creators (created by the user OR assigned)
|
|
||||||
const taskWhereClause =
|
const taskWhereClause =
|
||||||
assignedTaskIds.length > 0
|
assignedTaskIds.length > 0
|
||||||
? or(
|
? or(
|
||||||
|
|
@ -52,7 +50,6 @@ export default async function Dashboard() {
|
||||||
.innerJoin(usersTable, eq(tasksTable.userId, usersTable.id))
|
.innerJoin(usersTable, eq(tasksTable.userId, usersTable.id))
|
||||||
.where(taskWhereClause);
|
.where(taskWhereClause);
|
||||||
|
|
||||||
// Load all assignees for the loaded tasks in a batch
|
|
||||||
const allTaskIds = rawTasksWithCreator.map((item) => item.task.id);
|
const allTaskIds = rawTasksWithCreator.map((item) => item.task.id);
|
||||||
|
|
||||||
const assigneesData =
|
const assigneesData =
|
||||||
|
|
@ -67,14 +64,12 @@ export default async function Dashboard() {
|
||||||
.where(inArray(taskAssigneesTable.taskId, allTaskIds))
|
.where(inArray(taskAssigneesTable.taskId, allTaskIds))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Map for high-performance mapping (taskId -> array of emails)
|
|
||||||
const assigneesMap = new Map<string, string[]>();
|
const assigneesMap = new Map<string, string[]>();
|
||||||
for (const row of assigneesData) {
|
for (const row of assigneesData) {
|
||||||
const existing = assigneesMap.get(row.taskId) || [];
|
const existing = assigneesMap.get(row.taskId) || [];
|
||||||
assigneesMap.set(row.taskId, [...existing, row.email]);
|
assigneesMap.set(row.taskId, [...existing, row.email]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preparing Tasks
|
|
||||||
const tasks: Task[] = rawTasksWithCreator.map(({ task, creatorEmail }) => ({
|
const tasks: Task[] = rawTasksWithCreator.map(({ task, creatorEmail }) => ({
|
||||||
...task,
|
...task,
|
||||||
creator: creatorEmail,
|
creator: creatorEmail,
|
||||||
|
|
@ -85,21 +80,7 @@ export default async function Dashboard() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 max-w-7xl mx-auto pb-12">
|
<div className="space-y-8 max-w-7xl mx-auto pb-12">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 items-start">
|
<KanbanBoard tasks={tasks} />
|
||||||
{KANBAN_COLUMNS.map((col) => {
|
|
||||||
const columnTasks = tasks.filter((t) => t.status === col.id);
|
|
||||||
return (
|
|
||||||
<KanbanColumn
|
|
||||||
key={col.id}
|
|
||||||
id={col.id}
|
|
||||||
title={col.title}
|
|
||||||
count={columnTasks.length}
|
|
||||||
tasks={columnTasks}
|
|
||||||
color={col.color}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,13 @@ export interface KanbanColumnConfig {
|
||||||
export interface KanbanColumnProps extends KanbanColumnConfig {
|
export interface KanbanColumnProps extends KanbanColumnConfig {
|
||||||
count: number;
|
count: number;
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
|
updatingTaskIds?: Set<string>;
|
||||||
|
onTaskMove?: (taskId: string, targetStatus: TaskStatus) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KanbanCardProps {
|
export interface KanbanCardProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
|
isUpdating?: boolean;
|
||||||
onStatusChange?: (taskId: string, newStatus: TaskStatus) => void;
|
onStatusChange?: (taskId: string, newStatus: TaskStatus) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue