feat(enhanced-kanban): integrate react-window-infinite-loader and update project view
- Added react-window-infinite-loader to improve performance in rendering large lists. - Integrated enhancedKanbanReducer into the Redux store for state management. - Updated ProjectViewEnhancedBoard to utilize EnhancedKanbanBoard for better project visualization.
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
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 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';
|
||||
|
||||
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.enhanced-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
|
||||
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[];
|
||||
}
|
||||
|
||||
const initialState: EnhancedKanbanState = {
|
||||
search: null,
|
||||
archived: false,
|
||||
groupBy: getCurrentGroup(),
|
||||
isSubtasksInclude: false,
|
||||
fields: [],
|
||||
taskGroups: [],
|
||||
loadingGroups: false,
|
||||
error: null,
|
||||
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: [],
|
||||
};
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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 };
|
||||
},
|
||||
},
|
||||
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
|
||||
action.payload.forEach(group => {
|
||||
state.groupCache[group.id] = group;
|
||||
group.tasks.forEach(task => {
|
||||
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];
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setGroupBy,
|
||||
setSearch,
|
||||
setArchived,
|
||||
setVirtualizedRendering,
|
||||
setDragState,
|
||||
selectTask,
|
||||
deselectTask,
|
||||
clearSelection,
|
||||
toggleSubtaskExpansion,
|
||||
reorderColumns,
|
||||
updateTaskCache,
|
||||
updateGroupCache,
|
||||
clearCaches,
|
||||
setTaskAssignees,
|
||||
setLabels,
|
||||
setPriorities,
|
||||
setMembers,
|
||||
updateTaskStatus,
|
||||
updateTaskPriority,
|
||||
deleteTask,
|
||||
resetState,
|
||||
} = enhancedKanbanSlice.actions;
|
||||
|
||||
export default enhancedKanbanSlice.reducer;
|
||||
Reference in New Issue
Block a user