Merge pull request #233 from Worklenz/fix/task-drag-and-drop-improvement
feat(task-management): improve dropdown behavior and enhance task row…
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user