From 6f66367282862d096a04b5267537ae8562399af6 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Fri, 4 Jul 2025 11:01:21 +0530 Subject: [PATCH] feat(task-management): improve dropdown behavior and enhance task row components - Updated AssigneeSelector and LabelsSelector to close dropdowns only when scrolling outside the dropdown area, enhancing user experience. - Introduced TaskLabelsCell component in TaskRow for better label rendering and organization. - Refactored date handling in TaskRow to consolidate formatted date logic and improve clarity. - Memoized date picker handlers for better performance and cleaner code. --- .../src/components/AssigneeSelector.tsx | 8 +- .../src/components/LabelsSelector.tsx | 8 +- .../src/components/task-list-v2/TaskRow.tsx | 228 +++++++++--------- 3 files changed, 121 insertions(+), 123 deletions(-) diff --git a/worklenz-frontend/src/components/AssigneeSelector.tsx b/worklenz-frontend/src/components/AssigneeSelector.tsx index 1c486746..1ca0cdf8 100644 --- a/worklenz-frontend/src/components/AssigneeSelector.tsx +++ b/worklenz-frontend/src/components/AssigneeSelector.tsx @@ -88,10 +88,12 @@ const AssigneeSelector: React.FC = ({ } }; - const handleScroll = () => { + const handleScroll = (event: Event) => { if (isOpen) { - // Close dropdown when scrolling to prevent it from moving with the content - setIsOpen(false); + // Only close dropdown if scrolling happens outside the dropdown + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } } }; diff --git a/worklenz-frontend/src/components/LabelsSelector.tsx b/worklenz-frontend/src/components/LabelsSelector.tsx index ed59085f..960970ae 100644 --- a/worklenz-frontend/src/components/LabelsSelector.tsx +++ b/worklenz-frontend/src/components/LabelsSelector.tsx @@ -73,10 +73,12 @@ const LabelsSelector: React.FC = ({ task, isDarkMode = fals } }; - const handleScroll = () => { + const handleScroll = (event: Event) => { if (isOpen) { - // Close dropdown when scrolling to prevent it from moving with the content - setIsOpen(false); + // Only close dropdown if scrolling happens outside the dropdown + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } } }; diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx index 4d761a24..a857ad43 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx @@ -6,10 +6,8 @@ import { Checkbox, DatePicker } from 'antd'; import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports'; import { Task } from '@/types/task-management.types'; import { InlineMember } from '@/types/teamMembers/inlineMember.types'; -import Avatar from '@/components/Avatar'; import AssigneeSelector from '@/components/AssigneeSelector'; import { format } from 'date-fns'; -import { Bars3Icon } from '@heroicons/react/24/outline'; import AvatarGroup from '../AvatarGroup'; import { DEFAULT_TASK_NAME } from '@/shared/constants'; import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress'; @@ -37,6 +35,42 @@ interface TaskRowProps { }>; } +interface TaskLabelsCellProps { + labels: Task['labels']; + isDarkMode: boolean; +} + +const TaskLabelsCell: React.FC = memo(({ labels, isDarkMode }) => { + if (!labels) { + return null; + } + + return ( + <> + {labels.map((label, index) => { + const extendedLabel = label as any; + return extendedLabel.end && extendedLabel.names && extendedLabel.name ? ( + + ) : ( + + ); + })} + + ); +}); + +TaskLabelsCell.displayName = 'TaskLabelsCell'; + // Utility function to get task display name with fallbacks const getTaskDisplayName = (task: Task): string => { // Check each field and only use if it has actual content after trimming @@ -111,15 +145,16 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn }), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]); // Memoize formatted dates - const formattedDueDate = useMemo(() => { - const dateValue = task.dueDate || task.due_date; - return dateValue ? formatDate(dateValue) : null; - }, [task.dueDate, task.due_date]); - - const formattedStartDate = useMemo(() => - task.startDate ? formatDate(task.startDate) : null, - [task.startDate] - ); + const formattedDates = useMemo(() => ({ + due: (() => { + const dateValue = task.dueDate || task.due_date; + return dateValue ? formatDate(dateValue) : null; + })(), + start: task.startDate ? formatDate(task.startDate) : null, + completed: task.completedAt ? formatDate(task.completedAt) : null, + created: task.created_at ? formatDate(task.created_at) : null, + updated: task.updatedAt ? formatDate(task.updatedAt) : null, + }), [task.dueDate, task.due_date, task.startDate, task.completedAt, task.created_at, task.updatedAt]); // Memoize date values for DatePicker const dateValues = useMemo( @@ -129,23 +164,24 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn }), [task.startDate, task.dueDate, task.due_date] ); - - const formattedCompletedDate = useMemo(() => - task.completedAt ? formatDate(task.completedAt) : null, - [task.completedAt] - ); - - const formattedCreatedDate = useMemo(() => - task.created_at ? formatDate(task.created_at) : null, - [task.created_at] - ); - - const formattedUpdatedDate = useMemo(() => - task.updatedAt ? formatDate(task.updatedAt) : null, - [task.updatedAt] - ); - + // Create labels adapter for LabelsSelector + const labelsAdapter = useMemo(() => ({ + id: task.id, + name: task.title || task.name, + parent_task_id: task.parent_task_id, + manual_progress: false, + all_labels: task.labels?.map(label => ({ + id: label.id, + name: label.name, + color_code: label.color, + })) || [], + labels: task.labels?.map(label => ({ + id: label.id, + name: label.name, + color_code: label.color, + })) || [], + }), [task.id, task.title, task.name, task.parent_task_id, task.labels]); // Handle checkbox change const handleCheckboxChange = useCallback((e: any) => { @@ -180,35 +216,21 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn [connected, socket, task.id] ); - // Memoize status style - const statusStyle = useMemo(() => ({ - backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)', - color: task.statusColor || 'rgb(31, 41, 55)', - }), [task.statusColor]); - - // Memoize priority style - const priorityStyle = useMemo(() => ({ - backgroundColor: task.priorityColor ? `${task.priorityColor}20` : 'rgb(229, 231, 235)', - color: task.priorityColor || 'rgb(31, 41, 55)', - }), [task.priorityColor]); - - // Create labels adapter for LabelsSelector - const labelsAdapter = useMemo(() => ({ - id: task.id, - name: task.title || task.name, - parent_task_id: task.parent_task_id, - manual_progress: false, - all_labels: task.labels?.map(label => ({ - id: label.id, - name: label.name, - color_code: label.color, - })) || [], - labels: task.labels?.map(label => ({ - id: label.id, - name: label.name, - color_code: label.color, - })) || [], - }), [task.id, task.title, task.name, task.parent_task_id, task.labels]); + // Memoize date picker handlers + const datePickerHandlers = useMemo(() => ({ + setDueDate: () => setActiveDatePicker('dueDate'), + setStartDate: () => setActiveDatePicker('startDate'), + clearDueDate: (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + handleDateChange(null, 'dueDate'); + }, + clearStartDate: (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + handleDateChange(null, 'startDate'); + }, + }), [handleDateChange]); const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => { const baseStyle = { width }; @@ -318,11 +340,7 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn {/* Custom clear button */} {dateValues.due && (