feat(store): integrate task management reducers into the store
- Added taskManagementReducer, groupingReducer, and selectionReducer to the Redux store. - Organized imports and store configuration for better clarity and maintainability.
This commit is contained in:
189
worklenz-frontend/src/features/task-management/grouping.slice.ts
Normal file
189
worklenz-frontend/src/features/task-management/grouping.slice.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit';
|
||||
import { GroupingState, TaskGroup } from '@/types/task-management.types';
|
||||
import { RootState } from '@/app/store';
|
||||
import { taskManagementSelectors } from './task-management.slice';
|
||||
|
||||
const initialState: GroupingState = {
|
||||
currentGrouping: 'status',
|
||||
customPhases: ['Planning', 'Development', 'Testing', 'Deployment'],
|
||||
groupOrder: {
|
||||
status: ['todo', 'doing', 'done'],
|
||||
priority: ['critical', 'high', 'medium', 'low'],
|
||||
phase: ['Planning', 'Development', 'Testing', 'Deployment'],
|
||||
},
|
||||
groupStates: {},
|
||||
};
|
||||
|
||||
const groupingSlice = createSlice({
|
||||
name: 'grouping',
|
||||
initialState,
|
||||
reducers: {
|
||||
setCurrentGrouping: (state, action: PayloadAction<'status' | 'priority' | 'phase'>) => {
|
||||
state.currentGrouping = action.payload;
|
||||
},
|
||||
|
||||
addCustomPhase: (state, action: PayloadAction<string>) => {
|
||||
const phase = action.payload.trim();
|
||||
if (phase && !state.customPhases.includes(phase)) {
|
||||
state.customPhases.push(phase);
|
||||
state.groupOrder.phase.push(phase);
|
||||
}
|
||||
},
|
||||
|
||||
removeCustomPhase: (state, action: PayloadAction<string>) => {
|
||||
const phase = action.payload;
|
||||
state.customPhases = state.customPhases.filter(p => p !== phase);
|
||||
state.groupOrder.phase = state.groupOrder.phase.filter(p => p !== phase);
|
||||
},
|
||||
|
||||
updateCustomPhases: (state, action: PayloadAction<string[]>) => {
|
||||
state.customPhases = action.payload;
|
||||
state.groupOrder.phase = action.payload;
|
||||
},
|
||||
|
||||
updateGroupOrder: (state, action: PayloadAction<{ groupType: string; order: string[] }>) => {
|
||||
const { groupType, order } = action.payload;
|
||||
state.groupOrder[groupType] = order;
|
||||
},
|
||||
|
||||
toggleGroupCollapsed: (state, action: PayloadAction<string>) => {
|
||||
const groupId = action.payload;
|
||||
if (!state.groupStates[groupId]) {
|
||||
state.groupStates[groupId] = { collapsed: false };
|
||||
}
|
||||
state.groupStates[groupId].collapsed = !state.groupStates[groupId].collapsed;
|
||||
},
|
||||
|
||||
setGroupCollapsed: (state, action: PayloadAction<{ groupId: string; collapsed: boolean }>) => {
|
||||
const { groupId, collapsed } = action.payload;
|
||||
if (!state.groupStates[groupId]) {
|
||||
state.groupStates[groupId] = { collapsed: false };
|
||||
}
|
||||
state.groupStates[groupId].collapsed = collapsed;
|
||||
},
|
||||
|
||||
collapseAllGroups: (state) => {
|
||||
Object.keys(state.groupStates).forEach(groupId => {
|
||||
state.groupStates[groupId].collapsed = true;
|
||||
});
|
||||
},
|
||||
|
||||
expandAllGroups: (state) => {
|
||||
Object.keys(state.groupStates).forEach(groupId => {
|
||||
state.groupStates[groupId].collapsed = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setCurrentGrouping,
|
||||
addCustomPhase,
|
||||
removeCustomPhase,
|
||||
updateCustomPhases,
|
||||
updateGroupOrder,
|
||||
toggleGroupCollapsed,
|
||||
setGroupCollapsed,
|
||||
collapseAllGroups,
|
||||
expandAllGroups,
|
||||
} = groupingSlice.actions;
|
||||
|
||||
// Selectors
|
||||
export const selectCurrentGrouping = (state: RootState) => state.grouping.currentGrouping;
|
||||
export const selectCustomPhases = (state: RootState) => state.grouping.customPhases;
|
||||
export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder;
|
||||
export const selectGroupStates = (state: RootState) => state.grouping.groupStates;
|
||||
|
||||
// Complex selectors using createSelector for memoization
|
||||
export const selectCurrentGroupOrder = createSelector(
|
||||
[selectCurrentGrouping, selectGroupOrder],
|
||||
(currentGrouping, groupOrder) => groupOrder[currentGrouping] || []
|
||||
);
|
||||
|
||||
export const selectTaskGroups = createSelector(
|
||||
[taskManagementSelectors.selectAll, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates],
|
||||
(tasks, currentGrouping, groupOrder, groupStates) => {
|
||||
const groups: TaskGroup[] = [];
|
||||
|
||||
// Get unique values for the current grouping
|
||||
const groupValues = groupOrder.length > 0 ? groupOrder :
|
||||
[...new Set(tasks.map(task => {
|
||||
if (currentGrouping === 'status') return task.status;
|
||||
if (currentGrouping === 'priority') return task.priority;
|
||||
return task.phase;
|
||||
}))];
|
||||
|
||||
groupValues.forEach(value => {
|
||||
const tasksInGroup = tasks.filter(task => {
|
||||
if (currentGrouping === 'status') return task.status === value;
|
||||
if (currentGrouping === 'priority') return task.priority === value;
|
||||
return task.phase === value;
|
||||
}).sort((a, b) => a.order - b.order);
|
||||
|
||||
const groupId = `${currentGrouping}-${value}`;
|
||||
|
||||
groups.push({
|
||||
id: groupId,
|
||||
title: value.charAt(0).toUpperCase() + value.slice(1),
|
||||
groupType: currentGrouping,
|
||||
groupValue: value,
|
||||
collapsed: groupStates[groupId]?.collapsed || false,
|
||||
taskIds: tasksInGroup.map(task => task.id),
|
||||
color: getGroupColor(currentGrouping, value),
|
||||
});
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectTasksByCurrentGrouping = createSelector(
|
||||
[taskManagementSelectors.selectAll, selectCurrentGrouping],
|
||||
(tasks, currentGrouping) => {
|
||||
const grouped: Record<string, typeof tasks> = {};
|
||||
|
||||
tasks.forEach(task => {
|
||||
let key: string;
|
||||
if (currentGrouping === 'status') key = task.status;
|
||||
else if (currentGrouping === 'priority') key = task.priority;
|
||||
else key = task.phase;
|
||||
|
||||
if (!grouped[key]) grouped[key] = [];
|
||||
grouped[key].push(task);
|
||||
});
|
||||
|
||||
// Sort tasks within each group by order
|
||||
Object.keys(grouped).forEach(key => {
|
||||
grouped[key].sort((a, b) => a.order - b.order);
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function to get group colors
|
||||
const getGroupColor = (groupType: string, value: string): string => {
|
||||
const colorMaps = {
|
||||
status: {
|
||||
todo: '#f0f0f0',
|
||||
doing: '#1890ff',
|
||||
done: '#52c41a',
|
||||
},
|
||||
priority: {
|
||||
critical: '#ff4d4f',
|
||||
high: '#ff7a45',
|
||||
medium: '#faad14',
|
||||
low: '#52c41a',
|
||||
},
|
||||
phase: {
|
||||
Planning: '#722ed1',
|
||||
Development: '#1890ff',
|
||||
Testing: '#faad14',
|
||||
Deployment: '#52c41a',
|
||||
},
|
||||
};
|
||||
|
||||
return colorMaps[groupType as keyof typeof colorMaps]?.[value as keyof any] || '#d9d9d9';
|
||||
};
|
||||
|
||||
export default groupingSlice.reducer;
|
||||
@@ -0,0 +1,110 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { SelectionState } from '@/types/task-management.types';
|
||||
import { RootState } from '@/app/store';
|
||||
|
||||
const initialState: SelectionState = {
|
||||
selectedTaskIds: [],
|
||||
lastSelectedId: null,
|
||||
};
|
||||
|
||||
const selectionSlice = createSlice({
|
||||
name: 'selection',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleTaskSelection: (state, action: PayloadAction<string>) => {
|
||||
const taskId = action.payload;
|
||||
const index = state.selectedTaskIds.indexOf(taskId);
|
||||
|
||||
if (index === -1) {
|
||||
state.selectedTaskIds.push(taskId);
|
||||
} else {
|
||||
state.selectedTaskIds.splice(index, 1);
|
||||
}
|
||||
|
||||
state.lastSelectedId = taskId;
|
||||
},
|
||||
|
||||
selectTask: (state, action: PayloadAction<string>) => {
|
||||
const taskId = action.payload;
|
||||
if (!state.selectedTaskIds.includes(taskId)) {
|
||||
state.selectedTaskIds.push(taskId);
|
||||
}
|
||||
state.lastSelectedId = taskId;
|
||||
},
|
||||
|
||||
deselectTask: (state, action: PayloadAction<string>) => {
|
||||
const taskId = action.payload;
|
||||
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId);
|
||||
if (state.lastSelectedId === taskId) {
|
||||
state.lastSelectedId = state.selectedTaskIds[state.selectedTaskIds.length - 1] || null;
|
||||
}
|
||||
},
|
||||
|
||||
selectMultipleTasks: (state, action: PayloadAction<string[]>) => {
|
||||
const taskIds = action.payload;
|
||||
// Add new task IDs that aren't already selected
|
||||
taskIds.forEach(id => {
|
||||
if (!state.selectedTaskIds.includes(id)) {
|
||||
state.selectedTaskIds.push(id);
|
||||
}
|
||||
});
|
||||
state.lastSelectedId = taskIds[taskIds.length - 1] || state.lastSelectedId;
|
||||
},
|
||||
|
||||
selectRangeTasks: (state, action: PayloadAction<{ startId: string; endId: string; allTaskIds: string[] }>) => {
|
||||
const { startId, endId, allTaskIds } = action.payload;
|
||||
const startIndex = allTaskIds.indexOf(startId);
|
||||
const endIndex = allTaskIds.indexOf(endId);
|
||||
|
||||
if (startIndex !== -1 && endIndex !== -1) {
|
||||
const [start, end] = startIndex <= endIndex ? [startIndex, endIndex] : [endIndex, startIndex];
|
||||
const rangeIds = allTaskIds.slice(start, end + 1);
|
||||
|
||||
// Add range IDs that aren't already selected
|
||||
rangeIds.forEach(id => {
|
||||
if (!state.selectedTaskIds.includes(id)) {
|
||||
state.selectedTaskIds.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
state.lastSelectedId = endId;
|
||||
}
|
||||
},
|
||||
|
||||
selectAllTasks: (state, action: PayloadAction<string[]>) => {
|
||||
state.selectedTaskIds = action.payload;
|
||||
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
|
||||
},
|
||||
|
||||
clearSelection: (state) => {
|
||||
state.selectedTaskIds = [];
|
||||
state.lastSelectedId = null;
|
||||
},
|
||||
|
||||
setSelection: (state, action: PayloadAction<string[]>) => {
|
||||
state.selectedTaskIds = action.payload;
|
||||
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
toggleTaskSelection,
|
||||
selectTask,
|
||||
deselectTask,
|
||||
selectMultipleTasks,
|
||||
selectRangeTasks,
|
||||
selectAllTasks,
|
||||
clearSelection,
|
||||
setSelection,
|
||||
} = selectionSlice.actions;
|
||||
|
||||
// Selectors
|
||||
export const selectSelectedTaskIds = (state: RootState) => state.taskManagementSelection.selectedTaskIds;
|
||||
export const selectLastSelectedId = (state: RootState) => state.taskManagementSelection.lastSelectedId;
|
||||
export const selectHasSelection = (state: RootState) => state.taskManagementSelection.selectedTaskIds.length > 0;
|
||||
export const selectSelectionCount = (state: RootState) => state.taskManagementSelection.selectedTaskIds.length;
|
||||
export const selectIsTaskSelected = (taskId: string) => (state: RootState) =>
|
||||
state.taskManagementSelection.selectedTaskIds.includes(taskId);
|
||||
|
||||
export default selectionSlice.reducer;
|
||||
@@ -0,0 +1,135 @@
|
||||
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { Task, TaskManagementState } from '@/types/task-management.types';
|
||||
import { RootState } from '@/app/store';
|
||||
|
||||
// Entity adapter for normalized state
|
||||
const tasksAdapter = createEntityAdapter<Task>({
|
||||
selectId: (task) => task.id,
|
||||
sortComparer: (a, b) => a.order - b.order,
|
||||
});
|
||||
|
||||
const initialState: TaskManagementState = {
|
||||
entities: {},
|
||||
ids: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const taskManagementSlice = createSlice({
|
||||
name: 'taskManagement',
|
||||
initialState: tasksAdapter.getInitialState(initialState),
|
||||
reducers: {
|
||||
// Basic CRUD operations
|
||||
setTasks: (state, action: PayloadAction<Task[]>) => {
|
||||
tasksAdapter.setAll(state, action.payload);
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
},
|
||||
|
||||
addTask: (state, action: PayloadAction<Task>) => {
|
||||
tasksAdapter.addOne(state, action.payload);
|
||||
},
|
||||
|
||||
updateTask: (state, action: PayloadAction<{ id: string; changes: Partial<Task> }>) => {
|
||||
tasksAdapter.updateOne(state, {
|
||||
id: action.payload.id,
|
||||
changes: {
|
||||
...action.payload.changes,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
deleteTask: (state, action: PayloadAction<string>) => {
|
||||
tasksAdapter.removeOne(state, action.payload);
|
||||
},
|
||||
|
||||
// Bulk operations
|
||||
bulkUpdateTasks: (state, action: PayloadAction<{ ids: string[]; changes: Partial<Task> }>) => {
|
||||
const { ids, changes } = action.payload;
|
||||
const updates = ids.map(id => ({
|
||||
id,
|
||||
changes: {
|
||||
...changes,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
}));
|
||||
tasksAdapter.updateMany(state, updates);
|
||||
},
|
||||
|
||||
bulkDeleteTasks: (state, action: PayloadAction<string[]>) => {
|
||||
tasksAdapter.removeMany(state, action.payload);
|
||||
},
|
||||
|
||||
// Drag and drop operations
|
||||
reorderTasks: (state, action: PayloadAction<{ taskIds: string[]; newOrder: number[] }>) => {
|
||||
const { taskIds, newOrder } = action.payload;
|
||||
const updates = taskIds.map((id, index) => ({
|
||||
id,
|
||||
changes: { order: newOrder[index] },
|
||||
}));
|
||||
tasksAdapter.updateMany(state, updates);
|
||||
},
|
||||
|
||||
moveTaskToGroup: (state, action: PayloadAction<{ taskId: string; groupType: 'status' | 'priority' | 'phase'; groupValue: string }>) => {
|
||||
const { taskId, groupType, groupValue } = action.payload;
|
||||
const changes: Partial<Task> = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Update the appropriate field based on group type
|
||||
if (groupType === 'status') {
|
||||
changes.status = groupValue as Task['status'];
|
||||
} else if (groupType === 'priority') {
|
||||
changes.priority = groupValue as Task['priority'];
|
||||
} else if (groupType === 'phase') {
|
||||
changes.phase = groupValue;
|
||||
}
|
||||
|
||||
tasksAdapter.updateOne(state, { id: taskId, changes });
|
||||
},
|
||||
|
||||
// Loading states
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload;
|
||||
},
|
||||
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload;
|
||||
state.loading = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setTasks,
|
||||
addTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
bulkUpdateTasks,
|
||||
bulkDeleteTasks,
|
||||
reorderTasks,
|
||||
moveTaskToGroup,
|
||||
setLoading,
|
||||
setError,
|
||||
} = taskManagementSlice.actions;
|
||||
|
||||
// Selectors
|
||||
export const taskManagementSelectors = tasksAdapter.getSelectors<RootState>(
|
||||
(state) => state.taskManagement
|
||||
);
|
||||
|
||||
// Additional selectors
|
||||
export const selectTasksByStatus = (state: RootState, status: string) =>
|
||||
taskManagementSelectors.selectAll(state).filter(task => task.status === status);
|
||||
|
||||
export const selectTasksByPriority = (state: RootState, priority: string) =>
|
||||
taskManagementSelectors.selectAll(state).filter(task => task.priority === priority);
|
||||
|
||||
export const selectTasksByPhase = (state: RootState, phase: string) =>
|
||||
taskManagementSelectors.selectAll(state).filter(task => task.phase === phase);
|
||||
|
||||
export const selectTasksLoading = (state: RootState) => state.taskManagement.loading;
|
||||
export const selectTasksError = (state: RootState) => state.taskManagement.error;
|
||||
|
||||
export default taskManagementSlice.reducer;
|
||||
Reference in New Issue
Block a user