- Added functionality to handle task priority changes, emitting updates via socket for real-time synchronization. - Updated the EnhancedKanbanBoardNativeDnD component to include new logic for managing task priorities within the drag-and-drop interface. - Cleaned up console log statements in the useTaskSocketHandlers hook for improved performance monitoring.
1108 lines
35 KiB
TypeScript
1108 lines
35 KiB
TypeScript
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
|
|
import {
|
|
IGroupByOption,
|
|
ITaskListConfigV2,
|
|
ITaskListGroup,
|
|
ITaskListSortableColumn,
|
|
} from '@/types/tasks/taskList.types';
|
|
import { tasksApiService } from '@/api/tasks/tasks.api.service';
|
|
import { subTasksApiService } from '@/api/tasks/subtasks.api.service';
|
|
import logger from '@/utils/errorLogger';
|
|
import { ITaskListMemberFilter } from '@/types/tasks/taskListFilters.types';
|
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
|
import { ITaskStatusViewModel } from '@/types/tasks/taskStatusGetResponse.types';
|
|
import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.types';
|
|
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
|
import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
|
|
import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service';
|
|
import { ITaskAssigneesUpdateResponse } from '@/types/tasks/task-assignee-update-response';
|
|
import { ITaskAssignee } from '@/types/project/projectTasksViewModel.types';
|
|
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
|
import { ILabelsChangeResponse } from '@/types/tasks/taskList.types';
|
|
|
|
export enum IGroupBy {
|
|
STATUS = 'status',
|
|
PRIORITY = 'priority',
|
|
PHASE = 'phase',
|
|
MEMBERS = 'members',
|
|
}
|
|
|
|
export const GROUP_BY_OPTIONS: IGroupByOption[] = [
|
|
{ label: 'Status', value: IGroupBy.STATUS },
|
|
{ label: 'Priority', value: IGroupBy.PRIORITY },
|
|
{ label: 'Phase', value: IGroupBy.PHASE },
|
|
];
|
|
|
|
const LOCALSTORAGE_GROUP_KEY = 'worklenz.kanban.group_by';
|
|
|
|
export const getCurrentGroup = (): IGroupBy => {
|
|
const key = localStorage.getItem(LOCALSTORAGE_GROUP_KEY);
|
|
if (key && Object.values(IGroupBy).includes(key as IGroupBy)) {
|
|
return key as IGroupBy;
|
|
}
|
|
setCurrentGroup(IGroupBy.STATUS);
|
|
return IGroupBy.STATUS;
|
|
};
|
|
|
|
export const setCurrentGroup = (groupBy: IGroupBy): void => {
|
|
localStorage.setItem(LOCALSTORAGE_GROUP_KEY, groupBy);
|
|
};
|
|
|
|
interface EnhancedKanbanState {
|
|
// Core state
|
|
search: string | null;
|
|
archived: boolean;
|
|
groupBy: IGroupBy;
|
|
isSubtasksInclude: boolean;
|
|
fields: ITaskListSortableColumn[];
|
|
|
|
// Task data
|
|
taskGroups: ITaskListGroup[];
|
|
loadingGroups: boolean;
|
|
error: string | null;
|
|
|
|
// Filters - Original data (should not be filtered)
|
|
originalTaskAssignees: ITaskListMemberFilter[];
|
|
originalLabels: ITaskLabelFilter[];
|
|
|
|
// Filters - Current filtered data
|
|
taskAssignees: ITaskListMemberFilter[];
|
|
loadingAssignees: boolean;
|
|
statuses: ITaskStatusViewModel[];
|
|
labels: ITaskLabelFilter[];
|
|
loadingLabels: boolean;
|
|
priorities: string[];
|
|
members: string[];
|
|
|
|
// Performance optimizations
|
|
virtualizedRendering: boolean;
|
|
taskCache: Record<string, IProjectTask>;
|
|
groupCache: Record<string, ITaskListGroup>;
|
|
|
|
// Performance monitoring
|
|
performanceMetrics: {
|
|
totalTasks: number;
|
|
largestGroupSize: number;
|
|
averageGroupSize: number;
|
|
renderTime: number;
|
|
lastUpdateTime: number;
|
|
virtualizationEnabled: boolean;
|
|
};
|
|
|
|
// Drag and drop state
|
|
dragState: {
|
|
activeTaskId: string | null;
|
|
activeGroupId: string | null;
|
|
overId: string | null;
|
|
isDragging: boolean;
|
|
};
|
|
|
|
// UI state
|
|
selectedTaskIds: string[];
|
|
expandedSubtasks: Record<string, boolean>;
|
|
columnOrder: string[];
|
|
editableSectionId: string | null;
|
|
}
|
|
|
|
const initialState: EnhancedKanbanState = {
|
|
search: null,
|
|
archived: false,
|
|
groupBy: getCurrentGroup(),
|
|
isSubtasksInclude: false,
|
|
fields: [],
|
|
taskGroups: [],
|
|
loadingGroups: false,
|
|
error: null,
|
|
originalTaskAssignees: [],
|
|
originalLabels: [],
|
|
taskAssignees: [],
|
|
loadingAssignees: false,
|
|
statuses: [],
|
|
labels: [],
|
|
loadingLabels: false,
|
|
priorities: [],
|
|
members: [],
|
|
virtualizedRendering: true,
|
|
taskCache: {},
|
|
groupCache: {},
|
|
performanceMetrics: {
|
|
totalTasks: 0,
|
|
largestGroupSize: 0,
|
|
averageGroupSize: 0,
|
|
renderTime: 0,
|
|
lastUpdateTime: 0,
|
|
virtualizationEnabled: false,
|
|
},
|
|
dragState: {
|
|
activeTaskId: null,
|
|
activeGroupId: null,
|
|
overId: null,
|
|
isDragging: false,
|
|
},
|
|
selectedTaskIds: [],
|
|
expandedSubtasks: {},
|
|
columnOrder: [],
|
|
editableSectionId: null,
|
|
};
|
|
|
|
// Performance monitoring utility
|
|
const calculatePerformanceMetrics = (taskGroups: ITaskListGroup[]) => {
|
|
const totalTasks = taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
|
|
const groupSizes = taskGroups.map(group => group.tasks.length);
|
|
const largestGroupSize = Math.max(...groupSizes, 0);
|
|
const averageGroupSize = groupSizes.length > 0 ? totalTasks / groupSizes.length : 0;
|
|
|
|
return {
|
|
totalTasks,
|
|
largestGroupSize,
|
|
averageGroupSize,
|
|
renderTime: performance.now(),
|
|
lastUpdateTime: Date.now(),
|
|
virtualizationEnabled: largestGroupSize > 50,
|
|
};
|
|
};
|
|
|
|
// Optimized task fetching with caching
|
|
export const fetchEnhancedKanbanGroups = createAsyncThunk(
|
|
'enhancedKanban/fetchGroups',
|
|
async (projectId: string, { rejectWithValue, getState }) => {
|
|
try {
|
|
const state = getState() as { enhancedKanbanReducer: EnhancedKanbanState };
|
|
const { enhancedKanbanReducer } = state;
|
|
const selectedMembers = enhancedKanbanReducer.taskAssignees
|
|
.filter(member => member.selected)
|
|
.map(member => member.id)
|
|
.join(' ');
|
|
|
|
const selectedLabels = enhancedKanbanReducer.labels
|
|
.filter(label => label.selected)
|
|
.map(label => label.id)
|
|
.join(' ');
|
|
|
|
const config: ITaskListConfigV2 = {
|
|
id: projectId,
|
|
archived: enhancedKanbanReducer.archived,
|
|
group: enhancedKanbanReducer.groupBy,
|
|
field: enhancedKanbanReducer.fields
|
|
.map(field => `${field.key} ${field.sort_order}`)
|
|
.join(','),
|
|
order: '',
|
|
search: enhancedKanbanReducer.search || '',
|
|
statuses: '',
|
|
members: selectedMembers,
|
|
projects: '',
|
|
isSubtasksInclude: enhancedKanbanReducer.isSubtasksInclude,
|
|
labels: selectedLabels,
|
|
priorities: enhancedKanbanReducer.priorities.join(' '),
|
|
};
|
|
|
|
const response = await tasksApiService.getTaskList(config);
|
|
return response.body;
|
|
} catch (error) {
|
|
logger.error('Fetch Enhanced Kanban Groups', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch task groups');
|
|
}
|
|
}
|
|
);
|
|
|
|
// Optimized task reordering
|
|
export const reorderEnhancedKanbanTasks = createAsyncThunk(
|
|
'enhancedKanban/reorderTasks',
|
|
async (
|
|
{
|
|
activeGroupId,
|
|
overGroupId,
|
|
fromIndex,
|
|
toIndex,
|
|
task,
|
|
updatedSourceTasks,
|
|
updatedTargetTasks,
|
|
}: {
|
|
activeGroupId: string;
|
|
overGroupId: string;
|
|
fromIndex: number;
|
|
toIndex: number;
|
|
task: IProjectTask;
|
|
updatedSourceTasks: IProjectTask[];
|
|
updatedTargetTasks: IProjectTask[];
|
|
},
|
|
{ rejectWithValue }
|
|
) => {
|
|
try {
|
|
// Optimistic update - return immediately for UI responsiveness
|
|
return {
|
|
activeGroupId,
|
|
overGroupId,
|
|
fromIndex,
|
|
toIndex,
|
|
task,
|
|
updatedSourceTasks,
|
|
updatedTargetTasks,
|
|
};
|
|
} catch (error) {
|
|
logger.error('Reorder Enhanced Kanban Tasks', error);
|
|
return rejectWithValue('Failed to reorder tasks');
|
|
}
|
|
}
|
|
);
|
|
|
|
// Group reordering
|
|
export const reorderEnhancedKanbanGroups = createAsyncThunk(
|
|
'enhancedKanban/reorderGroups',
|
|
async (
|
|
{
|
|
fromIndex,
|
|
toIndex,
|
|
reorderedGroups,
|
|
}: {
|
|
fromIndex: number;
|
|
toIndex: number;
|
|
reorderedGroups: ITaskListGroup[];
|
|
},
|
|
{ rejectWithValue }
|
|
) => {
|
|
try {
|
|
// Optimistic update - return immediately for UI responsiveness
|
|
return {
|
|
fromIndex,
|
|
toIndex,
|
|
reorderedGroups,
|
|
};
|
|
} catch (error) {
|
|
logger.error('Reorder Enhanced Kanban Groups', error);
|
|
return rejectWithValue('Failed to reorder groups');
|
|
}
|
|
}
|
|
);
|
|
|
|
export const fetchBoardSubTasks = createAsyncThunk(
|
|
'enhancedKanban/fetchBoardSubTasks',
|
|
async (
|
|
{ taskId, projectId }: { taskId: string; projectId: string },
|
|
{ rejectWithValue, getState }
|
|
) => {
|
|
try {
|
|
// Use the dedicated subtasks API endpoint
|
|
const response = await subTasksApiService.getSubTasks(taskId);
|
|
return response.body || [];
|
|
} catch (error) {
|
|
logger.error('Fetch Enhanced Board Sub Tasks', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch sub tasks');
|
|
}
|
|
}
|
|
);
|
|
|
|
// Async thunk for loading task assignees
|
|
export const fetchEnhancedKanbanTaskAssignees = createAsyncThunk(
|
|
'enhancedKanban/fetchTaskAssignees',
|
|
async (projectId: string, { rejectWithValue }) => {
|
|
try {
|
|
const response = await tasksApiService.fetchTaskAssignees(projectId);
|
|
return response.body;
|
|
} catch (error) {
|
|
logger.error('Fetch Enhanced Kanban Task Assignees', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch task assignees');
|
|
}
|
|
}
|
|
);
|
|
|
|
// Async thunk for loading labels
|
|
export const fetchEnhancedKanbanLabels = createAsyncThunk(
|
|
'enhancedKanban/fetchLabels',
|
|
async (projectId: string, { rejectWithValue }) => {
|
|
try {
|
|
const response = await labelsApiService.getPriorityByProject(projectId);
|
|
return response.body;
|
|
} catch (error) {
|
|
logger.error('Fetch Enhanced Kanban Labels', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch project labels');
|
|
}
|
|
}
|
|
);
|
|
|
|
// Helper functions for common operations (similar to board-slice.ts)
|
|
const findTaskInAllGroups = (
|
|
taskGroups: ITaskListGroup[],
|
|
taskId: string
|
|
): { task: IProjectTask; group: ITaskListGroup; groupId: string } | null => {
|
|
for (const group of taskGroups) {
|
|
const task = group.tasks.find(t => t.id === taskId);
|
|
if (task) return { task, group, groupId: group.id };
|
|
|
|
// Check in subtasks
|
|
for (const parentTask of group.tasks) {
|
|
if (!parentTask.sub_tasks) continue;
|
|
const subtask = parentTask.sub_tasks.find(st => st.id === taskId);
|
|
if (subtask) return { task: subtask, group, groupId: group.id };
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const deleteTaskFromGroup = (
|
|
taskGroups: ITaskListGroup[],
|
|
task: IProjectTask,
|
|
groupId: string,
|
|
index: number | null = null
|
|
): void => {
|
|
const group = taskGroups.find(g => g.id === groupId);
|
|
if (!group || !task.id) return;
|
|
|
|
if (task.is_sub_task) {
|
|
const parentTask = group.tasks.find(t => t.id === task.parent_task_id);
|
|
if (parentTask) {
|
|
const subTaskIndex = parentTask.sub_tasks?.findIndex(t => t.id === task.id);
|
|
if (typeof subTaskIndex !== 'undefined' && subTaskIndex !== -1) {
|
|
parentTask.sub_tasks_count = Math.max((parentTask.sub_tasks_count || 0) - 1, 0);
|
|
parentTask.sub_tasks?.splice(subTaskIndex, 1);
|
|
}
|
|
}
|
|
} else {
|
|
const taskIndex = index ?? group.tasks.findIndex(t => t.id === task.id);
|
|
if (taskIndex !== -1) {
|
|
group.tasks.splice(taskIndex, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
const enhancedKanbanSlice = createSlice({
|
|
name: 'enhancedKanbanReducer',
|
|
initialState,
|
|
reducers: {
|
|
setGroupBy: (state, action: PayloadAction<IGroupBy>) => {
|
|
state.groupBy = action.payload;
|
|
setCurrentGroup(action.payload);
|
|
// Clear caches when grouping changes
|
|
state.taskCache = {};
|
|
state.groupCache = {};
|
|
},
|
|
|
|
setSearch: (state, action: PayloadAction<string | null>) => {
|
|
state.search = action.payload;
|
|
},
|
|
|
|
setArchived: (state, action: PayloadAction<boolean>) => {
|
|
state.archived = action.payload;
|
|
},
|
|
|
|
setVirtualizedRendering: (state, action: PayloadAction<boolean>) => {
|
|
state.virtualizedRendering = action.payload;
|
|
},
|
|
|
|
// Optimized drag state management
|
|
setDragState: (state, action: PayloadAction<Partial<EnhancedKanbanState['dragState']>>) => {
|
|
state.dragState = { ...state.dragState, ...action.payload };
|
|
},
|
|
|
|
// Task selection
|
|
selectTask: (state, action: PayloadAction<string>) => {
|
|
if (!state.selectedTaskIds.includes(action.payload)) {
|
|
state.selectedTaskIds.push(action.payload);
|
|
}
|
|
},
|
|
|
|
deselectTask: (state, action: PayloadAction<string>) => {
|
|
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== action.payload);
|
|
},
|
|
|
|
clearSelection: state => {
|
|
state.selectedTaskIds = [];
|
|
},
|
|
|
|
// Subtask expansion
|
|
toggleSubtaskExpansion: (state, action: PayloadAction<string>) => {
|
|
const taskId = action.payload;
|
|
if (state.expandedSubtasks[taskId]) {
|
|
delete state.expandedSubtasks[taskId];
|
|
} else {
|
|
state.expandedSubtasks[taskId] = true;
|
|
}
|
|
},
|
|
|
|
// Column reordering
|
|
reorderColumns: (state, action: PayloadAction<string[]>) => {
|
|
state.columnOrder = action.payload;
|
|
},
|
|
|
|
// Cache management
|
|
updateTaskCache: (state, action: PayloadAction<{ id: string; task: IProjectTask }>) => {
|
|
state.taskCache[action.payload.id] = action.payload.task;
|
|
},
|
|
|
|
updateGroupCache: (state, action: PayloadAction<{ id: string; group: ITaskListGroup }>) => {
|
|
state.groupCache[action.payload.id] = action.payload.group;
|
|
},
|
|
|
|
clearCaches: state => {
|
|
state.taskCache = {};
|
|
state.groupCache = {};
|
|
},
|
|
|
|
// Filter management
|
|
setTaskAssignees: (state, action: PayloadAction<ITaskListMemberFilter[]>) => {
|
|
state.taskAssignees = action.payload;
|
|
},
|
|
|
|
setLabels: (state, action: PayloadAction<ITaskLabelFilter[]>) => {
|
|
state.labels = action.payload;
|
|
},
|
|
|
|
setPriorities: (state, action: PayloadAction<string[]>) => {
|
|
state.priorities = action.payload;
|
|
},
|
|
|
|
setMembers: (state, action: PayloadAction<string[]>) => {
|
|
state.members = action.payload;
|
|
},
|
|
|
|
// New actions for filter selection that work with original data
|
|
setTaskAssigneeSelection: (state, action: PayloadAction<{ id: string; selected: boolean }>) => {
|
|
const { id, selected } = action.payload;
|
|
// Update both original and current data
|
|
state.originalTaskAssignees = state.originalTaskAssignees.map(assignee =>
|
|
assignee.id === id ? { ...assignee, selected } : assignee
|
|
);
|
|
state.taskAssignees = state.originalTaskAssignees;
|
|
},
|
|
|
|
setLabelSelection: (state, action: PayloadAction<{ id: string; selected: boolean }>) => {
|
|
const { id, selected } = action.payload;
|
|
// Update both original and current data
|
|
state.originalLabels = state.originalLabels.map(label =>
|
|
label.id === id ? { ...label, selected } : label
|
|
);
|
|
state.labels = state.originalLabels;
|
|
},
|
|
|
|
// Add missing actions for filter compatibility
|
|
setSelectedPriorities: (state, action: PayloadAction<string[]>) => {
|
|
state.priorities = action.payload;
|
|
},
|
|
|
|
setBoardSearch: (state, action: PayloadAction<string | null>) => {
|
|
state.search = action.payload;
|
|
},
|
|
|
|
setBoardArchived: (state, action: PayloadAction<boolean>) => {
|
|
state.archived = action.payload;
|
|
},
|
|
|
|
// Status updates
|
|
updateTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
|
|
const { id: task_id, status_id } = action.payload;
|
|
|
|
// Update in all groups
|
|
state.taskGroups.forEach(group => {
|
|
group.tasks.forEach(task => {
|
|
if (task.id === task_id) {
|
|
task.status_id = status_id;
|
|
// Update cache
|
|
state.taskCache[task_id] = task;
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
// Enhanced Kanban external status update (for use in task drawer dropdown)
|
|
updateEnhancedKanbanTaskStatus: (
|
|
state,
|
|
action: PayloadAction<ITaskListStatusChangeResponse>
|
|
) => {
|
|
const {
|
|
id: task_id,
|
|
status_id,
|
|
color_code,
|
|
color_code_dark,
|
|
complete_ratio,
|
|
statusCategory,
|
|
} = action.payload;
|
|
let oldGroupId: string | null = null;
|
|
let foundTask: IProjectTask | null = null;
|
|
// Find the task and its group
|
|
for (const group of state.taskGroups) {
|
|
const task = group.tasks.find(t => t.id === task_id);
|
|
if (task) {
|
|
foundTask = task;
|
|
oldGroupId = group.id;
|
|
break;
|
|
}
|
|
}
|
|
if (!foundTask) return;
|
|
|
|
// Update the task properties
|
|
foundTask.status_color = color_code;
|
|
foundTask.status_color_dark = color_code_dark;
|
|
foundTask.complete_ratio = +complete_ratio;
|
|
foundTask.status = status_id;
|
|
foundTask.status_category = statusCategory;
|
|
|
|
// If grouped by status and the group changes, move the task
|
|
if (state.groupBy === IGroupBy.STATUS && oldGroupId && oldGroupId !== status_id) {
|
|
// Remove from old group
|
|
const oldGroup = state.taskGroups.find(g => g.id === oldGroupId);
|
|
if (oldGroup) {
|
|
oldGroup.tasks = oldGroup.tasks.filter(t => t.id !== task_id);
|
|
}
|
|
// Add to new group at the top
|
|
const newGroup = state.taskGroups.find(g => g.id === status_id);
|
|
if (newGroup) {
|
|
foundTask.status_id = status_id;
|
|
newGroup.tasks.unshift(foundTask);
|
|
}
|
|
} else {
|
|
// Just update the status_id
|
|
foundTask.status_id = status_id;
|
|
}
|
|
// Update cache
|
|
state.taskCache[task_id] = foundTask;
|
|
},
|
|
|
|
// Enhanced Kanban priority update (for use in task drawer dropdown)
|
|
updateEnhancedKanbanTaskPriority: (
|
|
state,
|
|
action: PayloadAction<ITaskListPriorityChangeResponse>
|
|
) => {
|
|
const { id, priority_id, color_code, color_code_dark } = action.payload;
|
|
// Find the task in any group
|
|
const taskInfo = findTaskInAllGroups(state.taskGroups, id);
|
|
if (!taskInfo || !priority_id) return;
|
|
|
|
const { task, groupId } = taskInfo;
|
|
|
|
// Update the task properties
|
|
task.priority = priority_id;
|
|
task.priority_color = color_code;
|
|
task.priority_color_dark = color_code_dark;
|
|
|
|
// If grouped by priority and not a subtask, move the task to the new priority group
|
|
if (state.groupBy === IGroupBy.PRIORITY && !task.is_sub_task && groupId !== priority_id) {
|
|
// Remove from current group
|
|
deleteTaskFromGroup(state.taskGroups, task, groupId);
|
|
|
|
// Add to new priority group
|
|
const newGroup = state.taskGroups.find(g => g.id === priority_id);
|
|
if (newGroup) {
|
|
newGroup.tasks.unshift(task);
|
|
state.groupCache[priority_id] = newGroup;
|
|
}
|
|
}
|
|
|
|
// Update cache
|
|
state.taskCache[id] = task;
|
|
},
|
|
// Enhanced Kanban assignee update (for use in task drawer dropdown)
|
|
updateEnhancedKanbanTaskAssignees: (
|
|
state,
|
|
action: PayloadAction<ITaskAssigneesUpdateResponse>
|
|
) => {
|
|
const { id, assignees, names } = action.payload;
|
|
|
|
// Find the task in any group
|
|
const taskInfo = findTaskInAllGroups(state.taskGroups, id);
|
|
if (!taskInfo) return;
|
|
|
|
const { task } = taskInfo;
|
|
|
|
// Update the task properties
|
|
task.assignees = assignees as ITaskAssignee[];
|
|
task.names = names as InlineMember[];
|
|
|
|
// Update cache
|
|
state.taskCache[id] = task;
|
|
},
|
|
|
|
// Enhanced Kanban label update (for use in task drawer dropdown)
|
|
updateEnhancedKanbanTaskLabels: (state, action: PayloadAction<ILabelsChangeResponse>) => {
|
|
const label = action.payload;
|
|
for (const group of state.taskGroups) {
|
|
// Find the task or its subtask
|
|
const task =
|
|
group.tasks.find(task => task.id === label.id) ||
|
|
group.tasks
|
|
.flatMap(task => task.sub_tasks || [])
|
|
.find(subtask => subtask.id === label.id);
|
|
if (task) {
|
|
task.labels = label.labels || [];
|
|
task.all_labels = label.all_labels || [];
|
|
// Update cache
|
|
state.taskCache[label.id] = task;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Enhanced Kanban progress update (for use in task drawer and socket events)
|
|
updateEnhancedKanbanTaskProgress: (
|
|
state,
|
|
action: PayloadAction<{
|
|
id: string;
|
|
complete_ratio: number;
|
|
completed_count: number;
|
|
total_tasks_count: number;
|
|
parent_task: string;
|
|
}>
|
|
) => {
|
|
const { id, complete_ratio, completed_count, total_tasks_count, parent_task } =
|
|
action.payload;
|
|
|
|
// Find the task in any group
|
|
const taskInfo = findTaskInAllGroups(state.taskGroups, parent_task || id);
|
|
|
|
// Check if taskInfo exists before destructuring
|
|
if (!taskInfo) return;
|
|
|
|
const { task } = taskInfo;
|
|
|
|
// Update the task properties
|
|
task.complete_ratio = +complete_ratio;
|
|
task.completed_count = completed_count;
|
|
task.total_tasks_count = total_tasks_count;
|
|
|
|
// Update cache
|
|
state.taskCache[parent_task || id] = task;
|
|
},
|
|
|
|
// Enhanced Kanban task name update (for use in task drawer header)
|
|
updateEnhancedKanbanTaskName: (
|
|
state,
|
|
action: PayloadAction<{
|
|
task: IProjectTask;
|
|
}>
|
|
) => {
|
|
const { task } = action.payload;
|
|
|
|
// Find the task and update it
|
|
const result = findTaskInAllGroups(state.taskGroups, task.id || '');
|
|
if (result) {
|
|
result.task.name = task.name;
|
|
// Update cache
|
|
state.taskCache[task.id!] = result.task;
|
|
}
|
|
},
|
|
|
|
updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
|
|
const { id: task_id, priority_id } = action.payload;
|
|
|
|
// Update in all groups
|
|
state.taskGroups.forEach(group => {
|
|
group.tasks.forEach(task => {
|
|
if (task.id === task_id) {
|
|
task.priority = priority_id;
|
|
// Update cache
|
|
state.taskCache[task_id] = task;
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
// Task deletion
|
|
deleteTask: (state, action: PayloadAction<string>) => {
|
|
const taskId = action.payload;
|
|
|
|
// Remove from all groups
|
|
state.taskGroups.forEach(group => {
|
|
group.tasks = group.tasks.filter(task => task.id !== taskId);
|
|
});
|
|
|
|
// Remove from caches
|
|
delete state.taskCache[taskId];
|
|
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId);
|
|
},
|
|
|
|
// Reset state
|
|
resetState: state => {
|
|
return { ...initialState, groupBy: state.groupBy };
|
|
},
|
|
|
|
// Synchronous reorder for tasks
|
|
reorderTasks: (
|
|
state,
|
|
action: PayloadAction<{
|
|
activeGroupId: string;
|
|
overGroupId: string;
|
|
fromIndex: number;
|
|
toIndex: number;
|
|
task: IProjectTask;
|
|
updatedSourceTasks: IProjectTask[];
|
|
updatedTargetTasks: IProjectTask[];
|
|
}>
|
|
) => {
|
|
const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } = action.payload;
|
|
const sourceGroupIndex = state.taskGroups.findIndex(group => group.id === activeGroupId);
|
|
const targetGroupIndex = state.taskGroups.findIndex(group => group.id === overGroupId);
|
|
if (sourceGroupIndex !== -1) {
|
|
state.taskGroups[sourceGroupIndex].tasks = updatedSourceTasks;
|
|
state.groupCache[activeGroupId] = state.taskGroups[sourceGroupIndex];
|
|
}
|
|
if (targetGroupIndex !== -1 && activeGroupId !== overGroupId) {
|
|
state.taskGroups[targetGroupIndex].tasks = updatedTargetTasks;
|
|
state.groupCache[overGroupId] = state.taskGroups[targetGroupIndex];
|
|
}
|
|
},
|
|
|
|
// Synchronous reorder for groups
|
|
reorderGroups: (
|
|
state,
|
|
action: PayloadAction<{
|
|
fromIndex: number;
|
|
toIndex: number;
|
|
reorderedGroups: ITaskListGroup[];
|
|
}>
|
|
) => {
|
|
const { reorderedGroups } = action.payload;
|
|
state.taskGroups = reorderedGroups;
|
|
state.groupCache = reorderedGroups.reduce(
|
|
(cache, group) => {
|
|
cache[group.id] = group;
|
|
return cache;
|
|
},
|
|
{} as Record<string, ITaskListGroup>
|
|
);
|
|
state.columnOrder = reorderedGroups.map(group => group.id);
|
|
},
|
|
|
|
addTaskToGroup: (state, action) => {
|
|
const { sectionId, task } = action.payload;
|
|
const group = state.taskGroups.find(g => g.id === sectionId);
|
|
if (group) {
|
|
group.tasks.push(task);
|
|
// Update cache
|
|
state.taskCache[task.id!] = task;
|
|
state.groupCache[sectionId] = group;
|
|
}
|
|
},
|
|
|
|
// Enhanced Kanban end date update (for use in task drawer and socket events)
|
|
updateEnhancedKanbanTaskEndDate: (
|
|
state,
|
|
action: PayloadAction<{
|
|
task: IProjectTask;
|
|
}>
|
|
) => {
|
|
const { task } = action.payload;
|
|
|
|
// Find the task and update it
|
|
const result = findTaskInAllGroups(state.taskGroups, task.id || '');
|
|
if (result) {
|
|
result.task.end_date = task.end_date;
|
|
// Update cache
|
|
state.taskCache[task.id!] = result.task;
|
|
}
|
|
},
|
|
|
|
// Enhanced Kanban start date update (for use in task drawer and socket events)
|
|
updateEnhancedKanbanTaskStartDate: (
|
|
state,
|
|
action: PayloadAction<{
|
|
task: IProjectTask;
|
|
}>
|
|
) => {
|
|
const { task } = action.payload;
|
|
|
|
// Find the task and update it
|
|
const result = findTaskInAllGroups(state.taskGroups, task.id || '');
|
|
if (result) {
|
|
result.task.start_date = task.start_date;
|
|
// Update cache
|
|
state.taskCache[task.id!] = result.task;
|
|
}
|
|
},
|
|
|
|
// Enhanced Kanban task expansion toggle (for subtask expand/collapse)
|
|
toggleTaskExpansion: (state, action: PayloadAction<string>) => {
|
|
const taskId = action.payload;
|
|
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
|
|
|
if (result) {
|
|
result.task.show_sub_tasks = !result.task.show_sub_tasks;
|
|
// Update cache
|
|
state.taskCache[taskId] = result.task;
|
|
}
|
|
},
|
|
|
|
// Enhanced Kanban subtask update (for use in task drawer and socket events)
|
|
updateEnhancedKanbanSubtask: (
|
|
state,
|
|
action: PayloadAction<{
|
|
sectionId: string;
|
|
subtask: IProjectTask;
|
|
mode: 'add' | 'delete';
|
|
}>
|
|
) => {
|
|
const { sectionId, subtask, mode } = action.payload;
|
|
const parentTaskId = subtask?.parent_task_id || null;
|
|
|
|
if (!parentTaskId) return;
|
|
|
|
// Function to update a task with a new subtask
|
|
const updateTaskWithSubtask = (task: IProjectTask): boolean => {
|
|
if (!task) return false;
|
|
|
|
// Initialize sub_tasks array if it doesn't exist
|
|
if (!task.sub_tasks) {
|
|
task.sub_tasks = [];
|
|
}
|
|
|
|
if (mode === 'add') {
|
|
// Increment subtask count
|
|
task.sub_tasks_count = (task.sub_tasks_count || 0) + 1;
|
|
|
|
// Add the subtask
|
|
task.sub_tasks.push({ ...subtask });
|
|
} else {
|
|
// Remove the subtask
|
|
task.sub_tasks = task.sub_tasks.filter(t => t.id !== subtask.id);
|
|
task.sub_tasks_count = Math.max(0, (task.sub_tasks_count || 1) - 1);
|
|
}
|
|
|
|
// Update cache
|
|
state.taskCache[task.id!] = task;
|
|
return true;
|
|
};
|
|
|
|
// First try to find the task in the specified section
|
|
if (sectionId) {
|
|
const section = state.taskGroups.find(sec => sec.id === sectionId);
|
|
if (section) {
|
|
const task = section.tasks.find(task => task.id === parentTaskId);
|
|
if (task && updateTaskWithSubtask(task)) {
|
|
// Update group cache
|
|
state.groupCache[sectionId] = section;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not found in the specified section, try all groups
|
|
const result = findTaskInAllGroups(state.taskGroups, parentTaskId);
|
|
if (result) {
|
|
updateTaskWithSubtask(result.task);
|
|
// Update group cache
|
|
state.groupCache[result.groupId] = result.group;
|
|
}
|
|
},
|
|
|
|
setEditableSection: (state, action: PayloadAction<string | null>) => {
|
|
state.editableSectionId = action.payload;
|
|
},
|
|
|
|
deleteSection: (state, action: PayloadAction<{ sectionId: string }>) => {
|
|
state.taskGroups = state.taskGroups.filter(
|
|
section => section.id !== action.payload.sectionId
|
|
);
|
|
if (state.editableSectionId === action.payload.sectionId) {
|
|
state.editableSectionId = null;
|
|
}
|
|
},
|
|
},
|
|
extraReducers: builder => {
|
|
builder
|
|
.addCase(fetchEnhancedKanbanGroups.pending, state => {
|
|
state.loadingGroups = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchEnhancedKanbanGroups.fulfilled, (state, action) => {
|
|
state.loadingGroups = false;
|
|
state.taskGroups = action.payload;
|
|
|
|
// Update performance metrics
|
|
state.performanceMetrics = calculatePerformanceMetrics(action.payload);
|
|
|
|
// Update caches and initialize subtask properties
|
|
action.payload.forEach(group => {
|
|
state.groupCache[group.id] = group;
|
|
group.tasks.forEach(task => {
|
|
// Initialize subtask-related properties if they don't exist
|
|
if (task.sub_tasks === undefined) {
|
|
task.sub_tasks = [];
|
|
}
|
|
if (task.sub_tasks_loading === undefined) {
|
|
task.sub_tasks_loading = false;
|
|
}
|
|
if (task.show_sub_tasks === undefined) {
|
|
task.show_sub_tasks = false;
|
|
}
|
|
if (task.sub_tasks_count === undefined) {
|
|
task.sub_tasks_count = 0;
|
|
}
|
|
|
|
state.taskCache[task.id!] = task;
|
|
});
|
|
});
|
|
|
|
// Initialize column order if not set
|
|
if (state.columnOrder.length === 0) {
|
|
state.columnOrder = action.payload.map(group => group.id);
|
|
}
|
|
})
|
|
.addCase(fetchEnhancedKanbanGroups.rejected, (state, action) => {
|
|
state.loadingGroups = false;
|
|
state.error = action.payload as string;
|
|
})
|
|
.addCase(reorderEnhancedKanbanTasks.fulfilled, (state, action) => {
|
|
const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } =
|
|
action.payload;
|
|
|
|
// Update groups
|
|
const sourceGroupIndex = state.taskGroups.findIndex(group => group.id === activeGroupId);
|
|
const targetGroupIndex = state.taskGroups.findIndex(group => group.id === overGroupId);
|
|
|
|
if (sourceGroupIndex !== -1) {
|
|
state.taskGroups[sourceGroupIndex].tasks = updatedSourceTasks;
|
|
state.groupCache[activeGroupId] = state.taskGroups[sourceGroupIndex];
|
|
}
|
|
|
|
if (targetGroupIndex !== -1 && activeGroupId !== overGroupId) {
|
|
state.taskGroups[targetGroupIndex].tasks = updatedTargetTasks;
|
|
state.groupCache[overGroupId] = state.taskGroups[targetGroupIndex];
|
|
}
|
|
})
|
|
.addCase(reorderEnhancedKanbanGroups.fulfilled, (state, action) => {
|
|
const { fromIndex, toIndex, reorderedGroups } = action.payload;
|
|
|
|
// Update groups
|
|
state.taskGroups = reorderedGroups;
|
|
state.groupCache = reorderedGroups.reduce(
|
|
(cache, group) => {
|
|
cache[group.id] = group;
|
|
return cache;
|
|
},
|
|
{} as Record<string, ITaskListGroup>
|
|
);
|
|
|
|
// Update column order
|
|
state.columnOrder = reorderedGroups.map(group => group.id);
|
|
})
|
|
// Fetch Task Assignees
|
|
.addCase(fetchEnhancedKanbanTaskAssignees.pending, state => {
|
|
state.loadingAssignees = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchEnhancedKanbanTaskAssignees.fulfilled, (state, action) => {
|
|
state.loadingAssignees = false;
|
|
// Store original data and current data
|
|
state.originalTaskAssignees = action.payload;
|
|
state.taskAssignees = action.payload;
|
|
})
|
|
.addCase(fetchEnhancedKanbanTaskAssignees.rejected, (state, action) => {
|
|
state.loadingAssignees = false;
|
|
state.error = action.payload as string;
|
|
})
|
|
// Fetch Labels
|
|
.addCase(fetchEnhancedKanbanLabels.pending, state => {
|
|
state.loadingLabels = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchEnhancedKanbanLabels.fulfilled, (state, action) => {
|
|
state.loadingLabels = false;
|
|
// Transform labels to include selected property
|
|
const newLabels = action.payload.map((label: any) => ({ ...label, selected: false }));
|
|
// Store original data and current data
|
|
state.originalLabels = newLabels;
|
|
state.labels = newLabels;
|
|
})
|
|
.addCase(fetchEnhancedKanbanLabels.rejected, (state, action) => {
|
|
state.loadingLabels = false;
|
|
state.error = action.payload as string;
|
|
})
|
|
// Fetch Board Sub Tasks
|
|
.addCase(fetchBoardSubTasks.pending, (state, action) => {
|
|
state.error = null;
|
|
// Find the task and set sub_tasks_loading to true
|
|
const taskId = action.meta.arg.taskId;
|
|
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
|
if (result) {
|
|
result.task.sub_tasks_loading = true;
|
|
}
|
|
})
|
|
.addCase(fetchBoardSubTasks.fulfilled, (state, action: PayloadAction<IProjectTask[]>) => {
|
|
const taskId = (action as any).meta?.arg?.taskId;
|
|
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
|
|
|
if (result) {
|
|
result.task.sub_tasks_loading = false;
|
|
result.task.sub_tasks = action.payload;
|
|
result.task.show_sub_tasks = true;
|
|
|
|
// Only update the count if we don't have a count yet or if the API returned a different count
|
|
// This preserves the original count from the initial data load
|
|
if (!result.task.sub_tasks_count || result.task.sub_tasks_count === 0) {
|
|
result.task.sub_tasks_count = action.payload.length;
|
|
}
|
|
|
|
// Update cache
|
|
state.taskCache[taskId] = result.task;
|
|
}
|
|
})
|
|
.addCase(fetchBoardSubTasks.rejected, (state, action) => {
|
|
state.error = action.error.message || 'Failed to fetch sub tasks';
|
|
// Set loading to false on rejection
|
|
const taskId = action.meta.arg.taskId;
|
|
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
|
if (result) {
|
|
result.task.sub_tasks_loading = false;
|
|
// Update cache
|
|
state.taskCache[taskId] = result.task;
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
export const {
|
|
setGroupBy,
|
|
setSearch,
|
|
setArchived,
|
|
setVirtualizedRendering,
|
|
setDragState,
|
|
selectTask,
|
|
deselectTask,
|
|
clearSelection,
|
|
toggleSubtaskExpansion,
|
|
reorderColumns,
|
|
updateTaskCache,
|
|
updateGroupCache,
|
|
clearCaches,
|
|
setTaskAssignees,
|
|
setLabels,
|
|
setPriorities,
|
|
setMembers,
|
|
setTaskAssigneeSelection,
|
|
setLabelSelection,
|
|
setSelectedPriorities,
|
|
setBoardSearch,
|
|
setBoardArchived,
|
|
updateTaskStatus,
|
|
updateTaskPriority,
|
|
deleteTask,
|
|
resetState,
|
|
reorderTasks,
|
|
reorderGroups,
|
|
addTaskToGroup,
|
|
updateEnhancedKanbanTaskStatus,
|
|
updateEnhancedKanbanTaskPriority,
|
|
updateEnhancedKanbanTaskAssignees,
|
|
updateEnhancedKanbanTaskLabels,
|
|
updateEnhancedKanbanTaskProgress,
|
|
updateEnhancedKanbanTaskName,
|
|
updateEnhancedKanbanTaskEndDate,
|
|
updateEnhancedKanbanTaskStartDate,
|
|
updateEnhancedKanbanSubtask,
|
|
toggleTaskExpansion,
|
|
setEditableSection,
|
|
deleteSection,
|
|
} = enhancedKanbanSlice.actions;
|
|
|
|
export default enhancedKanbanSlice.reducer;
|