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.
This commit is contained in:
chamikaJ
2025-07-04 11:01:21 +05:30
parent 8adeabce61
commit 6f66367282
3 changed files with 121 additions and 123 deletions

View File

@@ -88,10 +88,12 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
} }
}; };
const handleScroll = () => { const handleScroll = (event: Event) => {
if (isOpen) { if (isOpen) {
// Close dropdown when scrolling to prevent it from moving with the content // Only close dropdown if scrolling happens outside the dropdown
setIsOpen(false); if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
} }
}; };

View File

@@ -73,10 +73,12 @@ const LabelsSelector: React.FC<LabelsSelectorProps> = ({ task, isDarkMode = fals
} }
}; };
const handleScroll = () => { const handleScroll = (event: Event) => {
if (isOpen) { if (isOpen) {
// Close dropdown when scrolling to prevent it from moving with the content // Only close dropdown if scrolling happens outside the dropdown
setIsOpen(false); if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
} }
}; };

View File

@@ -6,10 +6,8 @@ import { Checkbox, DatePicker } from 'antd';
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports'; import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
import { Task } from '@/types/task-management.types'; import { Task } from '@/types/task-management.types';
import { InlineMember } from '@/types/teamMembers/inlineMember.types'; import { InlineMember } from '@/types/teamMembers/inlineMember.types';
import Avatar from '@/components/Avatar';
import AssigneeSelector from '@/components/AssigneeSelector'; import AssigneeSelector from '@/components/AssigneeSelector';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { Bars3Icon } from '@heroicons/react/24/outline';
import AvatarGroup from '../AvatarGroup'; import AvatarGroup from '../AvatarGroup';
import { DEFAULT_TASK_NAME } from '@/shared/constants'; import { DEFAULT_TASK_NAME } from '@/shared/constants';
import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress'; 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<TaskLabelsCellProps> = memo(({ labels, isDarkMode }) => {
if (!labels) {
return null;
}
return (
<>
{labels.map((label, index) => {
const extendedLabel = label as any;
return extendedLabel.end && extendedLabel.names && extendedLabel.name ? (
<CustomNumberLabel
key={`${label.id}-${index}`}
labelList={extendedLabel.names}
namesString={extendedLabel.name}
isDarkMode={isDarkMode}
color={label.color}
/>
) : (
<CustomColordLabel
key={`${label.id}-${index}`}
label={label}
isDarkMode={isDarkMode}
/>
);
})}
</>
);
});
TaskLabelsCell.displayName = 'TaskLabelsCell';
// Utility function to get task display name with fallbacks // Utility function to get task display name with fallbacks
const getTaskDisplayName = (task: Task): string => { const getTaskDisplayName = (task: Task): string => {
// Check each field and only use if it has actual content after trimming // Check each field and only use if it has actual content after trimming
@@ -111,15 +145,16 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
}), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]); }), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]);
// Memoize formatted dates // Memoize formatted dates
const formattedDueDate = useMemo(() => { const formattedDates = useMemo(() => ({
const dateValue = task.dueDate || task.due_date; due: (() => {
return dateValue ? formatDate(dateValue) : null; const dateValue = task.dueDate || task.due_date;
}, [task.dueDate, task.due_date]); return dateValue ? formatDate(dateValue) : null;
})(),
const formattedStartDate = useMemo(() => start: task.startDate ? formatDate(task.startDate) : null,
task.startDate ? formatDate(task.startDate) : null, completed: task.completedAt ? formatDate(task.completedAt) : null,
[task.startDate] 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 // Memoize date values for DatePicker
const dateValues = useMemo( const dateValues = useMemo(
@@ -129,23 +164,24 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
}), }),
[task.startDate, task.dueDate, task.due_date] [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 // Handle checkbox change
const handleCheckboxChange = useCallback((e: any) => { const handleCheckboxChange = useCallback((e: any) => {
@@ -180,35 +216,21 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
[connected, socket, task.id] [connected, socket, task.id]
); );
// Memoize status style // Memoize date picker handlers
const statusStyle = useMemo(() => ({ const datePickerHandlers = useMemo(() => ({
backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)', setDueDate: () => setActiveDatePicker('dueDate'),
color: task.statusColor || 'rgb(31, 41, 55)', setStartDate: () => setActiveDatePicker('startDate'),
}), [task.statusColor]); clearDueDate: (e: React.MouseEvent) => {
e.preventDefault();
// Memoize priority style e.stopPropagation();
const priorityStyle = useMemo(() => ({ handleDateChange(null, 'dueDate');
backgroundColor: task.priorityColor ? `${task.priorityColor}20` : 'rgb(229, 231, 235)', },
color: task.priorityColor || 'rgb(31, 41, 55)', clearStartDate: (e: React.MouseEvent) => {
}), [task.priorityColor]); e.preventDefault();
e.stopPropagation();
// Create labels adapter for LabelsSelector handleDateChange(null, 'startDate');
const labelsAdapter = useMemo(() => ({ },
id: task.id, }), [handleDateChange]);
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]);
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => { const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
const baseStyle = { width }; const baseStyle = { width };
@@ -318,11 +340,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
{/* Custom clear button */} {/* Custom clear button */}
{dateValues.due && ( {dateValues.due && (
<button <button
onClick={e => { onClick={datePickerHandlers.clearDueDate}
e.preventDefault();
e.stopPropagation();
handleDateChange(null, 'dueDate');
}}
className={`absolute right-1 top-1/2 transform -translate-y-1/2 w-4 h-4 flex items-center justify-center rounded-full text-xs ${ className={`absolute right-1 top-1/2 transform -translate-y-1/2 w-4 h-4 flex items-center justify-center rounded-full text-xs ${
isDarkMode isDarkMode
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700' ? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
@@ -339,12 +357,12 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 rounded px-2 py-1 transition-colors" className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 rounded px-2 py-1 transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setActiveDatePicker('dueDate'); datePickerHandlers.setDueDate();
}} }}
> >
{formattedDueDate ? ( {formattedDates.due ? (
<span className="text-sm text-gray-500 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
{formattedDueDate} {formattedDates.due}
</span> </span>
) : ( ) : (
<span className="text-sm text-gray-400 dark:text-gray-500"> <span className="text-sm text-gray-400 dark:text-gray-500">
@@ -381,27 +399,9 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
); );
case 'labels': case 'labels':
if (task.labels) console.log('task.labels', task.labels);
return ( return (
<div className="flex items-center gap-1 flex-wrap min-w-0" style={{ ...baseStyle, minWidth: '200px' }}> <div className="flex items-center gap-1 flex-wrap min-w-0" style={{ ...baseStyle, minWidth: '200px' }}>
{task.labels?.map((label, index) => { <TaskLabelsCell labels={task.labels} isDarkMode={isDarkMode} />
const extendedLabel = label as any; // Type assertion for extended properties
return extendedLabel.end && extendedLabel.names && extendedLabel.name ? (
<CustomNumberLabel
key={`${label.id}-${index}`}
labelList={extendedLabel.names}
namesString={extendedLabel.name}
isDarkMode={isDarkMode}
color={label.color}
/>
) : (
<CustomColordLabel
key={`${label.id}-${index}`}
label={label}
isDarkMode={isDarkMode}
/>
);
})}
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} /> <LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
</div> </div>
); );
@@ -459,11 +459,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
{/* Custom clear button */} {/* Custom clear button */}
{dateValues.start && ( {dateValues.start && (
<button <button
onClick={e => { onClick={datePickerHandlers.clearStartDate}
e.preventDefault();
e.stopPropagation();
handleDateChange(null, 'startDate');
}}
className={`absolute right-1 top-1/2 transform -translate-y-1/2 w-4 h-4 flex items-center justify-center rounded-full text-xs ${ className={`absolute right-1 top-1/2 transform -translate-y-1/2 w-4 h-4 flex items-center justify-center rounded-full text-xs ${
isDarkMode isDarkMode
? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700' ? 'text-gray-400 hover:text-gray-200 hover:bg-gray-700'
@@ -480,12 +476,12 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 rounded px-2 py-1 transition-colors" className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 rounded px-2 py-1 transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setActiveDatePicker('startDate'); datePickerHandlers.setStartDate();
}} }}
> >
{formattedStartDate ? ( {formattedDates.start ? (
<span className="text-sm text-gray-500 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
{formattedStartDate} {formattedDates.start}
</span> </span>
) : ( ) : (
<span className="text-sm text-gray-400 dark:text-gray-500"> <span className="text-sm text-gray-400 dark:text-gray-500">
@@ -500,9 +496,9 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'completedDate': case 'completedDate':
return ( return (
<div style={baseStyle}> <div style={baseStyle}>
{formattedCompletedDate && ( {formattedDates.completed && (
<span className="text-sm text-gray-500 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
{formattedCompletedDate} {formattedDates.completed}
</span> </span>
)} )}
</div> </div>
@@ -511,9 +507,9 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'createdDate': case 'createdDate':
return ( return (
<div style={baseStyle}> <div style={baseStyle}>
{formattedCreatedDate && ( {formattedDates.created && (
<span className="text-sm text-gray-500 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
{formattedCreatedDate} {formattedDates.created}
</span> </span>
)} )}
</div> </div>
@@ -522,9 +518,9 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'lastUpdated': case 'lastUpdated':
return ( return (
<div style={baseStyle}> <div style={baseStyle}>
{formattedUpdatedDate && ( {formattedDates.updated && (
<span className="text-sm text-gray-500 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
{formattedUpdatedDate} {formattedDates.updated}
</span> </span>
)} )}
</div> </div>
@@ -543,33 +539,31 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
return null; return null;
} }
}, [ }, [
// Essential props and state
attributes, attributes,
listeners, listeners,
task.task_key,
task.status,
task.priority,
task.phase,
task.reporter,
task.assignee_names,
task.progress,
task.sub_tasks,
taskDisplayName,
statusStyle,
priorityStyle,
formattedDueDate,
formattedStartDate,
formattedCompletedDate,
formattedCreatedDate,
formattedUpdatedDate,
labelsAdapter,
isDarkMode,
convertedTask,
isSelected, isSelected,
handleCheckboxChange, handleCheckboxChange,
activeDatePicker, activeDatePicker,
isDarkMode,
projectId,
// Task data
task,
taskDisplayName,
convertedTask,
// Memoized values
dateValues, dateValues,
formattedDates,
labelsAdapter,
// Handlers
handleDateChange, handleDateChange,
datePickerHandlers,
// Translation
t,
]); ]);
return ( return (