feat(tasks): add color_code_dark to task groups and enhance task list display

- Introduced color_code_dark property to task groups for improved theming support.
- Updated task list components to utilize color_code_dark based on the current theme mode.
- Enhanced empty state handling in task list to dynamically create an unmapped group for better user experience.
- Refactored task management slice to support dynamic group creation for unmapped tasks.
This commit is contained in:
chamikaJ
2025-07-22 16:08:41 +05:30
parent 256f1eb3a9
commit 354b9422ed
8 changed files with 194 additions and 94 deletions

View File

@@ -16,6 +16,7 @@ export interface ITaskGroup {
start_date?: string;
end_date?: string;
color_code: string;
color_code_dark: string;
category_id: string | null;
old_category_id?: string;
todo_progress?: number;

View File

@@ -110,20 +110,20 @@ export default class TasksControllerV2 extends TasksControllerBase {
private static getQuery(userId: string, options: ParsedQs) {
// Determine which sort column to use based on grouping
const groupBy = options.group || 'status';
let defaultSortColumn = 'sort_order';
const groupBy = options.group || "status";
let defaultSortColumn = "sort_order";
switch (groupBy) {
case 'status':
defaultSortColumn = 'status_sort_order';
case "status":
defaultSortColumn = "status_sort_order";
break;
case 'priority':
defaultSortColumn = 'priority_sort_order';
case "priority":
defaultSortColumn = "priority_sort_order";
break;
case 'phase':
defaultSortColumn = 'phase_sort_order';
case "phase":
defaultSortColumn = "phase_sort_order";
break;
default:
defaultSortColumn = 'sort_order';
defaultSortColumn = "sort_order";
}
const searchField = options.search ? ["t.name", "CONCAT((SELECT key FROM projects WHERE id = t.project_id), '-', task_no)"] : defaultSortColumn;
@@ -436,6 +436,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
name: UNMAPPED,
category_id: null,
color_code: "#fbc84c69",
color_code_dark: "#fbc84c69",
tasks: unmapped
};
}
@@ -1008,7 +1009,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
const startTime = performance.now();
const isSubTasks = !!req.query.parent_task;
const groupBy = (req.query.group || GroupBy.STATUS) as string;
const archived = req.query.archived === "true";
// PERFORMANCE OPTIMIZATION: Skip expensive progress calculation by default
// Progress values are already calculated and stored in the database
@@ -1017,23 +1017,17 @@ export default class TasksControllerV2 extends TasksControllerBase {
const shouldRefreshProgress = req.query.refresh_progress === "true";
if (shouldRefreshProgress && req.params.id) {
const progressStartTime = performance.now();
await this.refreshProjectTaskProgressValues(req.params.id);
const progressEndTime = performance.now();
}
const queryStartTime = performance.now();
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
const result = await db.query(q, params);
const tasks = [...result.rows];
const queryEndTime = performance.now();
// Get groups metadata dynamically from database
const groupsStartTime = performance.now();
const groups = await this.getGroups(groupBy, req.params.id);
const groupsEndTime = performance.now();
// Create priority value to name mapping
const priorityMap: Record<string, string> = {
@@ -1051,10 +1045,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
}
}
// Transform tasks with all necessary data preprocessing
const transformStartTime = performance.now();
const transformedTasks = tasks.map((task, index) => {
// Update task with calculated values (lightweight version)
TasksControllerV2.updateTaskViewModel(task);
@@ -1125,10 +1116,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
reporter: task.reporter || null,
};
});
const transformEndTime = performance.now();
// Create groups based on dynamic data from database
const groupingStartTime = performance.now();
const groupedResponse: Record<string, any> = {};
// Initialize groups from database data
@@ -1148,6 +1135,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
tasks: [],
taskIds: [],
color: group.color_code || this.getDefaultGroupColor(groupBy, groupKey),
color_code_dark: group.color_code_dark || this.getDefaultGroupColor(groupBy, groupKey),
// Include additional metadata from database
category_id: group.category_id,
start_date: group.start_date,
@@ -1294,8 +1282,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
responseGroups.push(groupedResponse[UNMAPPED.toLowerCase()]);
}
const groupingEndTime = performance.now();
const endTime = performance.now();
const totalTime = endTime - startTime;
@@ -1333,16 +1319,11 @@ export default class TasksControllerV2 extends TasksControllerBase {
done: "#52c41a",
},
[GroupBy.PRIORITY]: {
critical: "#ff4d4f",
high: "#ff7a45",
medium: "#faad14",
low: "#52c41a",
},
[GroupBy.PHASE]: {
planning: "#722ed1",
development: "#1890ff",
testing: "#faad14",
deployment: "#52c41a",
unmapped: "#fbc84c69",
},
};

