From fdb485614f91d6c96d64c366f9c9654232d771a9 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Fri, 27 Jun 2025 10:59:21 +0530 Subject: [PATCH] feat(task-management): enhance task management UI with subtask functionality - Added localization support for task management messages in multiple languages, including Albanian, German, English, Spanish, and Portuguese. - Implemented subtask addition feature in the `TaskRow` component, allowing users to create and manage subtasks directly within the task interface. - Introduced hover effects and improved styling for task rows to enhance user experience and interaction. - Updated task indicators to display counts for subtasks, comments, and attachments, providing better visibility of task details. --- .../public/locales/alb/task-management.json | 12 +- .../public/locales/de/task-management.json | 12 +- .../public/locales/en/task-management.json | 12 +- .../public/locales/es/task-management.json | 12 +- .../public/locales/pt/task-management.json | 12 +- .../task-management/task-row-optimized.css | 101 +++++++ .../components/task-management/task-row.tsx | 274 +++++++++++++++++- 7 files changed, 416 insertions(+), 19 deletions(-) diff --git a/worklenz-frontend/public/locales/alb/task-management.json b/worklenz-frontend/public/locales/alb/task-management.json index 477be621..5fe5aef6 100644 --- a/worklenz-frontend/public/locales/alb/task-management.json +++ b/worklenz-frontend/public/locales/alb/task-management.json @@ -1,5 +1,15 @@ { "noTasksInGroup": "Nuk ka detyra në këtë grup", "noTasksInGroupDescription": "Shtoni një detyrë për të filluar", - "addFirstTask": "Shtoni detyrën tuaj të parë" + "addFirstTask": "Shtoni detyrën tuaj të parë", + "openTask": "Hap", + "subtask": "nën-detyrë", + "subtasks": "nën-detyra", + "comment": "koment", + "comments": "komente", + "attachment": "bashkëngjitje", + "attachments": "bashkëngjitje", + "enterSubtaskName": "Shkruani emrin e nën-detyrës...", + "add": "Shto", + "cancel": "Anulo" } \ No newline at end of file diff --git a/worklenz-frontend/public/locales/de/task-management.json b/worklenz-frontend/public/locales/de/task-management.json index 35e406e1..45ae2836 100644 --- a/worklenz-frontend/public/locales/de/task-management.json +++ b/worklenz-frontend/public/locales/de/task-management.json @@ -1,5 +1,15 @@ { "noTasksInGroup": "Keine Aufgaben in dieser Gruppe", "noTasksInGroupDescription": "Fügen Sie eine Aufgabe hinzu, um zu beginnen", - "addFirstTask": "Fügen Sie Ihre erste Aufgabe hinzu" + "addFirstTask": "Fügen Sie Ihre erste Aufgabe hinzu", + "openTask": "Öffnen", + "subtask": "Unteraufgabe", + "subtasks": "Unteraufgaben", + "comment": "Kommentar", + "comments": "Kommentare", + "attachment": "Anhang", + "attachments": "Anhänge", + "enterSubtaskName": "Unteraufgabenname eingeben...", + "add": "Hinzufügen", + "cancel": "Abbrechen" } \ No newline at end of file diff --git a/worklenz-frontend/public/locales/en/task-management.json b/worklenz-frontend/public/locales/en/task-management.json index d76e4d9b..27df7a05 100644 --- a/worklenz-frontend/public/locales/en/task-management.json +++ b/worklenz-frontend/public/locales/en/task-management.json @@ -1,5 +1,15 @@ { "noTasksInGroup": "No tasks in this group", "noTasksInGroupDescription": "Add a task to get started", - "addFirstTask": "Add your first task" + "addFirstTask": "Add your first task", + "openTask": "Open", + "subtask": "subtask", + "subtasks": "subtasks", + "comment": "comment", + "comments": "comments", + "attachment": "attachment", + "attachments": "attachments", + "enterSubtaskName": "Enter subtask name...", + "add": "Add", + "cancel": "Cancel" } \ No newline at end of file diff --git a/worklenz-frontend/public/locales/es/task-management.json b/worklenz-frontend/public/locales/es/task-management.json index e24bcb6d..4b916d5b 100644 --- a/worklenz-frontend/public/locales/es/task-management.json +++ b/worklenz-frontend/public/locales/es/task-management.json @@ -1,5 +1,15 @@ { "noTasksInGroup": "No hay tareas en este grupo", "noTasksInGroupDescription": "Añade una tarea para comenzar", - "addFirstTask": "Añade tu primera tarea" + "addFirstTask": "Añade tu primera tarea", + "openTask": "Abrir", + "subtask": "subtarea", + "subtasks": "subtareas", + "comment": "comentario", + "comments": "comentarios", + "attachment": "adjunto", + "attachments": "adjuntos", + "enterSubtaskName": "Ingresa el nombre de la subtarea...", + "add": "Añadir", + "cancel": "Cancelar" } \ No newline at end of file diff --git a/worklenz-frontend/public/locales/pt/task-management.json b/worklenz-frontend/public/locales/pt/task-management.json index a0b23c6f..5f9bc0d4 100644 --- a/worklenz-frontend/public/locales/pt/task-management.json +++ b/worklenz-frontend/public/locales/pt/task-management.json @@ -1,5 +1,15 @@ { "noTasksInGroup": "Nenhuma tarefa neste grupo", "noTasksInGroupDescription": "Adicione uma tarefa para começar", - "addFirstTask": "Adicione sua primeira tarefa" + "addFirstTask": "Adicione sua primeira tarefa", + "openTask": "Abrir", + "subtask": "subtarefa", + "subtasks": "subtarefas", + "comment": "comentário", + "comments": "comentários", + "attachment": "anexo", + "attachments": "anexos", + "enterSubtaskName": "Digite o nome da subtarefa...", + "add": "Adicionar", + "cancel": "Cancelar" } \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/task-row-optimized.css b/worklenz-frontend/src/components/task-management/task-row-optimized.css index 6a0322b4..57e811d4 100644 --- a/worklenz-frontend/src/components/task-management/task-row-optimized.css +++ b/worklenz-frontend/src/components/task-management/task-row-optimized.css @@ -235,4 +235,105 @@ .task-row-optimized * { box-sizing: border-box; +} + +/* Task row hover effects for better performance */ +.task-cell-container:hover .task-open-button { + opacity: 1 !important; +} + +.task-open-button { + opacity: 0; + transition: opacity 0.2s ease-in-out; +} + +/* Expand icon smart visibility */ +.expand-icon-container { + transition: opacity 0.2s ease-in-out; +} + +/* Always show expand icon if task has subtasks */ +.expand-icon-container.has-subtasks { + opacity: 1; +} + +.expand-icon-container.has-subtasks .expand-toggle-btn { + opacity: 0.8; +} + +.task-cell-container:hover .expand-icon-container.has-subtasks .expand-toggle-btn { + opacity: 1; +} + +/* Show expand icon on hover for tasks without subtasks (for adding subtasks) */ +.expand-icon-container.hover-only { + opacity: 0; +} + +.task-cell-container:hover .expand-icon-container.hover-only { + opacity: 1; +} + +.expand-icon-container.hover-only .expand-toggle-btn { + opacity: 0.6; +} + +.task-cell-container:hover .expand-icon-container.hover-only .expand-toggle-btn { + opacity: 1; +} + +/* Add subtask row styling */ +.add-subtask-row { + opacity: 0; + max-height: 0; + overflow: hidden; + transition: all 0.3s ease-in-out; + transform: translateY(-10px); +} + +.add-subtask-row.visible { + opacity: 1; + max-height: 60px; + transform: translateY(0); +} + +.add-subtask-input { + transition: all 0.2s ease-in-out; +} + +.add-subtask-input:focus { + transform: scale(1.02); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15); +} + +/* Dark mode add subtask row */ +.dark .add-subtask-row { + background-color: #1f2937; + border-color: #374151; +} + +.dark .add-subtask-input { + background-color: #374151; + border-color: #4b5563; + color: #f3f4f6; +} + +.dark .add-subtask-input:focus { + border-color: #60a5fa; + box-shadow: 0 2px 8px rgba(96, 165, 250, 0.25); +} + +/* Task indicators hover effects */ +.task-indicators .indicator-badge { + transition: all 0.2s ease-in-out; +} + +.task-indicators .indicator-badge:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Dark mode specific hover effects */ +.dark .task-indicators .indicator-badge:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/task-row.tsx b/worklenz-frontend/src/components/task-management/task-row.tsx index de8731b1..41c40d8d 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -16,6 +16,8 @@ import { UserOutlined, type InputRef } from './antd-imports'; +import { DownOutlined, RightOutlined, ExpandAltOutlined, DoubleRightOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; import { Task } from '@/types/task-management.types'; import { RootState } from '@/app/store'; import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, LabelsSelector, Progress, Tooltip } from '@/components'; @@ -185,7 +187,10 @@ const TaskRow: React.FC = React.memo(({ // Edit task name state const [editTaskName, setEditTaskName] = useState(false); const [taskName, setTaskName] = useState(task.title || ''); - const inputRef = useRef(null); + const [showAddSubtask, setShowAddSubtask] = useState(false); + const [newSubtaskName, setNewSubtaskName] = useState(''); + const inputRef = useRef(null); + const addSubtaskInputRef = useRef(null); const wrapperRef = useRef(null); // PERFORMANCE OPTIMIZATION: Intersection Observer for lazy loading @@ -244,6 +249,9 @@ const TaskRow: React.FC = React.memo(({ // Get theme from Redux store - memoized selector const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark'); + + // Translation hook + const { t } = useTranslation('task-management'); // PERFORMANCE OPTIMIZATION: Only setup click outside detection when editing useEffect(() => { @@ -265,7 +273,7 @@ const TaskRow: React.FC = React.memo(({ // Optimized task name save handler const handleTaskNameSave = useCallback(() => { - const newTaskName = inputRef.current?.value?.trim(); + const newTaskName = taskName?.trim(); if (newTaskName && connected && newTaskName !== task.title) { socket?.emit( SocketEvents.TASK_NAME_CHANGE.toString(), @@ -277,7 +285,30 @@ const TaskRow: React.FC = React.memo(({ ); } setEditTaskName(false); - }, [connected, socket, task.id, task.title]); + }, [connected, socket, task.id, task.title, taskName]); + + // Handle adding new subtask + const handleAddSubtask = useCallback(() => { + const subtaskName = newSubtaskName?.trim(); + if (subtaskName && connected) { + socket?.emit( + SocketEvents.TASK_NAME_CHANGE.toString(), // Using existing event for now + JSON.stringify({ + name: subtaskName, + parent_task_id: task.id, + project_id: projectId, + }) + ); + setNewSubtaskName(''); + setShowAddSubtask(false); + } + }, [newSubtaskName, connected, socket, task.id, projectId]); + + // Handle canceling add subtask + const handleCancelAddSubtask = useCallback(() => { + setNewSubtaskName(''); + setShowAddSubtask(false); + }, []); // Optimized style calculations with better memoization const dragStyle = useMemo(() => { @@ -302,6 +333,18 @@ const TaskRow: React.FC = React.memo(({ onToggleSubtasks?.(task.id); }, [onToggleSubtasks, task.id]); + // Handle expand/collapse or add subtask + const handleExpandClick = useCallback(() => { + // For now, just toggle add subtask row for all tasks + setShowAddSubtask(!showAddSubtask); + if (!showAddSubtask) { + // Focus the input after state update + setTimeout(() => { + addSubtaskInputRef.current?.focus(); + }, 100); + } + }, [showAddSubtask]); + // Optimized date handling with better memoization const dateValues = useMemo(() => ({ start: task.startDate ? dayjs(task.startDate) : undefined, @@ -494,26 +537,46 @@ const TaskRow: React.FC = React.memo(({ return (
-
-
+
+ {/* Left section with expand icon and task content */} +
+ {/* Expand/Collapse Icon - Smart visibility */} +
+ +
+ + {/* Task name and input */}
{editTaskName ? ( - setTaskName(e.target.value)} onBlur={handleTaskNameSave} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleTaskNameSave(); - } - }} + onPressEnter={handleTaskNameSave} + variant="borderless" style={{ - color: isDarkMode ? '#ffffff' : '#262626' + color: isDarkMode ? '#ffffff' : '#262626', + padding: 0 }} autoFocus /> @@ -528,7 +591,90 @@ const TaskRow: React.FC = React.memo(({ )}
+ + {/* Indicators section */} + {!editTaskName && ( +
+ {/* Subtasks count */} + {task.subtasks_count && task.subtasks_count > 0 && ( + +
{ + e.preventDefault(); + e.stopPropagation(); + handleToggleSubtasks?.(); + }} + > + {task.subtasks_count} + +
+
+ )} + + {/* Comments indicator */} + {task.comments_count && task.comments_count > 0 && ( + +
+ + {task.comments_count} +
+
+ )} + + {/* Attachments indicator */} + {task.attachments_count && task.attachments_count > 0 && ( + +
+ + {task.attachments_count} +
+
+ )} +
+ )}
+ + {/* Right section with open button - CSS hover only */} + {!editTaskName && ( +
+ +
+ )}
); @@ -767,6 +913,106 @@ const TaskRow: React.FC = React.memo(({
)}
+ + {/* Add Subtask Row */} + {showAddSubtask && ( +
+
+ {/* Fixed Columns for Add Subtask */} + {fixedColumns && fixedColumns.length > 0 && ( +
sum + col.width, 0), + }} + > + {fixedColumns.map((col, index) => { + const isLast = index === fixedColumns.length - 1; + const borderClasses = `${isLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; + + if (col.key === 'task') { + return ( +
+
+ setNewSubtaskName(e.target.value)} + onPressEnter={handleAddSubtask} + onBlur={handleCancelAddSubtask} + className={`add-subtask-input flex-1 ${ + isDarkMode + ? 'bg-gray-700 border-gray-600 text-gray-200' + : 'bg-white border-gray-300 text-gray-900' + }`} + size="small" + autoFocus + /> +
+ + +
+
+
+ ); + } else { + return ( +
+ ); + } + })} +
+ )} + + {/* Scrollable Columns for Add Subtask */} + {scrollableColumns && scrollableColumns.length > 0 && ( +
sum + col.width, 0) + }} + > + {scrollableColumns.map((col, index) => { + const isLast = index === scrollableColumns.length - 1; + const borderClasses = `${isLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`; + + return ( +
+ ); + })} +
+ )} +
+
+ )}
); }, (prevProps, nextProps) => {