Skip to content

Commit 5d9e960

Browse files
committed
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.
1 parent f9926e7 commit 5d9e960

6 files changed

Lines changed: 157 additions & 88 deletions

File tree

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
1+
import React from 'react';
12
import Tooltip from 'antd/es/tooltip';
23
import Avatar from 'antd/es/avatar';
34

45
import { AvatarNamesMap } from '../shared/constants';
56

6-
const CustomAvatar = ({ avatarName, size = 32 }: { avatarName: string; size?: number }) => {
7-
const avatarCharacter = avatarName[0].toUpperCase();
7+
interface CustomAvatarProps {
8+
avatarName: string;
9+
size?: number;
10+
}
811

9-
return (
10-
<Tooltip title={avatarName}>
11-
<Avatar
12-
style={{
13-
backgroundColor: AvatarNamesMap[avatarCharacter],
14-
verticalAlign: 'middle',
15-
width: size,
16-
height: size,
17-
}}
18-
>
19-
{avatarCharacter}
20-
</Avatar>
21-
</Tooltip>
22-
);
23-
};
12+
const CustomAvatar = React.forwardRef<HTMLDivElement, CustomAvatarProps>(
13+
({ avatarName, size = 32 }, ref) => {
14+
const avatarCharacter = avatarName[0].toUpperCase();
15+
16+
return (
17+
<Tooltip title={avatarName}>
18+
<div ref={ref} style={{ display: 'inline-block' }}>
19+
<Avatar
20+
style={{
21+
backgroundColor: AvatarNamesMap[avatarCharacter],
22+
verticalAlign: 'middle',
23+
width: size,
24+
height: size,
25+
}}
26+
>
27+
{avatarCharacter}
28+
</Avatar>
29+
</div>
30+
</Tooltip>
31+
);
32+
}
33+
);
34+
35+
CustomAvatar.displayName = 'CustomAvatar';
2436

2537
export default CustomAvatar;

worklenz-frontend/src/components/CustomColordLabel.tsx

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,51 @@ interface CustomColordLabelProps {
88
isDarkMode?: boolean;
99
}
1010

11-
const CustomColordLabel: React.FC<CustomColordLabelProps> = ({ label, isDarkMode = false }) => {
12-
const truncatedName =
13-
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
11+
const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelProps>(
12+
({ label, isDarkMode = false }, ref) => {
13+
const truncatedName =
14+
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
1415

15-
// Ensure we have a valid color, fallback to a default if not
16-
const backgroundColor = label.color || '#6b7280'; // Default to gray-500 if no color
17-
18-
// Function to determine if we should use white or black text based on background color
19-
const getTextColor = (bgColor: string): string => {
20-
// Remove # if present
21-
const color = bgColor.replace('#', '');
16+
// Handle different color property names for different types
17+
const backgroundColor = (label as Label).color || (label as ITaskLabel).color_code || '#6b7280'; // Default to gray-500 if no color
2218

23-
// Convert to RGB
24-
const r = parseInt(color.substr(0, 2), 16);
25-
const g = parseInt(color.substr(2, 2), 16);
26-
const b = parseInt(color.substr(4, 2), 16);
27-
28-
// Calculate luminance
29-
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
30-
31-
// Return white for dark backgrounds, black for light backgrounds
32-
return luminance > 0.5 ? '#000000' : '#ffffff';
33-
};
19+
// Function to determine if we should use white or black text based on background color
20+
const getTextColor = (bgColor: string): string => {
21+
// Remove # if present
22+
const color = bgColor.replace('#', '');
23+
24+
// Convert to RGB
25+
const r = parseInt(color.substr(0, 2), 16);
26+
const g = parseInt(color.substr(2, 2), 16);
27+
const b = parseInt(color.substr(4, 2), 16);
28+
29+
// Calculate luminance
30+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
31+
32+
// Return white for dark backgrounds, black for light backgrounds
33+
return luminance > 0.5 ? '#000000' : '#ffffff';
34+
};
35+
36+
const textColor = getTextColor(backgroundColor);
3437

35-
const textColor = getTextColor(backgroundColor);
38+
return (
39+
<Tooltip title={label.name}>
40+
<span
41+
ref={ref}
42+
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]"
43+
style={{
44+
backgroundColor,
45+
color: textColor,
46+
border: `1px solid ${backgroundColor}`,
47+
}}
48+
>
49+
<span className="truncate">{truncatedName}</span>
50+
</span>
51+
</Tooltip>
52+
);
53+
}
54+
);
3655

37-
return (
38-
<Tooltip title={label.name}>
39-
<span
40-
className="inline-flex items-center px-2 py-0.5 rounded-sm text-xs font-medium shrink-0 max-w-[120px]"
41-
style={{
42-
backgroundColor,
43-
color: textColor,
44-
border: `1px solid ${backgroundColor}`,
45-
}}
46-
>
47-
<span className="truncate">{truncatedName}</span>
48-
</span>
49-
</Tooltip>
50-
);
51-
};
56+
CustomColordLabel.displayName = 'CustomColordLabel';
5257

5358
export default CustomColordLabel;

worklenz-frontend/src/components/CustomNumberLabel.tsx

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,28 @@ interface CustomNumberLabelProps {
99
color?: string; // Add color prop for label color
1010
}
1111

