mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 22:14:07 +01:00
Connect Kanban with Api (Fetch data) + Save Task Position
This commit is contained in:
parent
7d75c4a453
commit
063e7eda70
@ -1,100 +1,17 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import ColumnContainerCard from "./columnContainerWrapper";
|
import ColumnContainerCard from "./columnContainerWrapper";
|
||||||
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||||
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
|
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import TaskCard from "./taskCard";
|
import TaskCard from "./taskCard";
|
||||||
import { AiOutlinePlusCircle } from "react-icons/ai";
|
import { AiOutlinePlusCircle } from "react-icons/ai";
|
||||||
|
import axiosInstance from "../../api/configs/AxiosConfig";
|
||||||
const defaultCols = [
|
|
||||||
{
|
|
||||||
id: "todo",
|
|
||||||
title: "Todo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "doing",
|
|
||||||
title: "Work in progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "done",
|
|
||||||
title: "Done",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultTasks = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
columnId: "todo",
|
|
||||||
content: "List admin APIs for dashboard",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
columnId: "todo",
|
|
||||||
content:
|
|
||||||
"Develop user registration functionality with OTP delivered on SMS after email confirmation and phone number confirmation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
columnId: "doing",
|
|
||||||
content: "Conduct security testing",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
columnId: "doing",
|
|
||||||
content: "Analyze competitors",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
columnId: "done",
|
|
||||||
content: "Create UI kit documentation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "6",
|
|
||||||
columnId: "done",
|
|
||||||
content: "Dev meeting",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "7",
|
|
||||||
columnId: "done",
|
|
||||||
content: "Deliver dashboard prototype",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "8",
|
|
||||||
columnId: "todo",
|
|
||||||
content: "Optimize application performance",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "9",
|
|
||||||
columnId: "todo",
|
|
||||||
content: "Implement data validation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "10",
|
|
||||||
columnId: "todo",
|
|
||||||
content: "Design database schema",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "11",
|
|
||||||
columnId: "todo",
|
|
||||||
content: "Integrate SSL web certificates into workflow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "12",
|
|
||||||
columnId: "doing",
|
|
||||||
content: "Implement error logging and monitoring",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "13",
|
|
||||||
columnId: "doing",
|
|
||||||
content: "Design and implement responsive UI",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function KanbanBoard() {
|
function KanbanBoard() {
|
||||||
const [columns, setColumns] = useState(defaultCols);
|
const [columns, setColumns] = useState([]);
|
||||||
const columnsId = useMemo(() => columns.map(col => col.id), [columns]);
|
const columnsId = useMemo(() => columns.map(col => col.id), [columns]);
|
||||||
|
|
||||||
const [tasks, setTasks] = useState(defaultTasks);
|
const [tasks, setTasks] = useState([]);
|
||||||
|
|
||||||
const [activeColumn, setActiveColumn] = useState(null);
|
const [activeColumn, setActiveColumn] = useState(null);
|
||||||
|
|
||||||
@ -108,16 +25,97 @@ function KanbanBoard() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Example
|
||||||
|
// {
|
||||||
|
// "id": 95,
|
||||||
|
// "title": "Test Todo",
|
||||||
|
// "notes": "Test TodoTest TodoTest Todo",
|
||||||
|
// "importance": 1,
|
||||||
|
// "difficulty": 1,
|
||||||
|
// "challenge": false,
|
||||||
|
// "fromSystem": false,
|
||||||
|
// "creation_date": "2023-11-20T19:50:16.369308Z",
|
||||||
|
// "last_update": "2023-11-20T19:50:16.369308Z",
|
||||||
|
// "is_active": true,
|
||||||
|
// "is_full_day_event": false,
|
||||||
|
// "start_event": "2023-11-20T19:49:49Z",
|
||||||
|
// "end_event": "2023-11-23T18:00:00Z",
|
||||||
|
// "google_calendar_id": null,
|
||||||
|
// "completed": true,
|
||||||
|
// "completion_date": "2023-11-20T19:50:16.369308Z",
|
||||||
|
// "priority": 3,
|
||||||
|
// "user": 1,
|
||||||
|
// "list_board": 1,
|
||||||
|
// "tags": []
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "id": 8,
|
||||||
|
// "name": "test",
|
||||||
|
// "position": 2,
|
||||||
|
// "board": 3
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const tasksResponse = await axiosInstance.get("/todo");
|
||||||
|
|
||||||
|
// Transform
|
||||||
|
const transformedTasks = tasksResponse.data.map(task => ({
|
||||||
|
id: task.id,
|
||||||
|
columnId: task.list_board,
|
||||||
|
content: task.title,
|
||||||
|
difficulty: task.difficulty,
|
||||||
|
notes: task.notes,
|
||||||
|
importance: task.importance,
|
||||||
|
difficulty: task.difficulty,
|
||||||
|
challenge: task.challenge,
|
||||||
|
fromSystem: task.fromSystem,
|
||||||
|
creation_date: task.creation_date,
|
||||||
|
last_update: task.last_update,
|
||||||
|
is_active: task.is_active,
|
||||||
|
is_full_day_event: task.is_full_day_event,
|
||||||
|
start_event: task.start_event,
|
||||||
|
end_event: task.end_event,
|
||||||
|
google_calendar_id: task.google_calendar_id,
|
||||||
|
completed: task.completed,
|
||||||
|
completion_date: task.completion_date,
|
||||||
|
priority: task.priority,
|
||||||
|
user: task.user,
|
||||||
|
list_board: task.list_board,
|
||||||
|
tags: task.tags,
|
||||||
|
}));
|
||||||
|
setTasks(transformedTasks);
|
||||||
|
|
||||||
|
const columnsResponse = await axiosInstance.get("/lists");
|
||||||
|
|
||||||
|
// Transform
|
||||||
|
const transformedColumns = columnsResponse.data.map(column => ({
|
||||||
|
id: column.id,
|
||||||
|
title: column.name,
|
||||||
|
}));
|
||||||
|
setColumns(transformedColumns);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data from API:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
m-auto
|
m-auto
|
||||||
flex
|
flex
|
||||||
w-full
|
w-full
|
||||||
items-center
|
items-center
|
||||||
overflow-x-auto
|
overflow-x-auto
|
||||||
overflow-y-hidden
|
overflow-y-hidden
|
||||||
">
|
">
|
||||||
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
|
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
|
||||||
<div className="ml-2 flex gap-4">
|
<div className="ml-2 flex gap-4">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@ -136,26 +134,26 @@ function KanbanBoard() {
|
|||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</div>
|
</div>
|
||||||
{/* create new column */}
|
{/* create new column */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createNewColumn();
|
createNewColumn();
|
||||||
}}
|
}}
|
||||||
className="
|
className="
|
||||||
h-[60px]
|
h-[60px]
|
||||||
w-[268px]
|
w-[268px]
|
||||||
max-w-[268px]
|
max-w-[268px]
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
rounded-xl
|
rounded-xl
|
||||||
bg-[#f1f2f4]
|
bg-[#f1f2f4]
|
||||||
border-2
|
border-2
|
||||||
p-4
|
p-4
|
||||||
hover:bg-gray-200
|
hover:bg-gray-200
|
||||||
flex
|
flex
|
||||||
gap-2
|
gap-2
|
||||||
my-2
|
my-2
|
||||||
bg-opacity-60
|
bg-opacity-60
|
||||||
">
|
">
|
||||||
<div className="my-1">
|
<div className="my-1">
|
||||||
<AiOutlinePlusCircle />
|
<AiOutlinePlusCircle />
|
||||||
</div>
|
</div>
|
||||||
@ -256,18 +254,75 @@ function KanbanBoard() {
|
|||||||
const activeId = active.id;
|
const activeId = active.id;
|
||||||
const overId = over.id;
|
const overId = over.id;
|
||||||
|
|
||||||
if (activeId === overId) return;
|
|
||||||
|
|
||||||
const isActiveAColumn = active.data.current?.type === "Column";
|
const isActiveAColumn = active.data.current?.type === "Column";
|
||||||
if (!isActiveAColumn) return;
|
const isActiveATask = active.data.current?.type === "Task";
|
||||||
|
const isOverAColumn = over.data.current?.type === "Column";
|
||||||
|
const isOverATask = over.data.current?.type === "Task";
|
||||||
|
|
||||||
setColumns(columns => {
|
// Reorder columns if the dragged item is a column
|
||||||
const activeColumnIndex = columns.findIndex(col => col.id === activeId);
|
if (isActiveAColumn && isOverAColumn) {
|
||||||
|
setColumns(columns => {
|
||||||
|
const activeColumnIndex = columns.findIndex(col => col.id === activeId);
|
||||||
|
const overColumnIndex = columns.findIndex(col => col.id === overId);
|
||||||
|
|
||||||
const overColumnIndex = columns.findIndex(col => col.id === overId);
|
const reorderedColumns = arrayMove(columns, activeColumnIndex, overColumnIndex);
|
||||||
|
|
||||||
return arrayMove(columns, activeColumnIndex, overColumnIndex);
|
axiosInstance
|
||||||
});
|
.put("todo/change_task_list_board/", { columns: reorderedColumns })
|
||||||
|
.then(response => {
|
||||||
|
// Successful handle
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error updating column order:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return reorderedColumns;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder tasks within the same column
|
||||||
|
if (isActiveATask && isOverATask) {
|
||||||
|
setTasks(tasks => {
|
||||||
|
const activeIndex = tasks.findIndex(t => t.id === activeId);
|
||||||
|
const overIndex = tasks.findIndex(t => t.id === overId);
|
||||||
|
|
||||||
|
const reorderedTasks = arrayMove(tasks, activeIndex, overIndex);
|
||||||
|
|
||||||
|
return reorderedTasks;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move tasks between columns and update columnId
|
||||||
|
if (isActiveATask && isOverAColumn) {
|
||||||
|
setTasks(tasks => {
|
||||||
|
const activeIndex = tasks.findIndex(t => t.id === activeId);
|
||||||
|
const overIndex = tasks.findIndex(t => t.id === overId);
|
||||||
|
|
||||||
|
const newColumnId = overId;
|
||||||
|
const new_index = event.over?.index;
|
||||||
|
|
||||||
|
if (newColumnId != tasks[activeIndex].columnId) {
|
||||||
|
// Update the columnId of the task
|
||||||
|
tasks[activeIndex].columnId = newColumnId;
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.put(`todo/change_task_order/`, { activeId, newColumnId, new_index })
|
||||||
|
.then(response => {
|
||||||
|
// Successful update handle
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error updating task columnId and index:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If new_index is not provided, insert the task at the end
|
||||||
|
if (new_index !== null && 0 <= new_index && new_index <= tasks.length) {
|
||||||
|
return arrayMove(tasks, activeIndex, new_index);
|
||||||
|
} else {
|
||||||
|
return arrayMove(tasks, activeIndex, tasks.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragOver(event) {
|
function onDragOver(event) {
|
||||||
|
|||||||
@ -29,6 +29,8 @@ const KanbanPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<KanbanBoard />
|
<KanbanBoard />
|
||||||
|
<div className="flex justify-center border-2 ">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useSortable } from "@dnd-kit/sortable";
|
|||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import TaskDetailModal from "./taskDetailModal";
|
import TaskDetailModal from "./taskDetailModal";
|
||||||
|
|
||||||
function TaskCard({ task, deleteTask, updateTask }) {
|
function TaskCard({ task, deleteTask, updateTask, description, tags, difficulty, challenge, importance}) {
|
||||||
const [mouseIsOver, setMouseIsOver] = useState(false);
|
const [mouseIsOver, setMouseIsOver] = useState(false);
|
||||||
|
|
||||||
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
||||||
@ -15,6 +15,7 @@ function TaskCard({ task, deleteTask, updateTask }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transition,
|
transition,
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
@ -38,7 +39,14 @@ function TaskCard({ task, deleteTask, updateTask }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TaskDetailModal />
|
<TaskDetailModal
|
||||||
|
title={task.content}
|
||||||
|
description={task.description}
|
||||||
|
tags={task.tags}
|
||||||
|
difficulty={task.difficulty}
|
||||||
|
challenge={task.challenge}
|
||||||
|
importance={task.importance}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { FaTasks, FaRegListAlt } from "react-icons/fa";
|
|||||||
import { FaPlus } from "react-icons/fa6";
|
import { FaPlus } from "react-icons/fa6";
|
||||||
import { TbChecklist } from "react-icons/tb";
|
import { TbChecklist } from "react-icons/tb";
|
||||||
|
|
||||||
function TaskDetailModal() {
|
function TaskDetailModal({ title, description, tags, difficulty, challenge, importance }) {
|
||||||
const [difficulty, setDifficulty] = useState(50);
|
const [isChallengeChecked, setChallengeChecked] = useState(challenge);
|
||||||
const [isChallengeChecked, setChallengeChecked] = useState(true);
|
const [isImportantChecked, setImportantChecked] = useState(importance);
|
||||||
const [isImportantChecked, setImportantChecked] = useState(true);
|
const [currentDifficulty, setCurrentDifficulty] = useState(difficulty);
|
||||||
|
|
||||||
const handleChallengeChange = () => {
|
const handleChallengeChange = () => {
|
||||||
setChallengeChecked(!isChallengeChecked);
|
setChallengeChecked(!isChallengeChecked);
|
||||||
@ -15,8 +15,9 @@ function TaskDetailModal() {
|
|||||||
const handleImportantChange = () => {
|
const handleImportantChange = () => {
|
||||||
setImportantChecked(!isImportantChecked);
|
setImportantChecked(!isImportantChecked);
|
||||||
};
|
};
|
||||||
const handleDifficultyChange = event => {
|
|
||||||
setDifficulty(parseInt(event.target.value, 10));
|
const handleDifficultyChange = (event) => {
|
||||||
|
setCurrentDifficulty(parseInt(event.target.value, 10));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -26,9 +27,11 @@ function TaskDetailModal() {
|
|||||||
<div className="flex flex-col py-2">
|
<div className="flex flex-col py-2">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h3 className="font-bold text-lg">
|
<h3 className="font-bold text-lg">
|
||||||
<span className="flex gap-2">{<FaTasks className="my-2" />}Title</span>
|
<span className="flex gap-2">
|
||||||
|
{<FaTasks className="my-2" />}{title}
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs">Todo List</p>
|
<p className="text-xs">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -42,25 +45,13 @@ function TaskDetailModal() {
|
|||||||
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
<li>
|
<li>
|
||||||
<a>
|
<a>
|
||||||
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
|
<input type="checkbox" checked="checked" className="checkbox checkbox-sm"/>
|
||||||
Item 2
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a>
|
|
||||||
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
|
|
||||||
Item 2
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a>
|
|
||||||
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
|
|
||||||
Item 2
|
Item 2
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-nowrap overflow-x-auto"></div>
|
<div className="flex flex-nowrap overflow-x-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -72,10 +63,12 @@ function TaskDetailModal() {
|
|||||||
Description
|
Description
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<textarea className="textarea w-full" disabled></textarea>
|
<textarea className="textarea w-full" disabled>
|
||||||
|
{description}
|
||||||
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Difficulty, Challenge and Importance */}
|
{/* Difficulty, Challenge, and Importance */}
|
||||||
<div className="flex flex-row space-x-3 my-4">
|
<div className="flex flex-row space-x-3 my-4">
|
||||||
<div className="flex-1 card shadow border-2 p-2">
|
<div className="flex-1 card shadow border-2 p-2">
|
||||||
<input
|
<input
|
||||||
@ -83,7 +76,7 @@ function TaskDetailModal() {
|
|||||||
id="difficultySelector"
|
id="difficultySelector"
|
||||||
min={0}
|
min={0}
|
||||||
max="100"
|
max="100"
|
||||||
value={difficulty}
|
value={currentDifficulty}
|
||||||
className="range"
|
className="range"
|
||||||
step="25"
|
step="25"
|
||||||
onChange={handleDifficultyChange}
|
onChange={handleDifficultyChange}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user