View File

@@ -89,24 +89,24 @@ export const NumbersColorMap: { [x: string]: string } = {
};
export const PriorityColorCodes: { [x: number]: string; } = {
0: "#75c997",
1: "#fbc84c",
2: "#f37070"
0: "#2E8B57",
1: "#DAA520",
2: "#CD5C5C"
};
export const PriorityColorCodesDark: { [x: number]: string; } = {
0: "#46D980",
1: "#FFC227",
2: "#FF4141"
0: "#3CB371",
1: "#B8860B",
2: "#F08080"
};
export const TASK_STATUS_TODO_COLOR = "#a9a9a9";
export const TASK_STATUS_DOING_COLOR = "#70a6f3";
export const TASK_STATUS_DONE_COLOR = "#75c997";
export const TASK_PRIORITY_LOW_COLOR = "#75c997";
export const TASK_PRIORITY_MEDIUM_COLOR = "#fbc84c";
export const TASK_PRIORITY_HIGH_COLOR = "#f37070";
export const TASK_PRIORITY_LOW_COLOR = "#2E8B57";
export const TASK_PRIORITY_MEDIUM_COLOR = "#DAA520";
export const TASK_PRIORITY_HIGH_COLOR = "#CD5C5C";
export const TASK_DUE_COMPLETED_COLOR = "#75c997";
export const TASK_DUE_UPCOMING_COLOR = "#70a6f3";

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Tooltip } from 'antd';
interface GroupProgressBarProps {
todoProgress: number;
@@ -21,18 +22,27 @@ const GroupProgressBar: React.FC<GroupProgressBarProps> = ({
return null;
}
const total = todoProgress + doingProgress + doneProgress;
const total = (todoProgress || 0) + (doingProgress || 0) + (doneProgress || 0);
// Don't show if no progress values exist
if (total === 0) {
return null;
}
// Tooltip content with all values in rows
const tooltipContent = (
<div>
<div>{t('todo')}: {todoProgress || 0}%</div>
<div>{t('inProgress')}: {doingProgress || 0}%</div>
<div>{t('done')}: {doneProgress || 0}%</div>
</div>
);
return (
<div className="flex items-center gap-2">
{/* Compact progress text */}
<span className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap font-medium">
{doneProgress}% {t('done')}
{doneProgress || 0}% {t('done')}
</span>
{/* Compact progress bar */}
@@ -40,27 +50,30 @@ const GroupProgressBar: React.FC<GroupProgressBarProps> = ({
<div className="h-full flex">
{/* Todo section - light green */}
{todoProgress > 0 && (
<div
className="bg-green-200 dark:bg-green-800 transition-all duration-300"
style={{ width: `${(todoProgress / total) * 100}%` }}
title={`${t('todo')}: ${todoProgress}%`}
/>
<Tooltip title={tooltipContent} placement="top">
<div
className="bg-green-200 dark:bg-green-800 transition-all duration-300"
style={{ width: `${(todoProgress / total) * 100}%` }}
/>
</Tooltip>
)}
{/* Doing section - medium green */}
{doingProgress > 0 && (
<div
className="bg-green-400 dark:bg-green-600 transition-all duration-300"
style={{ width: `${(doingProgress / total) * 100}%` }}
title={`${t('inProgress')}: ${doingProgress}%`}
/>
<Tooltip title={tooltipContent} placement="top">
<div
className="bg-green-400 dark:bg-green-600 transition-all duration-300"
style={{ width: `${(doingProgress / total) * 100}%` }}
/>
</Tooltip>
)}
{/* Done section - dark green */}
{doneProgress > 0 && (
<div
className="bg-green-600 dark:bg-green-400 transition-all duration-300"
style={{ width: `${(doneProgress / total) * 100}%` }}
title={`${t('done')}: ${doneProgress}%`}
/>
<Tooltip title={tooltipContent} placement="top">
<div
className="bg-green-600 dark:bg-green-400 transition-all duration-300"
style={{ width: `${(doneProgress / total) * 100}%` }}
/>
</Tooltip>
)}
</div>
</div>
@@ -68,22 +81,25 @@ const GroupProgressBar: React.FC<GroupProgressBarProps> = ({
{/* Small legend dots with better spacing */}
<div className="flex items-center gap-1">
{todoProgress > 0 && (
<div
className="w-1.5 h-1.5 bg-green-200 dark:bg-green-800 rounded-full"
title={`${t('todo')}: ${todoProgress}%`}
/>
<Tooltip title={tooltipContent} placement="top">
<div
className="w-1.5 h-1.5 bg-green-200 dark:bg-green-800 rounded-full"
/>
</Tooltip>
)}
{doingProgress > 0 && (
<div
className="w-1.5 h-1.5 bg-green-400 dark:bg-green-600 rounded-full"
title={`${t('inProgress')}: ${doingProgress}%`}
/>
<Tooltip title={tooltipContent} placement="top">
<div
className="w-1.5 h-1.5 bg-green-400 dark:bg-green-600 rounded-full"
/>
</Tooltip>
)}
{doneProgress > 0 && (
<div
className="w-1.5 h-1.5 bg-green-600 dark:bg-green-400 rounded-full"
title={`${t('done')}: ${doneProgress}%`}
/>
<Tooltip title={tooltipContent} placement="top">
<div
className="w-1.5 h-1.5 bg-green-600 dark:bg-green-400 rounded-full"
/>
</Tooltip>
)}
</div>
</div>

View File

@@ -60,7 +60,6 @@ import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/
// Components
import TaskRowWithSubtasks from './TaskRowWithSubtasks';
import TaskGroupHeader from './TaskGroupHeader';
import ImprovedTaskFilters from '@/components/task-management/improved-task-filters';
import OptimizedBulkActionBar from '@/components/task-management/optimized-bulk-action-bar';
import CustomColumnModal from '@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal';
import AddTaskRow from './components/AddTaskRow';
@@ -177,7 +176,9 @@ const TaskListV2Section: React.FC = () => {
const { projectId: urlProjectId } = useParams();
const { t } = useTranslation('task-list-table');
const { socket, connected } = useSocket();
const themeMode = useAppSelector(state => state.themeReducer.mode);
const isDarkMode = themeMode === 'dark';
// Redux state selectors
const allTasks = useAppSelector(selectAllTasksArray);
const groups = useAppSelector(selectGroups);
@@ -490,7 +491,7 @@ const TaskListV2Section: React.FC = () => {
isAddTaskRow: true,
groupId: group.id,
groupType: currentGrouping || 'status',
groupValue: group.id, // Use the actual database ID from backend
groupValue: group.id, // Send the UUID that backend expects
projectId: urlProjectId,
rowId: `add-task-${group.id}-0`,
autoFocus: false,
@@ -501,7 +502,7 @@ const TaskListV2Section: React.FC = () => {
isAddTaskRow: true,
groupId: group.id,
groupType: currentGrouping || 'status',
groupValue: group.id,
groupValue: group.id, // Send the UUID that backend expects
projectId: urlProjectId,
rowId: rowId,
autoFocus: index === groupAddRows.length - 1, // Auto-focus the latest row
@@ -534,6 +535,7 @@ const TaskListV2Section: React.FC = () => {
return virtuosoGroups.flatMap(group => group.tasks);
}, [virtuosoGroups]);
// Render functions
const renderGroup = useCallback(
(groupIndex: number) => {
@@ -548,7 +550,7 @@ const TaskListV2Section: React.FC = () => {
id: group.id,
name: group.title,
count: group.actualCount,
color: group.color,
color: isDarkMode ? group.color_code_dark : group.color,
}}
isCollapsed={isGroupCollapsed}
onToggle={() => handleGroupCollapse(group.id)}
@@ -679,13 +681,97 @@ const TaskListV2Section: React.FC = () => {
</div>
);
// Show message when no data
// Show message when no data - but for phase grouping, create an unmapped group
if (groups.length === 0 && !loading) {
// If grouped by phase, show an unmapped group to allow task creation
if (currentGrouping === 'phase') {
const unmappedGroup = {
id: 'Unmapped',
title: 'Unmapped',
groupType: 'phase',
groupValue: 'Unmapped', // Use same ID as groupValue for consistency
collapsed: false,
tasks: [],
taskIds: [],
color: '#fbc84c69',
actualCount: 0,
count: 1, // For the add task row
startIndex: 0
};
const unmappedVirtuosoGroups = [unmappedGroup];
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex flex-col bg-white dark:bg-gray-900 h-full overflow-hidden">
<div
className="border border-gray-200 dark:border-gray-700 rounded-lg"
style={{
height: 'calc(100vh - 240px)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
<div
ref={contentScrollRef}
className="flex-1 bg-white dark:bg-gray-900 relative"
style={{
overflowX: 'auto',
overflowY: 'auto',
minHeight: 0,
}}
>
{/* Sticky Column Headers */}
<div
className="sticky top-0 z-30 bg-gray-50 dark:bg-gray-800"
style={{ width: '100%', minWidth: 'max-content' }}
>
{renderColumnHeaders()}
</div>
<div style={{ minWidth: 'max-content' }}>
<div className="mt-2">
<TaskGroupHeader
group={{
id: 'Unmapped',
name: 'Unmapped',
count: 0,
color: '#fbc84c69',
}}
isCollapsed={false}
onToggle={() => {}}
projectId={urlProjectId || ''}
/>
<AddTaskRow
groupId="Unmapped"
groupType="phase"
groupValue="Unmapped"
projectId={urlProjectId || ''}
visibleColumns={visibleColumns}
onTaskAdded={handleTaskAdded}
rowId="add-task-Unmapped-0"
autoFocus={false}
/>
</div>
</div>
</div>
</div>
</div>
</DndContext>
);
}
// For other groupings, show the empty state message
return (
<div className="flex flex-col bg-white dark:bg-gray-900 h-full">
<div className="flex-none" style={{ height: '74px', flexShrink: 0 }}>
<ImprovedTaskFilters position="list" />
</div>
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-medium text-gray-900 dark:text-white mb-2">
@@ -806,19 +892,17 @@ const TaskListV2Section: React.FC = () => {
{/* Drag Overlay */}
<DragOverlay dropAnimation={{ duration: 200, easing: 'ease-in-out' }}>
{activeId ? (
<div className="bg-white dark:bg-gray-800 shadow-2xl rounded-lg border-2 border-blue-500 dark:border-blue-400 scale-105">
<div className="px-4 py-3">
<div className="flex items-center gap-3">
<HolderOutlined className="text-blue-500 dark:text-blue-400" />
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{allTasks.find(task => task.id === activeId)?.name ||
allTasks.find(task => task.id === activeId)?.title ||
t('emptyStates.dragTaskFallback')}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{allTasks.find(task => task.id === activeId)?.task_key}
</div>
<div
className="bg-white dark:bg-gray-800 shadow-lg rounded-md border border-blue-400 dark:border-blue-500 opacity-90"
style={{ width: visibleColumns.find(col => col.id === 'title')?.width || '300px' }}
>
<div className="px-3 py-2">
<div className="flex items-center gap-2">
<HolderOutlined className="text-gray-400 dark:text-gray-500 text-xs" />
<div className="text-sm font-medium text-gray-900 dark:text-white truncate flex-1">
{allTasks.find(task => task.id === activeId)?.name ||
allTasks.find(task => task.id === activeId)?.title ||
t('emptyStates.dragTaskFallback')}
</div>
</div>
</div>

View File

@@ -526,9 +526,25 @@ const taskManagementSlice = createSlice({
},
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId: string }>) => {
const { task, groupId } = action.payload;
state.ids.push(task.id);
state.entities[task.id] = task;
const group = state.groups.find(g => g.id === groupId);
let group = state.groups.find(g => g.id === groupId);
// If group doesn't exist and it's "Unmapped", create it dynamically
if (!group && groupId === 'Unmapped') {
const unmappedGroup = {
id: 'Unmapped',
title: 'Unmapped',
taskIds: [],
type: 'phase' as const,
color: '#fbc84c69',
groupValue: 'Unmapped'
};
state.groups.push(unmappedGroup);
group = unmappedGroup;
}
if (group) {
group.taskIds.push(task.id);
}
@@ -1170,7 +1186,7 @@ export default taskManagementSlice.reducer;
// V3 API selectors - no processing needed, data is pre-processed by backend
export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups;
export const selectCurrentGroupingV3 = (state: RootState) => state.taskManagement.grouping;
export const selectCurrentGroupingV3 = (state: RootState) => state.grouping.currentGrouping;
// Column-related selectors
export const selectColumns = (state: RootState) => state.taskManagement.columns;

View File

@@ -855,10 +855,11 @@ export const useTaskSocketHandlers = () => {
// For priority grouping, use priority field (which contains the priority UUID)
groupId = data.priority;
} else if (grouping === 'phase') {
// For phase grouping, use phase_id
groupId = data.phase_id;
// For phase grouping, use phase_id, or 'Unmapped' if no phase_id
groupId = data.phase_id || 'Unmapped';
}
// Use addTaskToGroup with the actual group UUID
dispatch(addTaskToGroup({ task, groupId: groupId || '' }));

View File

@@ -62,6 +62,7 @@ export interface TaskGroup {
taskIds: string[];
type?: 'status' | 'priority' | 'phase' | 'members';
color?: string;
color_code_dark?: string;
collapsed?: boolean;
groupValue?: string;
// Add any other group properties as needed