12-
const CustomNumberLabel: React.FC<CustomNumberLabelProps> = ({
13-
labelList,
14-
namesString,
15-
isDarkMode = false,
16-
color,
17-
}) => {
18-
// Use provided color, or fall back to NumbersColorMap based on first digit
19-
const backgroundColor = color || (() => {
20-
const firstDigit = namesString.match(/\d/)?.[0] || '0';
21-
return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
22-
})();
23-
24-
return (
25-
<Tooltip title={labelList.join(', ')}>
26-
<span
27-
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
28-
style={{ backgroundColor }}
29-
>
30-
{namesString}
31-
</span>
32-
</Tooltip>
33-
);
34-
};
12+
const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>(
13+
({ labelList, namesString, isDarkMode = false, color }, ref) => {
14+
// Use provided color, or fall back to NumbersColorMap based on first digit
15+
const backgroundColor = color || (() => {
16+
const firstDigit = namesString.match(/\d/)?.[0] || '0';
17+
return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
18+
})();
19+
20+
return (
21+
<Tooltip title={labelList.join(', ')}>
22+
<span
23+
ref={ref}
24+
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white cursor-help"
25+
style={{ backgroundColor }}
26+
>
27+
{namesString}
28+
</span>
29+
</Tooltip>
30+
);
31+
}
32+
);
33+
34+
CustomNumberLabel.displayName = 'CustomNumberLabel';
3535

3636
export default CustomNumberLabel;

worklenz-frontend/src/components/common/project-status-icon/project-status-icon.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react';
12
import Icon, {
23
CheckCircleOutlined,
34
ClockCircleOutlined,
@@ -12,10 +13,23 @@ const iconMap = {
1213
'check-circle': CheckCircleOutlined,
1314
};
1415

15-
const ProjectStatusIcon = ({ iconName, color }: { iconName: string; color: string }) => {
16-
const IconComponent = iconMap[iconName as keyof typeof iconMap];
17-
if (!IconComponent) return null;
18-
return <IconComponent style={{ color: color }} />;
19-
};
16+
interface ProjectStatusIconProps {
17+
iconName: string;
18+
color: string;
19+
}
20+
21+
const ProjectStatusIcon = React.forwardRef<HTMLSpanElement, ProjectStatusIconProps>(
22+
({ iconName, color }, ref) => {
23+
const IconComponent = iconMap[iconName as keyof typeof iconMap];
24+
if (!IconComponent) return null;
25+
return (
26+
<span ref={ref} style={{ display: 'inline-block' }}>
27+
<IconComponent style={{ color: color }} />
28+
</span>
29+
);
30+
}
31+
);
32+
33+
ProjectStatusIcon.displayName = 'ProjectStatusIcon';
2034

2135
export default ProjectStatusIcon;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { Tooltip, TooltipProps } from 'antd';
3+
4+
interface TooltipWrapperProps extends Omit<TooltipProps, 'children'> {
5+
children: React.ReactElement;
6+
}
7+
8+
/**
9+
* TooltipWrapper - A wrapper component that helps avoid findDOMNode warnings in React StrictMode
10+
*
11+
* This component ensures that the child element can properly receive refs from Ant Design's Tooltip
12+
* by wrapping it in a div with a ref when necessary.
13+
*/
14+
const TooltipWrapper = React.forwardRef<HTMLDivElement, TooltipWrapperProps>(
15+
({ children, ...tooltipProps }, ref) => {
16+
return (
17+
<Tooltip {...tooltipProps}>
18+
<div ref={ref} style={{ display: 'inline-block' }}>
19+
{children}
20+
</div>
21+
</Tooltip>
22+
);
23+
}
24+
);
25+
26+
TooltipWrapper.displayName = 'TooltipWrapper';
27+
28+
export default TooltipWrapper;

worklenz-frontend/src/components/task-list-v2/TaskRow.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const TaskLabelsCell: React.FC<TaskLabelsCellProps> = memo(({ labels, isDarkMode
5454
}
5555

5656
return (
57-
<>
57+
<div className="flex items-center gap-0.5 flex-wrap">
5858
{labels.map((label, index) => {
5959
const extendedLabel = label as any;
6060
return extendedLabel.end && extendedLabel.names && extendedLabel.name ? (
@@ -73,7 +73,7 @@ const TaskLabelsCell: React.FC<TaskLabelsCellProps> = memo(({ labels, isDarkMode
7373
/>
7474
);
7575
})}
76-
</>
76+
</div>
7777
);
7878
});
7979

@@ -322,9 +322,19 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
322322
{isSubtask && <div className="w-2" />}
323323

324324
<div className="flex items-center gap-2">
325-
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">
326-
{taskDisplayName}
327-
</span>
325+
<Tooltip title={taskDisplayName}>
326+
<span
327+
className="text-sm text-gray-700 dark:text-gray-300 truncate cursor-pointer"
328+
style={{
329+
maxWidth: '200px',
330+
whiteSpace: 'nowrap',
331+
overflow: 'hidden',
332+
textOverflow: 'ellipsis',
333+
}}
334+
>
335+
{taskDisplayName}
336+
</span>
337+
</Tooltip>
328338

329339
{/* Subtask count indicator - only show if count > 1 */}
330340
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count !== 0 && (
@@ -552,7 +562,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
552562

553563
case 'labels':
554564
return (
555-
<div className="flex items-center gap-1 flex-wrap min-w-0" style={{ ...baseStyle, minWidth: '200px' }}>
565+
<div className="flex items-center gap-0.5 flex-wrap min-w-0" style={{ ...baseStyle, minWidth: '150px' }}>
556566
<TaskLabelsCell labels={task.labels} isDarkMode={isDarkMode} />
557567
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
558568
</div>

0 commit comments

Comments
 (0)