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

@@ -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