refactor(components): enhance component structure and add forwardRef support
- Refactored CustomAvatar, CustomColordLabel, CustomNumberLabel, and ProjectStatusIcon components to utilize React.forwardRef for improved ref handling. - Introduced TooltipWrapper component to avoid findDOMNode warnings in React StrictMode, ensuring better compatibility with Ant Design's Tooltip. - Updated TaskRow component to enhance layout and tooltip functionality for task display names, improving user experience and accessibility.
This commit is contained in:
@@ -1,13 +1,21 @@
|
|||||||
|
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 {
|
||||||
|
avatarName: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomAvatar = React.forwardRef<HTMLDivElement, CustomAvatarProps>(
|
||||||
|
({ avatarName, size = 32 }, ref) => {
|
||||||
const avatarCharacter = avatarName[0].toUpperCase();
|
const avatarCharacter = avatarName[0].toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={avatarName}>
|
<Tooltip title={avatarName}>
|
||||||
|
<div ref={ref} style={{ display: 'inline-block' }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: AvatarNamesMap[avatarCharacter],
|
backgroundColor: AvatarNamesMap[avatarCharacter],
|
||||||
@@ -18,8 +26,12 @@ const CustomAvatar = ({ avatarName, size = 32 }: { avatarName: string; size?: nu
|
|||||||
>
|
>
|
||||||
{avatarCharacter}
|
{avatarCharacter}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CustomAvatar.displayName = 'CustomAvatar';
|
||||||
|
|
||||||
export default CustomAvatar;
|
export default CustomAvatar;
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ interface CustomColordLabelProps {
|
|||||||
isDarkMode?: boolean;
|
isDarkMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomColordLabel: React.FC<CustomColordLabelProps> = ({ label, isDarkMode = false }) => {
|
const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelProps>(
|
||||||
|
({ label, isDarkMode = false }, ref) => {
|
||||||
const truncatedName =
|
const truncatedName =
|
||||||
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
|
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 => {
|
||||||
@@ -37,6 +38,7 @@ const CustomColordLabel: React.FC<CustomColordLabelProps> = ({ label, isDarkMode
|
|||||||
return (
|
return (
|
||||||
<Tooltip title={label.name}>
|
<Tooltip title={label.name}>
|
||||||
<span
|
<span
|
||||||
|
ref={ref}
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]"
|
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
@@ -48,6 +50,9 @@ const CustomColordLabel: React.FC<CustomColordLabelProps> = ({ label, isDarkMode
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CustomColordLabel.displayName = 'CustomColordLabel';
|
||||||
|
|
||||||
export default CustomColordLabel;
|
export default CustomColordLabel;
|
||||||
|
|||||||
@@ -9,12 +9,8 @@ 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,
|
|
||||||
isDarkMode = false,
|
|
||||||
color,
|
|
||||||
}) => {
|
|
||||||
// Use provided color, or fall back to NumbersColorMap based on first digit
|
// Use provided color, or fall back to NumbersColorMap based on first digit
|
||||||
const backgroundColor = color || (() => {
|
const backgroundColor = color || (() => {
|
||||||
const firstDigit = namesString.match(/\d/)?.[0] || '0';
|
const firstDigit = namesString.match(/\d/)?.[0] || '0';
|
||||||
@@ -24,6 +20,7 @@ const CustomNumberLabel: React.FC<CustomNumberLabelProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Tooltip title={labelList.join(', ')}>
|
<Tooltip title={labelList.join(', ')}>
|
||||||
<span
|
<span
|
||||||
|
ref={ref}
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
|
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor }}
|
||||||
>
|
>
|
||||||
@@ -31,6 +28,9 @@ const CustomNumberLabel: React.FC<CustomNumberLabelProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CustomNumberLabel.displayName = 'CustomNumberLabel';
|
||||||
|
|
||||||
export default CustomNumberLabel;
|
export default CustomNumberLabel;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
iconName: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectStatusIcon = React.forwardRef<HTMLSpanElement, ProjectStatusIconProps>(
|
||||||
|
({ iconName, color }, ref) => {
|
||||||
const IconComponent = iconMap[iconName as keyof typeof iconMap];
|
const IconComponent = iconMap[iconName as keyof typeof iconMap];
|
||||||
if (!IconComponent) return null;
|
if (!IconComponent) return null;
|
||||||
return <IconComponent style={{ color: color }} />;
|
return (
|
||||||
};
|
<span ref={ref} style={{ display: 'inline-block' }}>
|
||||||
|
<IconComponent style={{ color: color }} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ProjectStatusIcon.displayName = 'ProjectStatusIcon';
|
||||||
|
|
||||||
export default ProjectStatusIcon;
|
export default ProjectStatusIcon;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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}>
|
||||||
|
<span
|
||||||
|
className="text-sm text-gray-700 dark:text-gray-300 truncate cursor-pointer"
|
||||||
|
style={{
|
||||||
|
maxWidth: '200px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{taskDisplayName}
|
{taskDisplayName}
|
||||||
</span>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user