Merge pull request #274 from shancds/test/kanban-order-v1.2.2

Test/kanban order v1.2.2
This commit is contained in:
Chamika J
2025-07-16 09:35:12 +05:30
committed by GitHub
9 changed files with 252 additions and 75 deletions

View File

@@ -10,6 +10,17 @@
"deleteConfirmationOk": "Po",
"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",
"cancel": "Anulo",

View File

@@ -10,6 +10,17 @@
"deleteConfirmationOk": "Ja",
"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",
"cancel": "Abbrechen",

View File

@@ -10,6 +10,17 @@
"deleteConfirmationOk": "Yes",
"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",
"cancel": "Cancel",

View File

@@ -10,6 +10,17 @@
"deleteConfirmationOk": "Sí",
"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",
"cancel": "Cancelar",

View File

@@ -10,6 +10,17 @@
"deleteConfirmationOk": "Sim",
"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",
"cancel": "Cancelar",

View File

@@ -15,5 +15,13 @@
"assignToMe": "分配给我",
"archive": "归档",
"newTaskNamePlaceholder": "写一个任务名称",
"newSubtaskNamePlaceholder": "写一个子任务名称"
"newSubtaskNamePlaceholder": "写一个子任务名称",
"deleteTaskTitle": "删除任务",
"deleteTaskContent": "您确定要删除此任务吗?此操作无法撤销。",
"deleteTaskConfirm": "删除",
"deleteTaskCancel": "取消",
"deleteStatusTitle": "删除状态",
"deleteStatusContent": "您确定要删除此状态吗?此操作无法撤销。",
"deletePhaseTitle": "删除阶段",
"deletePhaseContent": "您确定要删除此阶段吗?此操作无法撤销。"
}

View File

@@ -118,6 +118,26 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
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
const handleTaskDragStart = (e: React.DragEvent, taskId: string, groupId: string) => {
setDraggedTaskId(taskId);
@@ -168,6 +188,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
let insertIdx = hoveredTaskIdx;
// Handle same group reordering
let newTaskGroups = [...taskGroups];
if (sourceGroup.id === targetGroup.id) {
// Create a single updated array for the same group
const updatedTasks = [...sourceGroup.tasks];
@@ -201,6 +222,8 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
updatedSourceTasks: updatedTasks,
updatedTargetTasks: updatedTasks,
}) as any);
// Update newTaskGroups for socket emit
newTaskGroups = newTaskGroups.map(g => g.id === sourceGroup.id ? { ...g, tasks: updatedTasks } : g);
} else {
// Handle cross-group reordering
const updatedSourceTasks = [...sourceGroup.tasks];
@@ -229,34 +252,33 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
updatedSourceTasks,
updatedTargetTasks,
}) 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) {
let toSortOrder = -1;
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;
}
const taskUpdates = getAllTaskUpdates(newTaskGroups, groupBy);
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
project_id: projectId,
from_index: movedTask.sort_order ?? 0,
to_index: toSortOrder,
to_last_index: toLastIndex,
group_by: groupBy || 'status',
task_updates: taskUpdates,
from_group: sourceGroup.id,
to_group: targetGroup.id,
group_by: groupBy || 'status',
task: movedTask,
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

View File

@@ -25,6 +25,7 @@ import {
IGroupBy,
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { createPortal } from 'react-dom';
import { Modal } from 'antd';
// Simple Portal component
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@@ -218,7 +219,42 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
};
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);
};
@@ -419,56 +455,7 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
</div>
{/* Simple Delete Confirmation */}
{showDeleteConfirm && (
<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>
)}
{/* Portal-based confirmation removed, now handled by Modal.confirm */}
<div className="enhanced-kanban-group-tasks">
{/* Create card at top */}
{showNewCardTop && (

View File

@@ -14,8 +14,11 @@ import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { getUserSession } from '@/utils/session-helper';
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 { Button, Modal } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import { tasksApiService } from '@/api/tasks/tasks.api.service';
// Simple Portal component
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@@ -70,6 +73,9 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
const d = selectedDate || new Date();
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(() => {
setSelectedDate(task.end_date ? new Date(task.end_date) : null);
@@ -102,6 +108,21 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
}
}, [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) => {
e.stopPropagation();
dispatch(setSelectedTaskId(id));
@@ -178,6 +199,48 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
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
const year = calendarMonth.getFullYear();
const month = calendarMonth.getMonth();
@@ -202,7 +265,37 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
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 */}
<div style={{ position: 'absolute', top: 6, right: 6, zIndex: 2 }}>
<TaskProgressCircle task={task} size={20} />
@@ -221,6 +314,11 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
onDrop={e => onTaskDrop(e, groupId, idx)}
onDragEnd={onDragEnd} // <-- add this
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_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 && (
<ul className="space-y-1">
{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 ? (
<span
className="w-2 h-2 rounded-full inline-block"