Connect Kanban with Api (Fetch data) + Save Task Position

This commit is contained in:
sosokker 2023-11-21 04:50:22 +07:00
parent 7d75c4a453
commit 063e7eda70
4 changed files with 202 additions and 144 deletions

View File

@ -1,100 +1,17 @@
import { useMemo, useState } from "react";
import { useMemo, useState, useEffect } from "react";
import ColumnContainerCard from "./columnContainerWrapper";
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
import { createPortal } from "react-dom";
import TaskCard from "./taskCard";
import { AiOutlinePlusCircle } from "react-icons/ai";
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",
},
];
import axiosInstance from "../../api/configs/AxiosConfig";
function KanbanBoard() {
const [columns, setColumns] = useState(defaultCols);
const [columns, setColumns] = useState([]);
const columnsId = useMemo(() => columns.map(col => col.id), [columns]);
const [tasks, setTasks] = useState(defaultTasks);
const [tasks, setTasks] = useState([]);
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 (
<div
className="
m-auto
flex
w-full
items-center
overflow-x-auto
overflow-y-hidden
">
m-auto
flex
w-full
items-center
overflow-x-auto
overflow-y-hidden
">
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
<div className="ml-2 flex gap-4">
<div className="flex gap-4">
@ -136,26 +134,26 @@ function KanbanBoard() {
))}
</SortableContext>
</div>
{/* create new column */}
{/* create new column */}
<button
onClick={() => {
createNewColumn();
}}
className="
h-[60px]
w-[268px]
max-w-[268px]
cursor-pointer
rounded-xl
bg-[#f1f2f4]
border-2
p-4
hover:bg-gray-200
flex
gap-2
my-2
bg-opacity-60
">
h-[60px]
w-[268px]
max-w-[268px]
cursor-pointer
rounded-xl
bg-[#f1f2f4]
border-2
p-4
hover:bg-gray-200
flex
gap-2
my-2
bg-opacity-60
">
<div className="my-1">
<AiOutlinePlusCircle />
</div>
@ -256,18 +254,75 @@ function KanbanBoard() {
const activeId = active.id;
const overId = over.id;
if (activeId === overId) return;
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 => {
const activeColumnIndex = columns.findIndex(col => col.id === activeId);
// Reorder columns if the dragged item is a column
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) {

View File

@ -29,6 +29,8 @@ const KanbanPage = () => {
</div>
</div>
<KanbanBoard />
<div className="flex justify-center border-2 ">
</div>
</div>
);
};

View File

@ -4,7 +4,7 @@ import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
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 { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
@ -15,6 +15,7 @@ function TaskCard({ task, deleteTask, updateTask }) {
},
});
const style = {
transition,
transform: CSS.Transform.toString(transform),
@ -38,7 +39,14 @@ function TaskCard({ task, deleteTask, updateTask }) {
return (
<div>
<TaskDetailModal />
<TaskDetailModal
title={task.content}
description={task.description}
tags={task.tags}
difficulty={task.difficulty}
challenge={task.challenge}
importance={task.importance}
/>
<div
ref={setNodeRef}
{...attributes}

View File

@ -3,10 +3,10 @@ import { FaTasks, FaRegListAlt } from "react-icons/fa";
import { FaPlus } from "react-icons/fa6";
import { TbChecklist } from "react-icons/tb";
function TaskDetailModal() {
const [difficulty, setDifficulty] = useState(50);
const [isChallengeChecked, setChallengeChecked] = useState(true);
const [isImportantChecked, setImportantChecked] = useState(true);
function TaskDetailModal({ title, description, tags, difficulty, challenge, importance }) {
const [isChallengeChecked, setChallengeChecked] = useState(challenge);
const [isImportantChecked, setImportantChecked] = useState(importance);
const [currentDifficulty, setCurrentDifficulty] = useState(difficulty);
const handleChallengeChange = () => {
setChallengeChecked(!isChallengeChecked);
@ -15,8 +15,9 @@ function TaskDetailModal() {
const handleImportantChange = () => {
setImportantChecked(!isImportantChecked);
};
const handleDifficultyChange = event => {
setDifficulty(parseInt(event.target.value, 10));
const handleDifficultyChange = (event) => {
setCurrentDifficulty(parseInt(event.target.value, 10));
};
return (
@ -26,9 +27,11 @@ function TaskDetailModal() {
<div className="flex flex-col py-2">
<div className="flex flex-col">
<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>
<p className="text-xs">Todo List</p>
<p className="text-xs">{title}</p>
</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">
<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
</a>
</li>
<li>
<a>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
<input type="checkbox" checked="checked" className="checkbox checkbox-sm"/>
Item 2
</a>
</li>
</ul>
</div>
</div>
</div>
<div className="flex flex-nowrap overflow-x-auto"></div>
</div>
@ -72,10 +63,12 @@ function TaskDetailModal() {
Description
</span>
</h2>
<textarea className="textarea w-full" disabled></textarea>
<textarea className="textarea w-full" disabled>
{description}
</textarea>
</div>
{/* Difficulty, Challenge and Importance */}
{/* Difficulty, Challenge, and Importance */}
<div className="flex flex-row space-x-3 my-4">
<div className="flex-1 card shadow border-2 p-2">
<input
@ -83,7 +76,7 @@ function TaskDetailModal() {
id="difficultySelector"
min={0}
max="100"
value={difficulty}
value={currentDifficulty}
className="range"
step="25"
onChange={handleDifficultyChange}