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:
@@ -16,6 +16,7 @@ export interface ITaskGroup {
|
|||||||
start_date?: string;
|
start_date?: string;
|
||||||
end_date?: string;
|
end_date?: string;
|
||||||
color_code: string;
|
color_code: string;
|
||||||
|
color_code_dark: string;
|
||||||
category_id: string | null;
|
category_id: string | null;
|
||||||
old_category_id?: string;
|
old_category_id?: string;
|
||||||
todo_progress?: number;
|
todo_progress?: number;
|
||||||
|
|||||||
@@ -110,20 +110,20 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
|
|
||||||
private static getQuery(userId: string, options: ParsedQs) {
|
private static getQuery(userId: string, options: ParsedQs) {
|
||||||
// Determine which sort column to use based on grouping
|
// Determine which sort column to use based on grouping
|
||||||
const groupBy = options.group || 'status';
|
const groupBy = options.group || "status";
|
||||||
let defaultSortColumn = 'sort_order';
|
let defaultSortColumn = "sort_order";
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case 'status':
|
case "status":
|
||||||
defaultSortColumn = 'status_sort_order';
|
defaultSortColumn = "status_sort_order";
|
||||||
break;
|
break;
|
||||||
case 'priority':
|
case "priority":
|
||||||
defaultSortColumn = 'priority_sort_order';
|
defaultSortColumn = "priority_sort_order";
|
||||||
break;
|
break;
|
||||||
case 'phase':
|
case "phase":
|
||||||
defaultSortColumn = 'phase_sort_order';
|
defaultSortColumn = "phase_sort_order";
|
||||||
break;
|
break;
|
||||||
default:
|
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;
|
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,
|
name: UNMAPPED,
|
||||||
category_id: null,
|
category_id: null,
|
||||||
color_code: "#fbc84c69",
|
color_code: "#fbc84c69",
|
||||||
|
color_code_dark: "#fbc84c69",
|
||||||
tasks: unmapped
|
tasks: unmapped
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1008,7 +1009,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const isSubTasks = !!req.query.parent_task;
|
const isSubTasks = !!req.query.parent_task;
|
||||||
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
||||||
const archived = req.query.archived === "true";
|
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Skip expensive progress calculation by default
|
// PERFORMANCE OPTIMIZATION: Skip expensive progress calculation by default
|
||||||
// Progress values are already calculated and stored in the database
|
// 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";
|
const shouldRefreshProgress = req.query.refresh_progress === "true";
|
||||||
|
|
||||||
if (shouldRefreshProgress && req.params.id) {
|
if (shouldRefreshProgress && req.params.id) {
|
||||||
const progressStartTime = performance.now();
|
|
||||||
await this.refreshProjectTaskProgressValues(req.params.id);
|
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 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 params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||||
|
|
||||||
const result = await db.query(q, params);
|
const result = await db.query(q, params);
|
||||||
const tasks = [...result.rows];
|
const tasks = [...result.rows];
|
||||||
const queryEndTime = performance.now();
|
|
||||||
|
|
||||||
// Get groups metadata dynamically from database
|
// Get groups metadata dynamically from database
|
||||||
const groupsStartTime = performance.now();
|
|
||||||
const groups = await this.getGroups(groupBy, req.params.id);
|
const groups = await this.getGroups(groupBy, req.params.id);
|
||||||
const groupsEndTime = performance.now();
|
|
||||||
|
|
||||||
// Create priority value to name mapping
|
// Create priority value to name mapping
|
||||||
const priorityMap: Record<string, string> = {
|
const priorityMap: Record<string, string> = {
|
||||||
@@ -1051,10 +1045,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Transform tasks with all necessary data preprocessing
|
// Transform tasks with all necessary data preprocessing
|
||||||
const transformStartTime = performance.now();
|
|
||||||
const transformedTasks = tasks.map((task, index) => {
|
const transformedTasks = tasks.map((task, index) => {
|
||||||
// Update task with calculated values (lightweight version)
|
// Update task with calculated values (lightweight version)
|
||||||
TasksControllerV2.updateTaskViewModel(task);
|
TasksControllerV2.updateTaskViewModel(task);
|
||||||
@@ -1125,10 +1116,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
reporter: task.reporter || null,
|
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> = {};
|
const groupedResponse: Record<string, any> = {};
|
||||||
|
|
||||||
// Initialize groups from database data
|
// Initialize groups from database data
|
||||||
@@ -1148,6 +1135,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
tasks: [],
|
tasks: [],
|
||||||
taskIds: [],
|
taskIds: [],
|
||||||
color: group.color_code || this.getDefaultGroupColor(groupBy, groupKey),
|
color: group.color_code || this.getDefaultGroupColor(groupBy, groupKey),
|
||||||
|
color_code_dark: group.color_code_dark || this.getDefaultGroupColor(groupBy, groupKey),
|
||||||
// Include additional metadata from database
|
// Include additional metadata from database
|
||||||
category_id: group.category_id,
|
category_id: group.category_id,
|
||||||
start_date: group.start_date,
|
start_date: group.start_date,
|
||||||
@@ -1294,8 +1282,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
responseGroups.push(groupedResponse[UNMAPPED.toLowerCase()]);
|
responseGroups.push(groupedResponse[UNMAPPED.toLowerCase()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupingEndTime = performance.now();
|
|
||||||
|
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
const totalTime = endTime - startTime;
|
const totalTime = endTime - startTime;
|
||||||
|
|
||||||
@@ -1333,16 +1319,11 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
done: "#52c41a",
|
done: "#52c41a",
|
||||||
},
|
},
|
||||||
[GroupBy.PRIORITY]: {
|
[GroupBy.PRIORITY]: {
|
||||||
critical: "#ff4d4f",
|
|
||||||
high: "#ff7a45",
|
high: "#ff7a45",
|
||||||
medium: "#faad14",
|
medium: "#faad14",
|
||||||
low: "#52c41a",
|
low: "#52c41a",
|
||||||
},
|
},
|
||||||
[GroupBy.PHASE]: {
|
[GroupBy.PHASE]: {
|
||||||
planning: "#722ed1",
|
|
||||||
development: "#1890ff",
|
|
||||||
testing: "#faad14",
|
|
||||||
deployment: "#52c41a",
|
|
||||||
unmapped: "#fbc84c69",
|
unmapped: "#fbc84c69",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,24 +89,24 @@ export const NumbersColorMap: { [x: string]: string } = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PriorityColorCodes: { [x: number]: string; } = {
|
export const PriorityColorCodes: { [x: number]: string; } = {
|
||||||
0: "#75c997",
|
0: "#2E8B57",
|
||||||
1: "#fbc84c",
|
1: "#DAA520",
|
||||||
2: "#f37070"
|
2: "#CD5C5C"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PriorityColorCodesDark: { [x: number]: string; } = {
|
export const PriorityColorCodesDark: { [x: number]: string; } = {
|
||||||
0: "#46D980",
|
0: "#3CB371",
|
||||||
1: "#FFC227",
|
1: "#B8860B",
|
||||||
2: "#FF4141"
|
2: "#F08080"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TASK_STATUS_TODO_COLOR = "#a9a9a9";
|
export const TASK_STATUS_TODO_COLOR = "#a9a9a9";
|
||||||
export const TASK_STATUS_DOING_COLOR = "#70a6f3";
|
export const TASK_STATUS_DOING_COLOR = "#70a6f3";
|
||||||
export const TASK_STATUS_DONE_COLOR = "#75c997";
|
export const TASK_STATUS_DONE_COLOR = "#75c997";
|
||||||
|
|
||||||
export const TASK_PRIORITY_LOW_COLOR = "#75c997";
|
export const TASK_PRIORITY_LOW_COLOR = "#2E8B57";
|
||||||
export const TASK_PRIORITY_MEDIUM_COLOR = "#fbc84c";
|
export const TASK_PRIORITY_MEDIUM_COLOR = "#DAA520";
|
||||||
export const TASK_PRIORITY_HIGH_COLOR = "#f37070";
|
export const TASK_PRIORITY_HIGH_COLOR = "#CD5C5C";
|
||||||
|
|
||||||
export const TASK_DUE_COMPLETED_COLOR = "#75c997";
|
export const TASK_DUE_COMPLETED_COLOR = "#75c997";
|
||||||
export const TASK_DUE_UPCOMING_COLOR = "#70a6f3";
|
export const TASK_DUE_UPCOMING_COLOR = "#70a6f3";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
|
||||||
interface GroupProgressBarProps {
|
interface GroupProgressBarProps {
|
||||||
todoProgress: number;
|
todoProgress: number;
|
||||||
@@ -21,18 +22,27 @@ const GroupProgressBar: React.FC<GroupProgressBarProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = todoProgress + doingProgress + doneProgress;
|
const total = (todoProgress || 0) + (doingProgress || 0) + (doneProgress || 0);
|
||||||
|
|
||||||
// Don't show if no progress values exist
|
// Don't show if no progress values exist
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Compact progress text */}
|
{/* Compact progress text */}
|
||||||
<span className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap font-medium">
|
<span className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap font-medium">
|
||||||
{doneProgress}% {t('done')}
|
{doneProgress || 0}% {t('done')}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Compact progress bar */}
|
{/* Compact progress bar */}
|
||||||
@@ -40,27 +50,30 @@ const GroupProgressBar: React.FC<GroupProgressBarProps> = ({
|
|||||||
<div className="h-full flex">
|
<div className="h-full flex">
|
||||||
{/* Todo section - light green */}
|
{/* Todo section - light green */}
|
||||||
{todoProgress > 0 && (
|
{todoProgress > 0 && (
|
||||||
<div
|
<Tooltip title={tooltipContent} placement="top">
|
||||||
className="bg-green-200 dark:bg-green-800 transition-all duration-300"
|
<div
|
||||||
style={{ width: `${(todoProgress / total) * 100}%` }}
|
className="bg-green-200 dark:bg-green-800 transition-all duration-300"
|
||||||
title={`${t('todo')}: ${todoProgress}%`}
|
style={{ width: `${(todoProgress / total) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{/* Doing section - medium green */}
|
{/* Doing section - medium green */}
|
||||||
{doingProgress > 0 && (
|
{doingProgress > 0 && (
|
||||||
<div
|
<Tooltip title={tooltipContent} placement="top">
|
||||||
className="bg-green-400 dark:bg-green-600 transition-all duration-300"
|
<div
|
||||||
style={{ width: `${(doingProgress / total) * 100}%` }}
|
className="bg-green-400 dark:bg-green-600 transition-all duration-300"
|
||||||
title={`${t('inProgress')}: ${doingProgress}%`}
|
style={{ width: `${(doingProgress / total) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{/* Done section - dark green */}
|
{/* Done section - dark green */}
|
||||||
{doneProgress > 0 && (
|
{doneProgress > 0 && (
|
||||||
<div
|
<Tooltip title={tooltipContent} placement="top">
|
||||||
className="bg-green-600 dark:bg-green-400 transition-all duration-300"
|
<div
|
||||||
style={{ width: `${(doneProgress / total) * 100}%` }}
|
className="bg-green-600 dark:bg-green-400 transition-all duration-300"
|
||||||
title={`${t('done')}: ${doneProgress}%`}
|
style={{ width: `${(doneProgress / total) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,22 +81,25 @@ const GroupProgressBar: React.FC<GroupProgressBarProps> = ({
|
|||||||
{/* Small legend dots with better spacing */}
|
{/* Small legend dots with better spacing */}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{todoProgress > 0 && (
|
{todoProgress > 0 && (
|
||||||
<div
|
<Tooltip title={tooltipContent} placement="top">
|
||||||
className="w-1.5 h-1.5 bg-green-200 dark:bg-green-800 rounded-full"
|
<div
|
||||||
title={`${t('todo')}: ${todoProgress}%`}
|
className="w-1.5 h-1.5 bg-green-200 dark:bg-green-800 rounded-full"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{doingProgress > 0 && (
|
{doingProgress > 0 && (
|
||||||
<div
|
<Tooltip title={tooltipContent} placement="top">
|
||||||
className="w-1.5 h-1.5 bg-green-400 dark:bg-green-600 rounded-full"
|
<div
|
||||||
title={`${t('inProgress')}: ${doingProgress}%`}
|
className="w-1.5 h-1.5 bg-green-400 dark:bg-green-600 rounded-full"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{doneProgress > 0 && (
|
{doneProgress > 0 && (
|
||||||
<div
|
<Tooltip title={tooltipContent} placement="top">
|
||||||
className="w-1.5 h-1.5 bg-green-600 dark:bg-green-400 rounded-full"
|
<div
|
||||||
title={`${t('done')}: ${doneProgress}%`}
|
className="w-1.5 h-1.5 bg-green-600 dark:bg-green-400 rounded-full"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/
|
|||||||
// Components
|
// Components
|
||||||
import TaskRowWithSubtasks from './TaskRowWithSubtasks';
|
import TaskRowWithSubtasks from './TaskRowWithSubtasks';
|
||||||
import TaskGroupHeader from './TaskGroupHeader';
|
import TaskGroupHeader from './TaskGroupHeader';
|
||||||
import ImprovedTaskFilters from '@/components/task-management/improved-task-filters';
|
|
||||||
import OptimizedBulkActionBar from '@/components/task-management/optimized-bulk-action-bar';
|
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 CustomColumnModal from '@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal';
|
||||||
import AddTaskRow from './components/AddTaskRow';
|
import AddTaskRow from './components/AddTaskRow';
|
||||||
@@ -177,7 +176,9 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
const { projectId: urlProjectId } = useParams();
|
const { projectId: urlProjectId } = useParams();
|
||||||
const { t } = useTranslation('task-list-table');
|
const { t } = useTranslation('task-list-table');
|
||||||
const { socket, connected } = useSocket();
|
const { socket, connected } = useSocket();
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
const isDarkMode = themeMode === 'dark';
|
||||||
|
|
||||||
// Redux state selectors
|
// Redux state selectors
|
||||||
const allTasks = useAppSelector(selectAllTasksArray);
|
const allTasks = useAppSelector(selectAllTasksArray);
|
||||||
const groups = useAppSelector(selectGroups);
|
const groups = useAppSelector(selectGroups);
|
||||||
@@ -490,7 +491,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
isAddTaskRow: true,
|
isAddTaskRow: true,
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
groupType: currentGrouping || 'status',
|
groupType: currentGrouping || 'status',
|
||||||
groupValue: group.id, // Use the actual database ID from backend
|
groupValue: group.id, // Send the UUID that backend expects
|
||||||
projectId: urlProjectId,
|
projectId: urlProjectId,
|
||||||
rowId: `add-task-${group.id}-0`,
|
rowId: `add-task-${group.id}-0`,
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
@@ -501,7 +502,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
isAddTaskRow: true,
|
isAddTaskRow: true,
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
groupType: currentGrouping || 'status',
|
groupType: currentGrouping || 'status',
|
||||||
groupValue: group.id,
|
groupValue: group.id, // Send the UUID that backend expects
|
||||||
projectId: urlProjectId,
|
projectId: urlProjectId,
|
||||||
rowId: rowId,
|
rowId: rowId,
|
||||||
autoFocus: index === groupAddRows.length - 1, // Auto-focus the latest row
|
autoFocus: index === groupAddRows.length - 1, // Auto-focus the latest row
|
||||||
@@ -534,6 +535,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
return virtuosoGroups.flatMap(group => group.tasks);
|
return virtuosoGroups.flatMap(group => group.tasks);
|
||||||
}, [virtuosoGroups]);
|
}, [virtuosoGroups]);
|
||||||
|
|
||||||
|
|
||||||
// Render functions
|
// Render functions
|
||||||
const renderGroup = useCallback(
|
const renderGroup = useCallback(
|
||||||
(groupIndex: number) => {
|
(groupIndex: number) => {
|
||||||
@@ -548,7 +550,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
id: group.id,
|
id: group.id,
|
||||||
name: group.title,
|
name: group.title,
|
||||||
count: group.actualCount,
|
count: group.actualCount,
|
||||||
color: group.color,
|
color: isDarkMode ? group.color_code_dark : group.color,
|
||||||
}}
|
}}
|
||||||
isCollapsed={isGroupCollapsed}
|
isCollapsed={isGroupCollapsed}
|
||||||
onToggle={() => handleGroupCollapse(group.id)}
|
onToggle={() => handleGroupCollapse(group.id)}
|
||||||
@@ -679,13 +681,97 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
</div>
|
</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 (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 (
|
return (
|
||||||
<div className="flex flex-col bg-white dark:bg-gray-900 h-full">
|
<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="flex-1 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
<div className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||||
@@ -806,19 +892,17 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
{/* Drag Overlay */}
|
{/* Drag Overlay */}
|
||||||
<DragOverlay dropAnimation={{ duration: 200, easing: 'ease-in-out' }}>
|
<DragOverlay dropAnimation={{ duration: 200, easing: 'ease-in-out' }}>
|
||||||
{activeId ? (
|
{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
|
||||||
<div className="px-4 py-3">
|
className="bg-white dark:bg-gray-800 shadow-lg rounded-md border border-blue-400 dark:border-blue-500 opacity-90"
|
||||||
<div className="flex items-center gap-3">
|
style={{ width: visibleColumns.find(col => col.id === 'title')?.width || '300px' }}
|
||||||
<HolderOutlined className="text-blue-500 dark:text-blue-400" />
|
>
|
||||||
<div>
|
<div className="px-3 py-2">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
<div className="flex items-center gap-2">
|
||||||
{allTasks.find(task => task.id === activeId)?.name ||
|
<HolderOutlined className="text-gray-400 dark:text-gray-500 text-xs" />
|
||||||
allTasks.find(task => task.id === activeId)?.title ||
|
<div className="text-sm font-medium text-gray-900 dark:text-white truncate flex-1">
|
||||||
t('emptyStates.dragTaskFallback')}
|
{allTasks.find(task => task.id === activeId)?.name ||
|
||||||
</div>
|
allTasks.find(task => task.id === activeId)?.title ||
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
t('emptyStates.dragTaskFallback')}
|
||||||
{allTasks.find(task => task.id === activeId)?.task_key}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -526,9 +526,25 @@ const taskManagementSlice = createSlice({
|
|||||||
},
|
},
|
||||||
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId: string }>) => {
|
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId: string }>) => {
|
||||||
const { task, groupId } = action.payload;
|
const { task, groupId } = action.payload;
|
||||||
|
|
||||||
state.ids.push(task.id);
|
state.ids.push(task.id);
|
||||||
state.entities[task.id] = task;
|
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) {
|
if (group) {
|
||||||
group.taskIds.push(task.id);
|
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
|
// V3 API selectors - no processing needed, data is pre-processed by backend
|
||||||
export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups;
|
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
|
// Column-related selectors
|
||||||
export const selectColumns = (state: RootState) => state.taskManagement.columns;
|
export const selectColumns = (state: RootState) => state.taskManagement.columns;
|
||||||
|
|||||||
@@ -855,10 +855,11 @@ export const useTaskSocketHandlers = () => {
|
|||||||
// For priority grouping, use priority field (which contains the priority UUID)
|
// For priority grouping, use priority field (which contains the priority UUID)
|
||||||
groupId = data.priority;
|
groupId = data.priority;
|
||||||
} else if (grouping === 'phase') {
|
} else if (grouping === 'phase') {
|
||||||
// For phase grouping, use phase_id
|
// For phase grouping, use phase_id, or 'Unmapped' if no phase_id
|
||||||
groupId = data.phase_id;
|
groupId = data.phase_id || 'Unmapped';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Use addTaskToGroup with the actual group UUID
|
// Use addTaskToGroup with the actual group UUID
|
||||||
dispatch(addTaskToGroup({ task, groupId: groupId || '' }));
|
dispatch(addTaskToGroup({ task, groupId: groupId || '' }));
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export interface TaskGroup {
|
|||||||
taskIds: string[];
|
taskIds: string[];
|
||||||
type?: 'status' | 'priority' | 'phase' | 'members';
|
type?: 'status' | 'priority' | 'phase' | 'members';
|
||||||
color?: string;
|
color?: string;
|
||||||
|
color_code_dark?: string;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
groupValue?: string;
|
groupValue?: string;
|
||||||
// Add any other group properties as needed
|
// Add any other group properties as needed
|
||||||
|
|||||||
Reference in New Issue
Block a user