feat(task-management): enhance priority and status dropdowns with fallback rendering

- Added helper functions to display names and colors for raw priority and status values, improving user experience.
- Implemented fallback rendering for dropdowns to handle cases where the priority or status is not found in the list.
- Updated task row to display formatted priority and status values, ensuring consistency across the UI.
- Enhanced error handling in task list rendering to provide meaningful feedback when data is unavailable.
This commit is contained in:
chamikaJ
2025-06-27 17:39:47 +05:30
parent c37ffd6991
commit e3324f0707
4 changed files with 353 additions and 67 deletions

View File

@@ -33,11 +33,59 @@ const PriorityDropdown = ({ task, teamId }: PriorityDropdownProps) => {
);
};
useEffect(() => {
const foundPriority = priorityList.find(priority => priority.id === task.priority);
setSelectedPriority(foundPriority);
// Helper function to get display name for raw priority values
const getPriorityDisplayName = (priority: string | undefined) => {
if (!priority) return 'Medium';
// Handle raw priority values from backend
const priorityDisplayMap: Record<string, string> = {
'critical': 'Critical',
'high': 'High',
'medium': 'Medium',
'low': 'Low',
};
return priorityDisplayMap[priority.toLowerCase()] || priority;
};
// Helper function to get priority color for raw priority values
const getPriorityColor = (priority: string | undefined) => {
if (!priority) return themeMode === 'dark' ? '#434343' : '#f0f0f0';
// Default colors for raw priority values
const priorityColorMap: Record<string, { light: string; dark: string }> = {
'critical': { light: '#ff4d4f', dark: '#ff7875' },
'high': { light: '#fa8c16', dark: '#ffa940' },
'medium': { light: '#1890ff', dark: '#40a9ff' },
'low': { light: '#52c41a', dark: '#73d13d' },
};
const colorPair = priorityColorMap[priority.toLowerCase()];
return colorPair ? (themeMode === 'dark' ? colorPair.dark : colorPair.light) : (themeMode === 'dark' ? '#434343' : '#f0f0f0');
};
// Find matching priority from the list, or use raw value
const currentPriority = useMemo(() => {
if (!task.priority) return null;
// First try to find by ID
const priorityById = priorityList.find(priority => priority.id === task.priority);
if (priorityById) return priorityById;
// Then try to find by name (case insensitive)
const priorityByName = priorityList.find(priority =>
priority.name.toLowerCase() === task.priority?.toLowerCase()
);
if (priorityByName) return priorityByName;
// Return null if no match found (will use fallback rendering)
return null;
}, [task.priority, priorityList]);
useEffect(() => {
setSelectedPriority(currentPriority || undefined);
}, [currentPriority]);
const options = useMemo(
() =>
priorityList.map(priority => ({
@@ -74,36 +122,51 @@ const PriorityDropdown = ({ task, teamId }: PriorityDropdownProps) => {
[priorityList, themeMode]
);
// If we have a valid priority from the list, render the dropdown
if (currentPriority && priorityList.length > 0) {
return (
<Select
variant="borderless"
value={currentPriority.id}
onChange={handlePriorityChange}
dropdownStyle={{ borderRadius: 8, minWidth: 150, maxWidth: 200 }}
style={{
backgroundColor:
themeMode === 'dark'
? currentPriority.color_code_dark
: currentPriority.color_code + ALPHA_CHANNEL,
borderRadius: 16,
height: 22,
}}
labelRender={() => {
return (
<Typography.Text style={{ fontSize: 13, color: '#383838' }}>
{currentPriority.name}
</Typography.Text>
);
}}
options={options}
/>
);
}
// Fallback rendering for raw priority values or when priority list is not loaded
return (
<>
{task.priority && (
<Select
variant="borderless"
value={task.priority}
onChange={handlePriorityChange}
dropdownStyle={{ borderRadius: 8, minWidth: 150, maxWidth: 200 }}
style={{
backgroundColor:
themeMode === 'dark'
? selectedPriority?.color_code_dark
: selectedPriority?.color_code + ALPHA_CHANNEL,
borderRadius: 16,
height: 22,
}}
labelRender={value => {
const priority = priorityList.find(priority => priority.id === value.value);
return priority ? (
<Typography.Text style={{ fontSize: 13, color: '#383838' }}>
{priority.name}
</Typography.Text>
) : (
''
);
}}
options={options}
/>
)}
</>
<div
className="px-2 py-1 text-xs rounded"
style={{
backgroundColor: getPriorityColor(task.priority) + ALPHA_CHANNEL,
borderRadius: 16,
height: 22,
display: 'flex',
alignItems: 'center',
fontSize: 13,
color: '#383838',
minWidth: 60,
}}
>
{getPriorityDisplayName(task.priority)}
</div>
);
};

View File

@@ -36,6 +36,59 @@ const StatusDropdown = ({ task, teamId }: StatusDropdownProps) => {
return getCurrentGroup().value === GROUP_BY_STATUS_VALUE;
};
// Helper function to get display name for raw status values
const getStatusDisplayName = (status: string | undefined) => {
if (!status) return 'To Do';
// Handle raw status values from backend
const statusDisplayMap: Record<string, string> = {
'to_do': 'To Do',
'todo': 'To Do',
'doing': 'Doing',
'in_progress': 'In Progress',
'done': 'Done',
'completed': 'Completed',
};
return statusDisplayMap[status.toLowerCase()] || status;
};
// Helper function to get status color for raw status values
const getStatusColor = (status: string | undefined) => {
if (!status) return themeMode === 'dark' ? '#434343' : '#f0f0f0';
// Default colors for raw status values
const statusColorMap: Record<string, { light: string; dark: string }> = {
'to_do': { light: '#f0f0f0', dark: '#434343' },
'todo': { light: '#f0f0f0', dark: '#434343' },
'doing': { light: '#1890ff', dark: '#177ddc' },
'in_progress': { light: '#1890ff', dark: '#177ddc' },
'done': { light: '#52c41a', dark: '#389e0d' },
'completed': { light: '#52c41a', dark: '#389e0d' },
};
const colorPair = statusColorMap[status.toLowerCase()];
return colorPair ? (themeMode === 'dark' ? colorPair.dark : colorPair.light) : (themeMode === 'dark' ? '#434343' : '#f0f0f0');
};
// Find matching status from the list, or use raw value
const currentStatus = useMemo(() => {
if (!task.status) return null;
// First try to find by ID
const statusById = statusList.find(status => status.id === task.status);
if (statusById) return statusById;
// Then try to find by name (case insensitive)
const statusByName = statusList.find(status =>
status.name.toLowerCase() === task.status?.toLowerCase()
);
if (statusByName) return statusByName;
// Return null if no match found (will use fallback rendering)
return null;
}, [task.status, statusList]);
const options = useMemo(
() =>
statusList.map(status => ({
@@ -46,31 +99,49 @@ const StatusDropdown = ({ task, teamId }: StatusDropdownProps) => {
[statusList, themeMode]
);
// If we have a valid status from the list, render the dropdown
if (currentStatus && statusList.length > 0) {
return (
<Select
variant="borderless"
value={currentStatus.id}
onChange={handleStatusChange}
dropdownStyle={{ borderRadius: 8, minWidth: 150, maxWidth: 200 }}
style={{
backgroundColor: themeMode === 'dark' ? currentStatus.color_code_dark : currentStatus.color_code,
borderRadius: 16,
height: 22,
}}
labelRender={() => {
return <span style={{ fontSize: 13 }}>{currentStatus.name}</span>;
}}
options={options}
optionRender={(option) => (
<Flex align="center">
{option.label}
</Flex>
)}
/>
);
}
// Fallback rendering for raw status values or when status list is not loaded
return (
<>
{task.status && (
<Select
variant="borderless"
value={task.status}
onChange={handleStatusChange}
dropdownStyle={{ borderRadius: 8, minWidth: 150, maxWidth: 200 }}
style={{
backgroundColor: themeMode === 'dark' ? task.status_color_dark : task.status_color,
borderRadius: 16,
height: 22,
}}
labelRender={status => {
return status ? <span style={{ fontSize: 13 }}>{status.label}</span> : '';
}}
options={options}
optionRender={(option) => (
<Flex align="center">
{option.label}
</Flex>
)}
/>
)}
</>
<div
className="px-2 py-1 text-xs rounded"
style={{
backgroundColor: getStatusColor(task.status),
borderRadius: 16,
height: 22,
display: 'flex',
alignItems: 'center',
fontSize: 13,
color: '#383838',
minWidth: 60,
}}
>
{getStatusDisplayName(task.status)}
</div>
);
};

View File

@@ -481,7 +481,10 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
return (
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
{task.status || 'Todo'}
{task.status === 'todo' ? 'To Do' :
task.status === 'doing' ? 'Doing' :
task.status === 'done' ? 'Done' :
task.status || 'To Do'}
</div>
</div>
);
@@ -499,7 +502,11 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
return (
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
{task.priority || 'Medium'}
{task.priority === 'critical' ? 'Critical' :
task.priority === 'high' ? 'High' :
task.priority === 'medium' ? 'Medium' :
task.priority === 'low' ? 'Low' :
task.priority || 'Medium'}
</div>
</div>
);
@@ -513,6 +520,81 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
</div>
);
case 'members':
return (
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
<div className="flex items-center gap-2">
{task.assignee_names && task.assignee_names.length > 0 ? (
<div className="flex items-center gap-1">
{task.assignee_names.slice(0, 3).map((member, index) => (
<div
key={index}
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
isDarkMode ? 'bg-gray-600 text-gray-200' : 'bg-gray-200 text-gray-700'
}`}
title={member.name}
>
{member.name ? member.name.charAt(0).toUpperCase() : '?'}
</div>
))}
{task.assignee_names.length > 3 && (
<div
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
isDarkMode ? 'bg-gray-600 text-gray-200' : 'bg-gray-200 text-gray-700'
}`}
>
+{task.assignee_names.length - 3}
</div>
)}
</div>
) : (
<div className={`w-6 h-6 rounded-full border-2 border-dashed flex items-center justify-center ${
isDarkMode ? 'border-gray-600' : 'border-gray-300'
}`}>
<UserOutlined className={`text-xs ${isDarkMode ? 'text-gray-600' : 'text-gray-400'}`} />
</div>
)}
</div>
</div>
);
case 'labels':
return (
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
<div className="flex items-center gap-1 flex-wrap">
{task.labels && task.labels.length > 0 ? (
task.labels.slice(0, 3).map((label, index) => (
<div
key={index}
className={`px-2 py-1 text-xs rounded ${
isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'
}`}
style={{
backgroundColor: label.color || (isDarkMode ? '#374151' : '#f3f4f6'),
color: label.color ? '#ffffff' : undefined
}}
>
{label.name || 'Label'}
</div>
))
) : (
<div className={`px-2 py-1 text-xs rounded border-dashed border ${
isDarkMode ? 'border-gray-600 text-gray-600' : 'border-gray-300 text-gray-400'
}`}>
No labels
</div>
)}
{task.labels && task.labels.length > 3 && (
<div className={`px-2 py-1 text-xs rounded ${
isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'
}`}>
+{task.labels.length - 3}
</div>
)}
</div>
</div>
);
default:
// For non-essential columns, show placeholder during initial load
return (

View File

@@ -159,16 +159,86 @@ const CustomCell = React.memo(({
renderColumnContent: any;
updateTaskCustomColumnValue: (taskId: string, columnKey: string, value: string) => void;
}) => {
if (column.custom_column && column.key && column.pinned) {
return renderCustomColumnContent(
column.custom_column_obj || {},
column.custom_column_obj?.fieldType,
task,
column.key,
updateTaskCustomColumnValue
);
try {
if (column.custom_column && column.key && column.pinned) {
return renderCustomColumnContent(
column.custom_column_obj || {},
column.custom_column_obj?.fieldType,
task,
column.key,
updateTaskCustomColumnValue
);
}
const result = renderColumnContent(column.key || '', task, isSubtask);
// If renderColumnContent returns null or undefined, provide a fallback
if (result === null || result === undefined) {
// Handle specific column types with fallbacks
switch (column.key) {
case 'STATUS':
return (
<div className="px-2 py-1 text-xs rounded bg-gray-100 text-gray-600">
{task.status_name || task.status || 'To Do'}
</div>
);
case 'PRIORITY':
return (
<div className="px-2 py-1 text-xs rounded bg-gray-100 text-gray-600">
{task.priority_name || task.priority || 'Medium'}
</div>
);
case 'ASSIGNEES':
return (
<div className="text-xs text-gray-500">
{task.assignees?.length ? `${task.assignees.length} assignee(s)` : 'No assignees'}
</div>
);
case 'LABELS':
return (
<div className="text-xs text-gray-500">
{task.labels?.length ? `${task.labels.length} label(s)` : 'No labels'}
</div>
);
default:
return <div className="text-xs text-gray-400">-</div>;
}
}
return result;
} catch (error) {
console.error('Error rendering task cell:', error, { column: column.key, task: task.id });
// Fallback rendering for errors
switch (column.key) {
case 'STATUS':
return (
<div className="px-2 py-1 text-xs rounded bg-red-100 text-red-600">
{task.status_name || task.status || 'Error'}
</div>
);
case 'PRIORITY':
return (
<div className="px-2 py-1 text-xs rounded bg-red-100 text-red-600">
{task.priority_name || task.priority || 'Error'}
</div>
);
case 'ASSIGNEES':
return (
<div className="text-xs text-red-500">
Error loading assignees
</div>
);
case 'LABELS':
return (
<div className="text-xs text-red-500">
Error loading labels
</div>
);
default:
return <div className="text-xs text-red-400">Error</div>;
}
}
return renderColumnContent(column.key || '', task, isSubtask);
});
// First, let's extract the custom column cell to a completely separate component