Merge pull request #274 from shancds/test/kanban-order-v1.2.2
Test/kanban order v1.2.2
This commit is contained in:
@@ -10,6 +10,17 @@
|
|||||||
"deleteConfirmationOk": "Po",
|
"deleteConfirmationOk": "Po",
|
||||||
"deleteConfirmationCancel": "Anulo",
|
"deleteConfirmationCancel": "Anulo",
|
||||||
|
|
||||||
|
"deleteTaskTitle": "Fshi Detyrën",
|
||||||
|
"deleteTaskContent": "Jeni i sigurt që doni të fshini këtë detyrë? Kjo veprim nuk mund të zhbëhet.",
|
||||||
|
"deleteTaskConfirm": "Fshi",
|
||||||
|
"deleteTaskCancel": "Anulo",
|
||||||
|
|
||||||
|
"deleteStatusTitle": "Fshi Statusin",
|
||||||
|
"deleteStatusContent": "Jeni i sigurt që doni të fshini këtë status? Kjo veprim nuk mund të zhbëhet.",
|
||||||
|
|
||||||
|
"deletePhaseTitle": "Fshi Fazen",
|
||||||
|
"deletePhaseContent": "Jeni i sigurt që doni të fshini këtë fazë? Kjo veprim nuk mund të zhbëhet.",
|
||||||
|
|
||||||
"dueDate": "Data e përfundimit",
|
"dueDate": "Data e përfundimit",
|
||||||
"cancel": "Anulo",
|
"cancel": "Anulo",
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
"deleteConfirmationOk": "Ja",
|
"deleteConfirmationOk": "Ja",
|
||||||
"deleteConfirmationCancel": "Abbrechen",
|
"deleteConfirmationCancel": "Abbrechen",
|
||||||
|
|
||||||
|
"deleteTaskTitle": "Aufgabe löschen",
|
||||||
|
"deleteTaskContent": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"deleteTaskConfirm": "Löschen",
|
||||||
|
"deleteTaskCancel": "Abbrechen",
|
||||||
|
|
||||||
|
"deleteStatusTitle": "Status löschen",
|
||||||
|
"deleteStatusContent": "Sind Sie sicher, dass Sie diesen Status löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
|
||||||
|
"deletePhaseTitle": "Phase löschen",
|
||||||
|
"deletePhaseContent": "Sind Sie sicher, dass Sie diese Phase löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
|
||||||
"dueDate": "Fälligkeitsdatum",
|
"dueDate": "Fälligkeitsdatum",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
"deleteConfirmationOk": "Yes",
|
"deleteConfirmationOk": "Yes",
|
||||||
"deleteConfirmationCancel": "Cancel",
|
"deleteConfirmationCancel": "Cancel",
|
||||||
|
|
||||||
|
"deleteTaskTitle": "Delete Task",
|
||||||
|
"deleteTaskContent": "Are you sure you want to delete this task? This action cannot be undone.",
|
||||||
|
"deleteTaskConfirm": "Delete",
|
||||||
|
"deleteTaskCancel": "Cancel",
|
||||||
|
|
||||||
|
"deleteStatusTitle": "Delete Status",
|
||||||
|
"deleteStatusContent": "Are you sure you want to delete this status? This action cannot be undone.",
|
||||||
|
|
||||||
|
"deletePhaseTitle": "Delete Phase",
|
||||||
|
"deletePhaseContent": "Are you sure you want to delete this phase? This action cannot be undone.",
|
||||||
|
|
||||||
"dueDate": "Due date",
|
"dueDate": "Due date",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
"deleteConfirmationOk": "Sí",
|
"deleteConfirmationOk": "Sí",
|
||||||
"deleteConfirmationCancel": "Cancelar",
|
"deleteConfirmationCancel": "Cancelar",
|
||||||
|
|
||||||
|
"deleteTaskTitle": "Eliminar tarea",
|
||||||
|
"deleteTaskContent": "¿Estás seguro de que deseas eliminar esta tarea? Esta acción no se puede deshacer.",
|
||||||
|
"deleteTaskConfirm": "Eliminar",
|
||||||
|
"deleteTaskCancel": "Cancelar",
|
||||||
|
|
||||||
|
"deleteStatusTitle": "Eliminar estado",
|
||||||
|
"deleteStatusContent": "¿Estás seguro de que deseas eliminar este estado? Esta acción no se puede deshacer.",
|
||||||
|
|
||||||
|
"deletePhaseTitle": "Eliminar fase",
|
||||||
|
"deletePhaseContent": "¿Estás seguro de que deseas eliminar esta fase? Esta acción no se puede deshacer.",
|
||||||
|
|
||||||
"dueDate": "Fecha de vencimiento",
|
"dueDate": "Fecha de vencimiento",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
"deleteConfirmationOk": "Sim",
|
"deleteConfirmationOk": "Sim",
|
||||||
"deleteConfirmationCancel": "Cancelar",
|
"deleteConfirmationCancel": "Cancelar",
|
||||||
|
|
||||||
|
"deleteTaskTitle": "Excluir Tarefa",
|
||||||
|
"deleteTaskContent": "Tem certeza de que deseja excluir esta tarefa? Esta ação não pode ser desfeita.",
|
||||||
|
"deleteTaskConfirm": "Excluir",
|
||||||
|
"deleteTaskCancel": "Cancelar",
|
||||||
|
|
||||||
|
"deleteStatusTitle": "Excluir Status",
|
||||||
|
"deleteStatusContent": "Tem certeza de que deseja excluir este status? Esta ação não pode ser desfeita.",
|
||||||
|
|
||||||
|
"deletePhaseTitle": "Excluir Fase",
|
||||||
|
"deletePhaseContent": "Tem certeza de que deseja excluir esta fase? Esta ação não pode ser desfeita.",
|
||||||
|
|
||||||
"dueDate": "Data de vencimento",
|
"dueDate": "Data de vencimento",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,13 @@
|
|||||||
"assignToMe": "分配给我",
|
"assignToMe": "分配给我",
|
||||||
"archive": "归档",
|
"archive": "归档",
|
||||||
"newTaskNamePlaceholder": "写一个任务名称",
|
"newTaskNamePlaceholder": "写一个任务名称",
|
||||||
"newSubtaskNamePlaceholder": "写一个子任务名称"
|
"newSubtaskNamePlaceholder": "写一个子任务名称",
|
||||||
|
"deleteTaskTitle": "删除任务",
|
||||||
|
"deleteTaskContent": "您确定要删除此任务吗?此操作无法撤销。",
|
||||||
|
"deleteTaskConfirm": "删除",
|
||||||
|
"deleteTaskCancel": "取消",
|
||||||
|
"deleteStatusTitle": "删除状态",
|
||||||
|
"deleteStatusContent": "您确定要删除此状态吗?此操作无法撤销。",
|
||||||
|
"deletePhaseTitle": "删除阶段",
|
||||||
|
"deletePhaseContent": "您确定要删除此阶段吗?此操作无法撤销。"
|
||||||
}
|
}
|
||||||
@@ -118,6 +118,26 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
setDragType(null);
|
setDragType(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Utility to recalculate all task orders for all groups
|
||||||
|
function getAllTaskUpdates(allGroups, groupBy) {
|
||||||
|
const taskUpdates = [];
|
||||||
|
let currentSortOrder = 0;
|
||||||
|
for (const group of allGroups) {
|
||||||
|
for (const task of group.tasks) {
|
||||||
|
const update = {
|
||||||
|
task_id: task.id,
|
||||||
|
sort_order: currentSortOrder,
|
||||||
|
};
|
||||||
|
if (groupBy === 'status') update.status_id = group.id;
|
||||||
|
else if (groupBy === 'priority') update.priority_id = group.id;
|
||||||
|
else if (groupBy === 'phase') update.phase_id = group.id;
|
||||||
|
taskUpdates.push(update);
|
||||||
|
currentSortOrder++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return taskUpdates;
|
||||||
|
}
|
||||||
|
|
||||||
// Task drag handlers
|
// Task drag handlers
|
||||||
const handleTaskDragStart = (e: React.DragEvent, taskId: string, groupId: string) => {
|
const handleTaskDragStart = (e: React.DragEvent, taskId: string, groupId: string) => {
|
||||||
setDraggedTaskId(taskId);
|
setDraggedTaskId(taskId);
|
||||||
@@ -168,6 +188,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
let insertIdx = hoveredTaskIdx;
|
let insertIdx = hoveredTaskIdx;
|
||||||
|
|
||||||
// Handle same group reordering
|
// Handle same group reordering
|
||||||
|
let newTaskGroups = [...taskGroups];
|
||||||
if (sourceGroup.id === targetGroup.id) {
|
if (sourceGroup.id === targetGroup.id) {
|
||||||
// Create a single updated array for the same group
|
// Create a single updated array for the same group
|
||||||
const updatedTasks = [...sourceGroup.tasks];
|
const updatedTasks = [...sourceGroup.tasks];
|
||||||
@@ -201,6 +222,8 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
updatedSourceTasks: updatedTasks,
|
updatedSourceTasks: updatedTasks,
|
||||||
updatedTargetTasks: updatedTasks,
|
updatedTargetTasks: updatedTasks,
|
||||||
}) as any);
|
}) as any);
|
||||||
|
// Update newTaskGroups for socket emit
|
||||||
|
newTaskGroups = newTaskGroups.map(g => g.id === sourceGroup.id ? { ...g, tasks: updatedTasks } : g);
|
||||||
} else {
|
} else {
|
||||||
// Handle cross-group reordering
|
// Handle cross-group reordering
|
||||||
const updatedSourceTasks = [...sourceGroup.tasks];
|
const updatedSourceTasks = [...sourceGroup.tasks];
|
||||||
@@ -229,34 +252,33 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
updatedSourceTasks,
|
updatedSourceTasks,
|
||||||
updatedTargetTasks,
|
updatedTargetTasks,
|
||||||
}) as any);
|
}) as any);
|
||||||
|
// Update newTaskGroups for socket emit
|
||||||
|
newTaskGroups = newTaskGroups.map(g => {
|
||||||
|
if (g.id === sourceGroup.id) return { ...g, tasks: updatedSourceTasks };
|
||||||
|
if (g.id === targetGroup.id) return { ...g, tasks: updatedTargetTasks };
|
||||||
|
return g;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socket emit for task order
|
// Socket emit for full task order
|
||||||
if (socket && projectId && teamId && movedTask) {
|
if (socket && projectId && teamId && movedTask) {
|
||||||
let toSortOrder = -1;
|
const taskUpdates = getAllTaskUpdates(newTaskGroups, groupBy);
|
||||||
let toLastIndex = false;
|
|
||||||
if (insertIdx === targetGroup.tasks.length) {
|
|
||||||
toSortOrder = -1;
|
|
||||||
toLastIndex = true;
|
|
||||||
} else if (targetGroup.tasks[insertIdx]) {
|
|
||||||
const sortOrder = targetGroup.tasks[insertIdx].sort_order;
|
|
||||||
toSortOrder = typeof sortOrder === 'number' ? sortOrder : 0;
|
|
||||||
toLastIndex = false;
|
|
||||||
} else if (targetGroup.tasks.length > 0) {
|
|
||||||
const lastSortOrder = targetGroup.tasks[targetGroup.tasks.length - 1].sort_order;
|
|
||||||
toSortOrder = typeof lastSortOrder === 'number' ? lastSortOrder : 0;
|
|
||||||
toLastIndex = false;
|
|
||||||
}
|
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
from_index: movedTask.sort_order ?? 0,
|
group_by: groupBy || 'status',
|
||||||
to_index: toSortOrder,
|
task_updates: taskUpdates,
|
||||||
to_last_index: toLastIndex,
|
|
||||||
from_group: sourceGroup.id,
|
from_group: sourceGroup.id,
|
||||||
to_group: targetGroup.id,
|
to_group: targetGroup.id,
|
||||||
group_by: groupBy || 'status',
|
|
||||||
task: movedTask,
|
|
||||||
team_id: teamId,
|
team_id: teamId,
|
||||||
|
from_index: taskIdx,
|
||||||
|
to_index: insertIdx,
|
||||||
|
to_last_index: insertIdx === (targetGroup.id === sourceGroup.id ? newTaskGroups.find(g => g.id === targetGroup.id)?.tasks.length - 1 : targetGroup.tasks.length),
|
||||||
|
task: {
|
||||||
|
id: movedTask.id,
|
||||||
|
project_id: movedTask.project_id || projectId,
|
||||||
|
status: movedTask.status || '',
|
||||||
|
priority: movedTask.priority || '',
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit progress update if status changed
|
// Emit progress update if status changed
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
IGroupBy,
|
IGroupBy,
|
||||||
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { Modal } from 'antd';
|
||||||
|
|
||||||
// Simple Portal component
|
// Simple Portal component
|
||||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
@@ -218,7 +219,42 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
setShowDeleteConfirm(true);
|
if (groupBy === IGroupBy.STATUS) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('deleteStatusTitle'),
|
||||||
|
content: t('deleteStatusContent'),
|
||||||
|
okText: t('deleteTaskConfirm'),
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: t('deleteTaskCancel'),
|
||||||
|
centered: true,
|
||||||
|
onOk: async () => {
|
||||||
|
await handleDeleteSection();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (groupBy === IGroupBy.PHASE) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('deletePhaseTitle'),
|
||||||
|
content: t('deletePhaseContent'),
|
||||||
|
okText: t('deleteTaskConfirm'),
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: t('deleteTaskCancel'),
|
||||||
|
centered: true,
|
||||||
|
onOk: async () => {
|
||||||
|
await handleDeleteSection();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('deleteConfirmationTitle'),
|
||||||
|
okText: t('deleteTaskConfirm'),
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: t('deleteTaskCancel'),
|
||||||
|
centered: true,
|
||||||
|
onOk: async () => {
|
||||||
|
await handleDeleteSection();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
setShowDropdown(false);
|
setShowDropdown(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -419,56 +455,7 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Simple Delete Confirmation */}
|
{/* Simple Delete Confirmation */}
|
||||||
{showDeleteConfirm && (
|
{/* Portal-based confirmation removed, now handled by Modal.confirm */}
|
||||||
<Portal>
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black bg-opacity-25 flex items-center justify-center z-[99999]"
|
|
||||||
onClick={() => setShowDeleteConfirm(false)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 max-w-sm w-full mx-4"
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<svg className="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className={`text-base font-medium ${themeMode === 'dark' ? 'text-white' : 'text-gray-900'}`}>
|
|
||||||
{t('deleteConfirmationTitle')}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded border transition-colors ${themeMode === 'dark'
|
|
||||||
? 'border-gray-600 text-gray-300 hover:bg-gray-600'
|
|
||||||
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
|
|
||||||
}`}
|
|
||||||
onClick={() => setShowDeleteConfirm(false)}
|
|
||||||
>
|
|
||||||
{t('deleteConfirmationCancel')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="px-3 py-1.5 text-sm font-medium text-white bg-red-600 border border-transparent rounded hover:bg-red-700 transition-colors"
|
|
||||||
onClick={() => {
|
|
||||||
handleDeleteSection();
|
|
||||||
setShowDeleteConfirm(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('deleteConfirmationOk')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
)}
|
|
||||||
<div className="enhanced-kanban-group-tasks">
|
<div className="enhanced-kanban-group-tasks">
|
||||||
{/* Create card at top */}
|
{/* Create card at top */}
|
||||||
{showNewCardTop && (
|
{showNewCardTop && (
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import { useSocket } from '@/socket/socketContext';
|
|||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import { getUserSession } from '@/utils/session-helper';
|
import { getUserSession } from '@/utils/session-helper';
|
||||||
import { themeWiseColor } from '@/utils/themeWiseColor';
|
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||||
import { toggleTaskExpansion, fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
import { toggleTaskExpansion, fetchBoardSubTasks, deleteTask as deleteKanbanTask, updateEnhancedKanbanSubtask } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import TaskProgressCircle from './TaskProgressCircle';
|
import TaskProgressCircle from './TaskProgressCircle';
|
||||||
|
import { Button, Modal } from 'antd';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import { tasksApiService } from '@/api/tasks/tasks.api.service';
|
||||||
|
|
||||||
// Simple Portal component
|
// Simple Portal component
|
||||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
@@ -70,6 +73,9 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
const d = selectedDate || new Date();
|
const d = selectedDate || new Date();
|
||||||
return new Date(d.getFullYear(), d.getMonth(), 1);
|
return new Date(d.getFullYear(), d.getMonth(), 1);
|
||||||
});
|
});
|
||||||
|
const [contextMenu, setContextMenu] = useState<{ visible: boolean; x: number; y: number }>({ visible: false, x: 0, y: 0 });
|
||||||
|
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [selectedTask, setSelectedTask] = useState<IProjectTask | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedDate(task.end_date ? new Date(task.end_date) : null);
|
setSelectedDate(task.end_date ? new Date(task.end_date) : null);
|
||||||
@@ -102,6 +108,21 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
}
|
}
|
||||||
}, [showDatePicker]);
|
}, [showDatePicker]);
|
||||||
|
|
||||||
|
// Hide context menu on click elsewhere
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) {
|
||||||
|
setContextMenu({ ...contextMenu, visible: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (contextMenu.visible) {
|
||||||
|
document.addEventListener('mousedown', handleClick);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClick);
|
||||||
|
};
|
||||||
|
}, [contextMenu]);
|
||||||
|
|
||||||
const handleCardClick = useCallback((e: React.MouseEvent, id: string) => {
|
const handleCardClick = useCallback((e: React.MouseEvent, id: string) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dispatch(setSelectedTaskId(id));
|
dispatch(setSelectedTaskId(id));
|
||||||
@@ -178,6 +199,48 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
handleSubTaskExpand();
|
handleSubTaskExpand();
|
||||||
}, [handleSubTaskExpand]);
|
}, [handleSubTaskExpand]);
|
||||||
|
|
||||||
|
// Delete logic (similar to task-drawer-header)
|
||||||
|
const handleDeleteTask = async (task: IProjectTask | null) => {
|
||||||
|
if (!task || !task.id) return;
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('deleteTaskTitle'),
|
||||||
|
content: t('deleteTaskContent'),
|
||||||
|
okText: t('deleteTaskConfirm'),
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: t('deleteTaskCancel'),
|
||||||
|
centered: true,
|
||||||
|
onOk: async () => {
|
||||||
|
if (!task.id) return;
|
||||||
|
const res = await tasksApiService.deleteTask(task.id);
|
||||||
|
if (res.done) {
|
||||||
|
dispatch(setSelectedTaskId(null));
|
||||||
|
if (task.is_sub_task) {
|
||||||
|
dispatch(updateEnhancedKanbanSubtask({
|
||||||
|
sectionId: '',
|
||||||
|
subtask: { id: task.id , parent_task_id: task.parent_task_id || '', manual_progress: false },
|
||||||
|
mode: 'delete',
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(deleteKanbanTask(task.id));
|
||||||
|
}
|
||||||
|
dispatch(setShowTaskDrawer(false));
|
||||||
|
if (task.parent_task_id) {
|
||||||
|
socket?.emit(
|
||||||
|
SocketEvents.GET_TASK_PROGRESS.toString(),
|
||||||
|
task.parent_task_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setContextMenu({ visible: false, x: 0, y: 0 });
|
||||||
|
setSelectedTask(null);
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
setContextMenu({ visible: false, x: 0, y: 0 });
|
||||||
|
setSelectedTask(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Calendar rendering helpers
|
// Calendar rendering helpers
|
||||||
const year = calendarMonth.getFullYear();
|
const year = calendarMonth.getFullYear();
|
||||||
const month = calendarMonth.getMonth();
|
const month = calendarMonth.getMonth();
|
||||||
@@ -202,7 +265,37 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="enhanced-kanban-task-card" style={{ background, color, display: 'block', position: 'relative' }} >
|
{/* Context menu for delete */}
|
||||||
|
{contextMenu.visible && (
|
||||||
|
<div
|
||||||
|
ref={contextMenuRef}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: contextMenu.y,
|
||||||
|
left: contextMenu.x,
|
||||||
|
zIndex: 9999,
|
||||||
|
background: themeMode === 'dark' ? '#23272f' : '#fff',
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
||||||
|
padding: 0,
|
||||||
|
minWidth: 120,
|
||||||
|
transition: 'translateY(0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<DeleteOutlined style={{ color: '#ef4444', fontSize: 16 }} />}
|
||||||
|
style={{ color: '#ef4444', width: '100%', textAlign: 'left', padding: '8px 16px', fontWeight: 500 }}
|
||||||
|
onClick={() => handleDeleteTask(selectedTask || null)}
|
||||||
|
>
|
||||||
|
{t('delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="enhanced-kanban-task-card"
|
||||||
|
style={{ background, color, display: 'block', position: 'relative' }}
|
||||||
|
>
|
||||||
{/* Progress circle at top right */}
|
{/* Progress circle at top right */}
|
||||||
<div style={{ position: 'absolute', top: 6, right: 6, zIndex: 2 }}>
|
<div style={{ position: 'absolute', top: 6, right: 6, zIndex: 2 }}>
|
||||||
<TaskProgressCircle task={task} size={20} />
|
<TaskProgressCircle task={task} size={20} />
|
||||||
@@ -221,6 +314,11 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
onDrop={e => onTaskDrop(e, groupId, idx)}
|
onDrop={e => onTaskDrop(e, groupId, idx)}
|
||||||
onDragEnd={onDragEnd} // <-- add this
|
onDragEnd={onDragEnd} // <-- add this
|
||||||
onClick={e => handleCardClick(e, task.id!)}
|
onClick={e => handleCardClick(e, task.id!)}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setContextMenu({ visible: true, x: e.clientX, y: e.clientY });
|
||||||
|
setSelectedTask(task);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="task-content">
|
<div className="task-content">
|
||||||
<div className="task_labels" style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
<div className="task_labels" style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
||||||
@@ -447,7 +545,14 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
{!task.sub_tasks_loading && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0 && (
|
{!task.sub_tasks_loading && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0 && (
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{task.sub_tasks.map(sub => (
|
{task.sub_tasks.map(sub => (
|
||||||
<li key={sub.id} onClick={e => handleCardClick(e, sub.id!)} className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800">
|
<li key={sub.id}
|
||||||
|
onClick={e => handleCardClick(e, sub.id!)}
|
||||||
|
className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setContextMenu({ visible: true, x: e.clientX, y: e.clientY });
|
||||||
|
setSelectedTask(sub);
|
||||||
|
}}>
|
||||||
{sub.priority_color || sub.priority_color_dark ? (
|
{sub.priority_color || sub.priority_color_dark ? (
|
||||||
<span
|
<span
|
||||||
className="w-2 h-2 rounded-full inline-block"
|
className="w-2 h-2 rounded-full inline-block"
|
||||||
|
|||||||
Reference in New Issue
Block a user