Merge pull request #239 from Worklenz/fix/task-drag-and-drop-improvement

refactor(components): enhance component structure and add forwardRef …
This commit is contained in:
Chamika J
2025-07-07 07:16:57 +05:30
committed by GitHub
6 changed files with 157 additions and 88 deletions

View File

@@ -1,25 +1,37 @@
import React from 'react';
import Tooltip from 'antd/es/tooltip'; import Tooltip from 'antd/es/tooltip';
import Avatar from 'antd/es/avatar'; import Avatar from 'antd/es/avatar';
import { AvatarNamesMap } from '../shared/constants'; import { AvatarNamesMap } from '../shared/constants';
const CustomAvatar = ({ avatarName, size = 32 }: { avatarName: string; size?: number }) => { interface CustomAvatarProps {
const avatarCharacter = avatarName[0].toUpperCase(); avatarName: string;
size?: number;
}
return ( const CustomAvatar = React.forwardRef<HTMLDivElement, CustomAvatarProps>(
<Tooltip title={avatarName}> ({ avatarName, size = 32 }, ref) => {
<Avatar const avatarCharacter = avatarName[0].toUpperCase();
style={{
backgroundColor: AvatarNamesMap[avatarCharacter], return (
verticalAlign: 'middle', <Tooltip title={avatarName}>
width: size, <div ref={ref} style={{ display: 'inline-block' }}>
height: size, <Avatar
}} style={{
> backgroundColor: AvatarNamesMap[avatarCharacter],
{avatarCharacter} verticalAlign: 'middle',
</Avatar> width: size,
</Tooltip> height: size,
); }}
}; >
{avatarCharacter}
</Avatar>
</div>
</Tooltip>
);
}
);
CustomAvatar.displayName = 'CustomAvatar';
export default CustomAvatar; export default CustomAvatar;

View File

@@ -8,46 +8,51 @@ interface CustomColordLabelProps {
isDarkMode?: boolean; isDarkMode?: boolean;
} }
const CustomColordLabel: React.FC<CustomColordLabelProps> = ({ label, isDarkMode = false }) => { const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelProps>(
const truncatedName = ({ label, isDarkMode = false }, ref) => {
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name; const truncatedName =
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
// Ensure we have a valid color, fallback to a default if not // Handle different color property names for different types
const backgroundColor = label.color || '#6b7280'; // Default to gray-500 if no color const backgroundColor = (label as Label).color || (label as ITaskLabel).color_code || '#6b7280'; // Default to gray-500 if no color
// Function to determine if we should use white or black text based on background color // Function to determine if we should use white or black text based on background color
const getTextColor = (bgColor: string): string => { const getTextColor = (bgColor: string): string => {
// Remove # if present // Remove # if present
const color = bgColor.replace('#', ''); const color = bgColor.replace('#', '');
// Convert to RGB // Convert to RGB
const r = parseInt(color.substr(0, 2), 16); const r = parseInt(color.substr(0, 2), 16);
const g = parseInt(color.substr(2, 2), 16); const g = parseInt(color.substr(2, 2), 16);
const b = parseInt(color.substr(4, 2), 16); const b = parseInt(color.substr(4, 2), 16);
// Calculate luminance // Calculate luminance
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
// Return white for dark backgrounds, black for light backgrounds // Return white for dark backgrounds, black for light backgrounds
return luminance > 0.5 ? '#000000' : '#ffffff'; return luminance > 0.5 ? '#000000' : '#ffffff';
}; };
const textColor = getTextColor(backgroundColor); const textColor = getTextColor(backgroundColor);
return ( return (
<Tooltip title={label.name}> <Tooltip title={label.name}>
<span <span
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]" ref={ref}
style={{ className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]"
backgroundColor, style={{
color: textColor, backgroundColor,
border: `1px solid ${backgroundColor}`, color: textColor,
}} border: `1px solid ${backgroundColor}`,
> }}
<span className="truncate">{truncatedName}</span> >
</span> <span className="truncate">{truncatedName}</span>
</Tooltip> </span>
); </Tooltip>
}; );
}
);
CustomColordLabel.displayName = 'CustomColordLabel';
export default CustomColordLabel; export default CustomColordLabel;

View File

