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

View File

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

View File

@@ -9,28 +9,28 @@ interface CustomNumberLabelProps {
color?: string; // Add color prop for label color
}
const CustomNumberLabel: React.FC<CustomNumberLabelProps> = ({
labelList,
namesString,
isDarkMode = false,
color,
}) => {
// 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 (
<Tooltip title={labelList.join(', ')}>
<span
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
style={{ backgroundColor }}
>
{namesString}
</span>
</Tooltip>
);
};
const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>(
({ labelList, namesString, isDarkMode = false, color }, ref) => {
// 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 (
<Tooltip title={labelList.join(', ')}>
<span
ref={ref}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
style={{ backgroundColor }}
>
{namesString}
</span>
</Tooltip>
);
}
);
CustomNumberLabel.displayName = 'CustomNumberLabel';
export default CustomNumberLabel;

View File

@@ -1,3 +1,4 @@
import React from 'react';
import Icon, {
CheckCircleOutlined,
ClockCircleOutlined,
@@ -12,10 +13,23 @@ const iconMap = {
'check-circle': CheckCircleOutlined,
};
const ProjectStatusIcon = ({ iconName, color }: { iconName: string; color: string }) => {
const IconComponent = iconMap[iconName as keyof typeof iconMap];
if (!IconComponent) return null;
return <IconComponent style={{ color: color }} />;
};
interface ProjectStatusIconProps {
iconName: string;
color: string;
}
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;

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 (
<>
<div className="flex items-center gap-0.5 flex-wrap">
{labels.map((label, index) => {
const extendedLabel = label as any;
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" />}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">
{taskDisplayName}
</span>
<Tooltip title={taskDisplayName}>
<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 */}
{!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':
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} />
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
</div>