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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user