@@ -9,28 +9,28 @@ interface CustomNumberLabelProps {
color?: string; // Add color prop for label color color?: string; // Add color prop for label color
} }
const CustomNumberLabel: React.FC<CustomNumberLabelProps> = ({ const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>(
labelList, ({ labelList, namesString, isDarkMode = false, color }, ref) => {
namesString, // Use provided color, or fall back to NumbersColorMap based on first digit
isDarkMode = false, const backgroundColor = color || (() => {
color, const firstDigit = namesString.match(/\d/)?.[0] || '0';
}) => { return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
// Use provided color, or fall back to NumbersColorMap based on first digit })();
const backgroundColor = color || (() => {
const firstDigit = namesString.match(/\d/)?.[0] || '0';
return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
})();
return ( return (
<Tooltip title={labelList.join(', ')}> <Tooltip title={labelList.join(', ')}>
<span <span
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help" ref={ref}
style={{ backgroundColor }} className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
> style={{ backgroundColor }}
{namesString} >
</span> {namesString}
</Tooltip> </span>
); </Tooltip>
}; );
}
);
CustomNumberLabel.displayName = 'CustomNumberLabel';
export default CustomNumberLabel; export default CustomNumberLabel;

View File

@@ -1,3 +1,4 @@
import React from 'react';
import Icon, { import Icon, {
CheckCircleOutlined, CheckCircleOutlined,
ClockCircleOutlined, ClockCircleOutlined,
@@ -12,10 +13,23 @@ const iconMap = {
'check-circle': CheckCircleOutlined, 'check-circle': CheckCircleOutlined,
}; };
const ProjectStatusIcon = ({ iconName, color }: { iconName: string; color: string }) => { interface ProjectStatusIconProps {
const IconComponent = iconMap[iconName as keyof typeof iconMap]; iconName: string;
if (!IconComponent) return null; color: string;
return <IconComponent style={{ color: color }} />; }
};
const ProjectStatusIcon = React.forwardRef<HTMLSpanElement, ProjectStatusIconProps>(
({ iconName, color }, ref) => {
const IconComponent = iconMap[iconName as keyof typeof iconMap];
if (!IconComponent) return null;
return (
<span ref={ref} style={{ display: 'inline-block' }}>
<IconComponent style={{ color: color }} />
</span>
);
}
);
ProjectStatusIcon.displayName = 'ProjectStatusIcon';
export default ProjectStatusIcon; export default ProjectStatusIcon;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Tooltip, TooltipProps } from 'antd';
interface TooltipWrapperProps extends Omit<TooltipProps, 'children'> {
children: React.ReactElement;
}
/**
* TooltipWrapper - A wrapper component that helps avoid findDOMNode warnings in React StrictMode
*
* This component ensures that the child element can properly receive refs from Ant Design's Tooltip
* by wrapping it in a div with a ref when necessary.
*/
const TooltipWrapper = React.forwardRef<HTMLDivElement, TooltipWrapperProps>(
({ children, ...tooltipProps }, ref) => {
return (
<Tooltip {...tooltipProps}>
<div ref={ref} style={{ display: 'inline-block' }}>
{children}
</div>
</Tooltip>
);
}
);
TooltipWrapper.displayName = 'TooltipWrapper';
export default TooltipWrapper;

View File

@@ -54,7 +54,7 @@ const TaskLabelsCell: React.FC<TaskLabelsCellProps> = memo(({ labels, isDarkMode
} }
return ( return (
<> <div className="flex items-center gap-0.5 flex-wrap">
{labels.map((label, index) => { {labels.map((label, index) => {
const extendedLabel = label as any; const extendedLabel = label as any;
return extendedLabel.end && extendedLabel.names && extendedLabel.name ? ( return extendedLabel.end && extendedLabel.names && extendedLabel.name ? (
@@ -73,7 +73,7 @@ const TaskLabelsCell: React.FC<TaskLabelsCellProps> = memo(({ labels, isDarkMode
/> />
); );
})} })}
</> </div>
); );
}); });
@@ -322,9 +322,19 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
{isSubtask && <div className="w-2" />} {isSubtask && <div className="w-2" />}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-700 dark:text-gray-300 truncate"> <Tooltip title={taskDisplayName}>
{taskDisplayName} <span
</span> className="text-sm text-gray-700 dark:text-gray-300 truncate cursor-pointer"
style={{
maxWidth: '200px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{taskDisplayName}
</span>
</Tooltip>
{/* Subtask count indicator - only show if count > 1 */} {/* Subtask count indicator - only show if count > 1 */}
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count !== 0 && ( {!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count !== 0 && (
@@ -552,7 +562,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'labels': case '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-0.5 flex-wrap min-w-0" style={{ ...baseStyle, minWidth: '150px' }}>
<TaskLabelsCell labels={task.labels} isDarkMode={isDarkMode} /> <TaskLabelsCell labels={task.labels} isDarkMode={isDarkMode} />
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} /> <LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
</div> </div>