feat: Реализован drag-and-drop на странице канбан доски
- часть c задачами реализована с помощью websocket, а категории временно нет
This commit is contained in:
@@ -119,7 +119,9 @@ export const useBoardLogic = (id, setError, setInfo, setCategories, setLoading,
|
|||||||
}
|
}
|
||||||
await updateTaskAPI(editedTaskId, taskTitle, taskDescription, taskPosition, taskDeadline, taskCategory);
|
await updateTaskAPI(editedTaskId, taskTitle, taskDescription, taskPosition, taskDeadline, taskCategory);
|
||||||
await loadBoardData();
|
await loadBoardData();
|
||||||
|
if(modalEditTask !== null) {
|
||||||
modalEditTask({}, null)();
|
modalEditTask({}, null)();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.message || 'Ошибка редактирования задачи');
|
setError(err.response?.data?.message || 'Ошибка редактирования задачи');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const KBBoard = () => {
|
|||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [isOwner, setIsOwner] = useState(null);
|
const [isOwner, setIsOwner] = useState(null);
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
|
const [draggedItem, setDraggedItem] = useState(null);
|
||||||
|
const [draggedType, setDraggedType] = useState(null);
|
||||||
|
|
||||||
const [memList, setMemList] = useState(false);
|
const [memList, setMemList] = useState(false);
|
||||||
const [crTask, setCrTask] = useState(false);
|
const [crTask, setCrTask] = useState(false);
|
||||||
@@ -36,12 +38,11 @@ const KBBoard = () => {
|
|||||||
const [taskDescription, setTaskDescription] = useState('');
|
const [taskDescription, setTaskDescription] = useState('');
|
||||||
const [taskCategory, setTaskCategory] = useState(null);
|
const [taskCategory, setTaskCategory] = useState(null);
|
||||||
const [taskCategori, setTaskCategori] = useState(null);
|
const [taskCategori, setTaskCategori] = useState(null);
|
||||||
const [taskDeadline, setTaskDeadline] = useState('');
|
const [taskDeadline, setTaskDeadline] = useState(null);
|
||||||
const [assignedMembers, setAssignedMembers] = useState(null);
|
const [assignedMembers, setAssignedMembers] = useState(null);
|
||||||
const [editedTask, setEditedTask] = useState({});
|
const [editedTask, setEditedTask] = useState({});
|
||||||
const [editedCateg, setEditedCateg] = useState({});
|
const [editedCateg, setEditedCateg] = useState({});
|
||||||
const [taskPosition, setTaskPosition] = useState(null);
|
const [taskPosition, setTaskPosition] = useState(null);
|
||||||
const [categoryPosition, setCategoryPosition] = useState(null);
|
|
||||||
const [assignedMember, setAssignedMember] = useState(0);
|
const [assignedMember, setAssignedMember] = useState(0);
|
||||||
const [addedUsername, setAddedUsername] = useState('');
|
const [addedUsername, setAddedUsername] = useState('');
|
||||||
const [deletedMember, setDeletedMember] = useState('');
|
const [deletedMember, setDeletedMember] = useState('');
|
||||||
@@ -78,6 +79,24 @@ const KBBoard = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const [socket, setSocket] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const ws = new WebSocket(`${protocol}//https://back.fool-stack.ru/api/boards/ws/`+id);
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket соединение установлено');
|
||||||
|
setSocket(ws);
|
||||||
|
};
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
loadBoardData();
|
||||||
|
};
|
||||||
|
ws.onclose = () => console.log('WebSocket соединение закрыто');
|
||||||
|
ws.onerror = (error) => console.error('Ошибка WebSocket:', error);
|
||||||
|
return () => ws.close();
|
||||||
|
}, [id, loadBoardData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) loadBoardData();
|
if (id) loadBoardData();
|
||||||
}, [id, loadBoardData]);
|
}, [id, loadBoardData]);
|
||||||
@@ -94,7 +113,7 @@ const KBBoard = () => {
|
|||||||
setTaskCategori(categori);
|
setTaskCategori(categori);
|
||||||
setTaskTitle('');
|
setTaskTitle('');
|
||||||
setTaskDescription('');
|
setTaskDescription('');
|
||||||
setTaskDeadline('')
|
setTaskDeadline(null)
|
||||||
}
|
}
|
||||||
const modalCrCateg = () => {
|
const modalCrCateg = () => {
|
||||||
setCrCateg(!crCateg);
|
setCrCateg(!crCateg);
|
||||||
@@ -119,7 +138,6 @@ const KBBoard = () => {
|
|||||||
setEdCateg(!edCateg);
|
setEdCateg(!edCateg);
|
||||||
setEditedCateg(categ);
|
setEditedCateg(categ);
|
||||||
setCategoryTitle(categ.title);
|
setCategoryTitle(categ.title);
|
||||||
setCategoryPosition(categ.position);
|
|
||||||
}
|
}
|
||||||
const modalDelTask = () => {
|
const modalDelTask = () => {
|
||||||
setDelTask(!delTask);
|
setDelTask(!delTask);
|
||||||
@@ -194,6 +212,137 @@ const KBBoard = () => {
|
|||||||
await deleteMember(id, deletedMember, modalDeleteMember);
|
await deleteMember(id, deletedMember, modalDeleteMember);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCategoryDragStart = (e, category) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDraggedType('c');
|
||||||
|
setDraggedItem(category);
|
||||||
|
e.dataTransfer.setData('text/plain', JSON.stringify({
|
||||||
|
id: category.id,
|
||||||
|
type: 'c',
|
||||||
|
position: category.position
|
||||||
|
}));
|
||||||
|
e.currentTarget.style.opacity = '0.5';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTaskDragStart = (e, task, categoryId) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDraggedType('t');
|
||||||
|
setDraggedItem({ ...task, categoryId });
|
||||||
|
e.dataTransfer.setData('text/plain', JSON.stringify({
|
||||||
|
id: task.id,
|
||||||
|
type: 't',
|
||||||
|
position: task.position,
|
||||||
|
categoryId: categoryId
|
||||||
|
}));
|
||||||
|
e.currentTarget.style.opacity = '0.5';
|
||||||
|
e.currentTarget.style.transform = 'scale(0.95)';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleDragEnd = (e) => {
|
||||||
|
setDraggedType(null);
|
||||||
|
setDraggedItem(null);
|
||||||
|
e.currentTarget.style.opacity = '1';
|
||||||
|
e.currentTarget.style.transform = 'scale(1)';
|
||||||
|
};
|
||||||
|
const handleDragOver = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = e.dataTransfer.getData('text/plain');
|
||||||
|
if (!data) return;
|
||||||
|
try {
|
||||||
|
const draggedData = JSON.parse(data);
|
||||||
|
if (draggedData.type === 't') {
|
||||||
|
e.currentTarget.style.boxShadow = '0 0 10px rgba(0, 0, 255, 0.3)';
|
||||||
|
e.currentTarget.style.border = '2px dashed #007bff';
|
||||||
|
} else if (draggedData.type === 'c') {
|
||||||
|
e.currentTarget.style.backgroundColor = '#f0f0f0';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка парсинга данных drag:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleDragLeave = (e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '';
|
||||||
|
e.currentTarget.style.boxShadow = '';
|
||||||
|
e.currentTarget.style.border = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCategoryReorder = async (e, targetPosition) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.style.boxShadow = '';
|
||||||
|
e.currentTarget.style.border = '';
|
||||||
|
e.currentTarget.style.backgroundColor = '';
|
||||||
|
e.currentTarget.style.transform = '';
|
||||||
|
|
||||||
|
const data = e.dataTransfer.getData('text/plain');
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const draggedData = JSON.parse(data);
|
||||||
|
if (draggedData.type !== 'c') return;
|
||||||
|
|
||||||
|
if (draggedType === 'c' && draggedItem.position !== targetPosition) {
|
||||||
|
setCategories((prevItems) => {
|
||||||
|
const draggedIndex = prevItems.findIndex(item => item.position === draggedItem.position);
|
||||||
|
const targetIndex = prevItems.findIndex(item => item.position === targetPosition);
|
||||||
|
const newItems = [...prevItems];
|
||||||
|
newItems.splice(draggedIndex, 1);
|
||||||
|
newItems.splice(targetIndex, 0, draggedItem);
|
||||||
|
const updatedItems = newItems.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
position: index
|
||||||
|
}));
|
||||||
|
return updatedItems;
|
||||||
|
});
|
||||||
|
await editCategoryPosition(draggedItem.id, targetPosition);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка при переупорядочивании категорий:', err);
|
||||||
|
setError('Не удалось переупорядочить категории');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleTaskReorder = async (e, targetPosition, targetCategoryPos) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.style.boxShadow = '';
|
||||||
|
e.currentTarget.style.border = '';
|
||||||
|
e.currentTarget.style.backgroundColor = '';
|
||||||
|
e.currentTarget.style.transform = '';
|
||||||
|
|
||||||
|
const data = e.dataTransfer.getData('text/plain');
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
if (!draggedItem || draggedType !== 't') return;
|
||||||
|
try {
|
||||||
|
const draggedData = JSON.parse(data);
|
||||||
|
|
||||||
|
// Обрабатываем только задачи
|
||||||
|
if (draggedData.type !== 't' || !draggedItem) return;
|
||||||
|
|
||||||
|
// Находим целевую категорию по позиции
|
||||||
|
const targetCat = categories.find(cat => cat.position === targetCategoryPos);
|
||||||
|
if (!targetCat) return;
|
||||||
|
|
||||||
|
// Проверяем, что задача не перемещается в ту же позицию той же категории
|
||||||
|
if ((draggedItem.position === targetPosition ||
|
||||||
|
(draggedItem.position === targetCat.length-1 && targetPosition === targetCat.length)) &&
|
||||||
|
draggedItem.category_id === targetCat.id) return;
|
||||||
|
|
||||||
|
|
||||||
|
await editTask( draggedItem.id, draggedItem.title,
|
||||||
|
draggedItem.description, targetPosition,
|
||||||
|
draggedItem.deadline, targetCat.id, null );
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка при переупорядочивании задач:', err);
|
||||||
|
setError('Не удалось переупорядочить задачи');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
@@ -260,12 +409,16 @@ const KBBoard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="board-panel" >
|
<div className="board-panel" >
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div className="categori" key={category.position}>
|
<div className="categori" key={category.position}
|
||||||
|
draggable="true"
|
||||||
|
|
||||||
|
onDragStart={(e) => handleCategoryDragStart(e, category)}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={(e) => handleCategoryReorder(e, category.position)}
|
||||||
|
>
|
||||||
<button onClick={modalEditCateg(category)}><h3>{category.title}</h3></button>
|
<button onClick={modalEditCateg(category)}><h3>{category.title}</h3></button>
|
||||||
<div className="categ-h">
|
|
||||||
<p>Позиция: {category.position}</p>
|
|
||||||
<p>Задач: {category.tasks.length}</p>
|
|
||||||
</div>
|
|
||||||
<div className='task create'>
|
<div className='task create'>
|
||||||
<button onClick={modalCrTask(category.id)}>
|
<button onClick={modalCrTask(category.id)}>
|
||||||
Новая задача
|
Новая задача
|
||||||
@@ -274,7 +427,15 @@ const KBBoard = () => {
|
|||||||
<div className='task-list'>
|
<div className='task-list'>
|
||||||
{category.tasks.length > 0 ? (
|
{category.tasks.length > 0 ? (
|
||||||
category.tasks.map((task) => (
|
category.tasks.map((task) => (
|
||||||
<button className='task' onClick={modalEditTask(task, category.id)} key={task.id}>
|
<button className='task' onClick={modalEditTask(task, category.id)} key={task.position}
|
||||||
|
draggable="true"
|
||||||
|
onDragStart={(e) => handleTaskDragStart(e, task, category.position)}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={(e) => handleTaskReorder(e, task.position, category.position)}
|
||||||
|
|
||||||
|
>
|
||||||
<div>{task.title}</div>
|
<div>{task.title}</div>
|
||||||
<div>{task.description} </div>
|
<div>{task.description} </div>
|
||||||
{task.deadline && (
|
{task.deadline && (
|
||||||
@@ -299,6 +460,12 @@ const KBBoard = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p>Нет задач</p>
|
<p>Нет задач</p>
|
||||||
)}
|
)}
|
||||||
|
<div className='task move'
|
||||||
|
onDrop={(e) => handleTaskReorder(e, category.tasks.length, category.position)}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -260,6 +260,14 @@
|
|||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task.move {
|
||||||
|
min-height: 24px;
|
||||||
|
margin-top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module.exports = function(app) {
|
|||||||
'/api',
|
'/api',
|
||||||
createProxyMiddleware({
|
createProxyMiddleware({
|
||||||
target: 'https://back.fool-stack.ru',
|
target: 'https://back.fool-stack.ru',
|
||||||
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
pathRewrite: { '^/api': '/api' },
|
pathRewrite: { '^/api': '/api' },
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user