responsive and move to dnd
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { Draggable, Droppable } from "@hello-pangea/dnd";
|
||||||
import { Add, AddCircle, CloseCircle } from "iconsax-react";
|
import { Add, AddCircle, CloseCircle } from "iconsax-react";
|
||||||
import { useMemo, useRef, useState, type FC } from "react";
|
import { useMemo, useRef, useState, type FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -15,11 +16,7 @@ import Task from "./Task";
|
|||||||
type Props = {
|
type Props = {
|
||||||
column: ColumnType;
|
column: ColumnType;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
draggedTaskId: string | null;
|
|
||||||
draggedColumnId: string | null;
|
draggedColumnId: string | null;
|
||||||
onDragStart: (task: TaskType) => void;
|
|
||||||
onDragEnd: () => void;
|
|
||||||
onDrop: (columnId: string, index: number) => void;
|
|
||||||
onColumnDragStart: (columnId: string) => void;
|
onColumnDragStart: (columnId: string) => void;
|
||||||
onColumnDragEnd: () => void;
|
onColumnDragEnd: () => void;
|
||||||
onColumnDrop: (overColumnId: string) => void;
|
onColumnDrop: (overColumnId: string) => void;
|
||||||
@@ -30,11 +27,7 @@ type Props = {
|
|||||||
const Column: FC<Props> = ({
|
const Column: FC<Props> = ({
|
||||||
column,
|
column,
|
||||||
projectId,
|
projectId,
|
||||||
draggedTaskId,
|
|
||||||
draggedColumnId,
|
draggedColumnId,
|
||||||
onDragStart,
|
|
||||||
onDragEnd,
|
|
||||||
onDrop,
|
|
||||||
onColumnDragStart,
|
onColumnDragStart,
|
||||||
onColumnDragEnd,
|
onColumnDragEnd,
|
||||||
onColumnDrop,
|
onColumnDrop,
|
||||||
@@ -63,7 +56,6 @@ const Column: FC<Props> = ({
|
|||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
|
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
|
||||||
const columnRef = useRef<HTMLDivElement>(null);
|
const columnRef = useRef<HTMLDivElement>(null);
|
||||||
const dragImageRef = useRef<HTMLElement | null>(null);
|
const dragImageRef = useRef<HTMLElement | null>(null);
|
||||||
@@ -94,11 +86,7 @@ const Column: FC<Props> = ({
|
|||||||
dragImageRef.current = clone;
|
dragImageRef.current = clone;
|
||||||
|
|
||||||
const rect = columnEl.getBoundingClientRect();
|
const rect = columnEl.getBoundingClientRect();
|
||||||
e.dataTransfer.setDragImage(
|
e.dataTransfer.setDragImage(clone, e.clientX - rect.left, e.clientY - rect.top);
|
||||||
clone,
|
|
||||||
e.clientX - rect.left,
|
|
||||||
e.clientY - rect.top,
|
|
||||||
);
|
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = "move";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,28 +101,6 @@ const Column: FC<Props> = ({
|
|||||||
onColumnDragEnd();
|
onColumnDragEnd();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
|
||||||
if (draggedColumnId) return;
|
|
||||||
e.preventDefault();
|
|
||||||
e.dataTransfer.dropEffect = "move";
|
|
||||||
setIsDragOver(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent) => {
|
|
||||||
if (draggedColumnId) return;
|
|
||||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
||||||
setIsDragOver(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent, index: number) => {
|
|
||||||
if (draggedColumnId) return;
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
onDrop(column.id, index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColumnDragOver = (e: React.DragEvent) => {
|
const handleColumnDragOver = (e: React.DragEvent) => {
|
||||||
if (!draggedColumnId || draggedColumnId === column.id) return;
|
if (!draggedColumnId || draggedColumnId === column.id) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -204,25 +170,12 @@ const Column: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={columnRef}
|
ref={columnRef}
|
||||||
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col max-h-[calc(100dvh-140px)] sm:max-h-[calc(100dvh-152px)] xl:max-h-[calc(100dvh-220px)] transition-colors ${
|
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col h-full min-h-0 transition-colors ${
|
||||||
isDragOver ? "ring-2 ring-primary/40" : ""
|
isColumnDragOver ? "ring-2 ring-black/30" : ""
|
||||||
} ${isColumnDragOver ? "ring-2 ring-black/30" : ""} ${
|
} ${isDraggingColumn ? "opacity-50" : ""}`}
|
||||||
isDraggingColumn ? "opacity-50" : ""
|
onDragOver={handleColumnDragOver}
|
||||||
}`}
|
onDragLeave={handleColumnDragLeave}
|
||||||
onDragOver={(e) => {
|
onDrop={handleColumnDrop}
|
||||||
handleColumnDragOver(e);
|
|
||||||
handleDragOver(e);
|
|
||||||
}}
|
|
||||||
onDragLeave={(e) => {
|
|
||||||
handleColumnDragLeave(e);
|
|
||||||
handleDragLeave(e);
|
|
||||||
}}
|
|
||||||
onDrop={(e) => {
|
|
||||||
handleColumnDrop(e);
|
|
||||||
if (!draggedColumnId) {
|
|
||||||
handleDrop(e, tasks.length);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center shrink-0 gap-2">
|
<div className="flex justify-between items-center shrink-0 gap-2">
|
||||||
<div
|
<div
|
||||||
@@ -244,35 +197,49 @@ const Column: FC<Props> = ({
|
|||||||
label={t("taskmanager.delete_column_confirm")}
|
label={t("taskmanager.delete_column_confirm")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Droppable droppableId={column.id} type="TASK" isDropDisabled={!!draggedColumnId}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className="mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-0 overflow-y-auto overscroll-y-contain flex flex-col gap-3 sm:gap-4"
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}
|
||||||
onScroll={handleTasksScroll}
|
onScroll={handleTasksScroll}
|
||||||
|
className={`mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-[80px] overflow-y-auto overscroll-y-contain rounded-lg transition-colors ${
|
||||||
|
snapshot.isDraggingOver ? "bg-primary/5 ring-2 ring-primary/30" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{isTasksPending
|
{isTasksPending
|
||||||
? null
|
? null
|
||||||
: tasks.map((task, index) => (
|
: tasks.map((task, index) => (
|
||||||
<div
|
<Draggable
|
||||||
key={task.id}
|
key={task.id}
|
||||||
onDragOver={(e) => {
|
draggableId={task.id}
|
||||||
if (draggedColumnId) return;
|
index={index}
|
||||||
e.preventDefault();
|
isDragDisabled={!!draggedColumnId}
|
||||||
e.stopPropagation();
|
>
|
||||||
}}
|
{(dragProvided, dragSnapshot) => (
|
||||||
onDrop={(e) => handleDrop(e, index)}
|
<div
|
||||||
|
ref={dragProvided.innerRef}
|
||||||
|
{...dragProvided.draggableProps}
|
||||||
|
{...dragProvided.dragHandleProps}
|
||||||
|
className="mb-3 sm:mb-4"
|
||||||
|
style={dragProvided.draggableProps.style}
|
||||||
>
|
>
|
||||||
<Task
|
<Task
|
||||||
task={task}
|
task={task}
|
||||||
isDragging={draggedTaskId === task.id}
|
isDragging={dragSnapshot.isDragging}
|
||||||
onDragStart={onDragStart}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
onClick={onTaskClick}
|
onClick={onTaskClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
))}
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
{isFetchingNextPage ? (
|
{isFetchingNextPage ? (
|
||||||
<div className="text-center text-description text-xs py-2">{t("loading")}</div>
|
<div className="text-center text-description text-xs py-2">{t("loading")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
|
||||||
{isAdding ? (
|
{isAdding ? (
|
||||||
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
||||||
|
|||||||
@@ -1,39 +1,35 @@
|
|||||||
import { AttachCircle, Calendar, TickSquare } from "iconsax-react";
|
import { AttachCircle, Calendar, TickSquare } from "iconsax-react";
|
||||||
import { useRef, type FC } from "react";
|
import { useEffect, useRef, type FC } from "react";
|
||||||
import type { Task as TaskType } from "../types";
|
import type { Task as TaskType } from "../types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
task: TaskType;
|
task: TaskType;
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
onDragStart: (task: TaskType) => void;
|
|
||||||
onDragEnd: () => void;
|
|
||||||
onClick?: (task: TaskType) => void;
|
onClick?: (task: TaskType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Task: FC<Props> = ({ task, isDragging, onDragStart, onDragEnd, onClick }) => {
|
const Task: FC<Props> = ({ task, isDragging, onClick }) => {
|
||||||
const hasDraggedRef = useRef(false);
|
const suppressClickRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
suppressClickRef.current = true;
|
||||||
|
}
|
||||||
|
}, [isDragging]);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (hasDraggedRef.current) return;
|
if (suppressClickRef.current) {
|
||||||
|
suppressClickRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
onClick?.(task);
|
onClick?.(task);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
draggable
|
|
||||||
onDragStart={() => {
|
|
||||||
hasDraggedRef.current = true;
|
|
||||||
onDragStart(task);
|
|
||||||
}}
|
|
||||||
onDragEnd={() => {
|
|
||||||
onDragEnd();
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
hasDraggedRef.current = false;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={`bg-white rounded-lg p-2 cursor-grab active:cursor-grabbing transition-opacity ${
|
className={`bg-white rounded-lg p-2 cursor-grab active:cursor-grabbing transition-opacity touch-manipulation ${
|
||||||
isDragging ? "opacity-40" : ""
|
isDragging ? "opacity-40 shadow-lg" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.color ? <div className="h-10 rounded-lg" style={{ backgroundColor: task.color }} /> : null}
|
{task.color ? <div className="h-10 rounded-lg" style={{ backgroundColor: task.color }} /> : null}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DragDropContext, type DragStart, type DropResult } from "@hello-pangea/dnd";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, useState, type FC } from "react";
|
import { useEffect, useState, type FC } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
@@ -12,6 +13,8 @@ import { useGetProject } from "./project/hooks/useProjectData";
|
|||||||
import { useUpdateTaskPhase } from "./task-phase/hooks/useTaskPhaseData";
|
import { useUpdateTaskPhase } from "./task-phase/hooks/useTaskPhaseData";
|
||||||
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
|
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
|
||||||
import { useChangeTaskPhase, useChangeTaskPriority } from "./task/hooks/useTaskData";
|
import { useChangeTaskPhase, useChangeTaskPriority } from "./task/hooks/useTaskData";
|
||||||
|
import type { TaskItemType } from "./task/types/TaskTypes";
|
||||||
|
import { mapTaskItemToTask } from "./task/utils/mapTaskItemToTask";
|
||||||
import type { Column as ColumnType, Task } from "./types";
|
import type { Column as ColumnType, Task } from "./types";
|
||||||
import { getPriorityDestinationTaskId } from "./utils/getPriorityDestinationTaskId";
|
import { getPriorityDestinationTaskId } from "./utils/getPriorityDestinationTaskId";
|
||||||
import { reorderColumns } from "./utils/reorderColumns";
|
import { reorderColumns } from "./utils/reorderColumns";
|
||||||
@@ -45,22 +48,21 @@ const Workspace: FC = () => {
|
|||||||
);
|
);
|
||||||
}, [project?.data?.taskPhases]);
|
}, [project?.data?.taskPhases]);
|
||||||
|
|
||||||
const handleDragStart = (task: Task) => {
|
const handleTaskDragStart = (start: DragStart) => {
|
||||||
setDraggedTask(task);
|
if (start.type !== "TASK") return;
|
||||||
|
|
||||||
|
const sourceColumnId = start.source.droppableId;
|
||||||
|
const previousData = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", sourceColumnId]);
|
||||||
|
const items = getTaskPhaseItems(previousData);
|
||||||
|
const item = items.find((task) => task.id === start.draggableId);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
setDraggedTask(mapTaskItemToTask(item as TaskItemType, sourceColumnId, start.source.index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
const handleDrop = (columnId: string, index: number, activeTask: Task) => {
|
||||||
setDraggedTask(null);
|
const sourceColumnId = activeTask.columnId;
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (columnId: string, index: number) => {
|
|
||||||
if (!draggedTask) return;
|
|
||||||
|
|
||||||
const sourceColumnId = draggedTask.columnId;
|
|
||||||
const columnChanged = sourceColumnId !== columnId;
|
const columnChanged = sourceColumnId !== columnId;
|
||||||
const activeTask = draggedTask;
|
|
||||||
|
|
||||||
setDraggedTask(null);
|
|
||||||
|
|
||||||
if (!columnChanged) {
|
if (!columnChanged) {
|
||||||
const previousData = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId]);
|
const previousData = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId]);
|
||||||
@@ -69,7 +71,9 @@ const Workspace: FC = () => {
|
|||||||
|
|
||||||
if (!destTaskId) return;
|
if (!destTaskId) return;
|
||||||
|
|
||||||
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) => updateTaskPhaseItems(current, (items) => reorderTaskItems(items, activeTask.id, index)));
|
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) =>
|
||||||
|
updateTaskPhaseItems(current, (items) => reorderTaskItems(items, activeTask.id, index)),
|
||||||
|
);
|
||||||
|
|
||||||
changeTaskPriority.mutate(
|
changeTaskPriority.mutate(
|
||||||
{ srcId: activeTask.id, destId: destTaskId, taskPhaseId: columnId, projectId },
|
{ srcId: activeTask.id, destId: destTaskId, taskPhaseId: columnId, projectId },
|
||||||
@@ -155,6 +159,18 @@ const Workspace: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTaskDragEnd = (result: DropResult) => {
|
||||||
|
const activeTask = draggedTask;
|
||||||
|
setDraggedTask(null);
|
||||||
|
|
||||||
|
if (!activeTask || !result.destination || result.type !== "TASK") return;
|
||||||
|
|
||||||
|
const { source, destination } = result;
|
||||||
|
if (source.droppableId === destination.droppableId && source.index === destination.index) return;
|
||||||
|
|
||||||
|
handleDrop(destination.droppableId, destination.index, activeTask);
|
||||||
|
};
|
||||||
|
|
||||||
const handleTaskPhaseChanged = (taskId: string, taskPhaseId: string) => {
|
const handleTaskPhaseChanged = (taskId: string, taskPhaseId: string) => {
|
||||||
const sourceColumnId = selectedTask?.columnId;
|
const sourceColumnId = selectedTask?.columnId;
|
||||||
|
|
||||||
@@ -216,20 +232,18 @@ const Workspace: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#01347A] h-full rounded-none xl:rounded-[32px] overflow-hidden flex flex-col">
|
<div className="bg-[#01347A] h-full min-h-0 rounded-none xl:rounded-[32px] overflow-hidden flex flex-col">
|
||||||
<HeaderWorkspace projectName={project?.data?.name ?? ""} />
|
<HeaderWorkspace projectName={project?.data?.name ?? ""} />
|
||||||
|
<div className="flex-1 min-h-0 flex flex-col px-3 sm:px-4 xl:px-8 py-4 xl:py-6">
|
||||||
|
<DragDropContext onDragStart={handleTaskDragStart} onDragEnd={handleTaskDragEnd}>
|
||||||
<div className="flex-1 min-h-0 overflow-x-auto overflow-y-hidden overscroll-x-contain">
|
<div className="flex-1 min-h-0 overflow-x-auto overflow-y-hidden overscroll-x-contain">
|
||||||
<div className="flex gap-3 sm:gap-4 xl:gap-7 px-3 sm:px-4 xl:px-8 pt-4 xl:pt-6 pb-4 xl:pb-6 h-full items-start w-max min-w-full">
|
<div className="flex gap-3 sm:gap-4 xl:gap-7 h-full min-h-0 items-stretch w-max min-w-full">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<Column
|
<Column
|
||||||
key={column.id}
|
key={column.id}
|
||||||
column={column}
|
column={column}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
draggedTaskId={draggedTask?.id ?? null}
|
|
||||||
draggedColumnId={draggedColumnId}
|
draggedColumnId={draggedColumnId}
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onColumnDragStart={handleColumnDragStart}
|
onColumnDragStart={handleColumnDragStart}
|
||||||
onColumnDragEnd={handleColumnDragEnd}
|
onColumnDragEnd={handleColumnDragEnd}
|
||||||
onColumnDrop={handleColumnDrop}
|
onColumnDrop={handleColumnDrop}
|
||||||
@@ -245,6 +259,8 @@ const Workspace: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</DragDropContext>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
open={selectedTask !== null}
|
open={selectedTask !== null}
|
||||||
|
|||||||
+2
-2
@@ -114,8 +114,8 @@ const MainRouter: FC = () => {
|
|||||||
{!isWorkspace && <SideBar />}
|
{!isWorkspace && <SideBar />}
|
||||||
<Header />
|
<Header />
|
||||||
<div className={clx("flex-1 mt-[68px] xl:mt-[81px]", !isWorkspace && "xl:ms-[269px]", !isWorkspace && hasSubMenu && "xl:ms-[305px]")}>
|
<div className={clx("flex-1 mt-[68px] xl:mt-[81px]", !isWorkspace && "xl:ms-[269px]", !isWorkspace && hasSubMenu && "xl:ms-[305px]")}>
|
||||||
<div className={clx("flex flex-col", isWorkspace ? "h-[calc(100dvh-68px)] xl:h-[calc(100dvh-97px)] overflow-hidden" : "overflow-auto h-[calc(100vh-113px)]")}>
|
<div className={clx("flex flex-col", isWorkspace ? "h-[calc(100dvh-68px)] xl:h-[calc(100dvh-113px)] overflow-hidden" : "overflow-auto h-[calc(100vh-113px)]")}>
|
||||||
<div className={clx("flex-1 min-h-0", isWorkspace && "overflow-hidden")}>
|
<div className={clx("flex-1 min-h-0", isWorkspace && "overflow-hidden h-full")}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path={Pages.dashboard}
|
path={Pages.dashboard}
|
||||||
|
|||||||
Reference in New Issue
Block a user