feat(task-management): enhance task grouping and localization support

- Implemented unmapped task grouping for better organization of tasks without valid phases.
- Updated task distribution logic to handle unmapped tasks and added a corresponding group in the response.
- Enhanced localization by adding translations for "noTasksInGroup" in multiple languages.
- Improved task list components to support custom columns and better task management features.
- Refactored task management slice to include loading states for columns and custom columns.
This commit is contained in:
chamikaJ
2025-07-04 20:41:03 +05:30
parent 9e29031703
commit f30fde553d
23 changed files with 1560 additions and 380 deletions

View File

@@ -24,7 +24,7 @@ const ConfigPhaseButton = () => {
onClick={() => dispatch(toggleDrawer())}
icon={
<SettingOutlined
style={{ color: themeMode === 'dark' ? colors.white : colors.skyBlue }}
style={{ color: themeMode === 'dark' ? colors.white : 'black' }}
/>
}
/>

View File

@@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CustomTableColumnsType } from '../taskListColumns/taskColumnsSlice';
import { LabelType } from '../../../../types/label.type';
import { LabelType } from '../../../../pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/label-type-column/label-type-column';
import { SelectionType } from '../../../../pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/selection-type-column/selection-type-column';
export type CustomFieldsTypes =
@@ -21,6 +21,7 @@ type TaskListCustomColumnsState = {
isCustomColumnModalOpen: boolean;
customColumnModalType: 'create' | 'edit';
customColumnId: string | null;
currentColumnData: any | null; // Store the current column data for editing
customFieldType: CustomFieldsTypes;
customFieldNumberType: CustomFieldNumberTypes;
@@ -39,6 +40,7 @@ const initialState: TaskListCustomColumnsState = {
isCustomColumnModalOpen: false,
customColumnModalType: 'create',
customColumnId: null,
currentColumnData: null,
customFieldType: 'people',
customFieldNumberType: 'formatted',
@@ -62,10 +64,11 @@ const taskListCustomColumnsSlice = createSlice({
},
setCustomColumnModalAttributes: (
state,
action: PayloadAction<{ modalType: 'create' | 'edit'; columnId: string | null }>
action: PayloadAction<{ modalType: 'create' | 'edit'; columnId: string | null; columnData?: any }>
) => {
state.customColumnModalType = action.payload.modalType;
state.customColumnId = action.payload.columnId;
state.currentColumnData = action.payload.columnData || null;
},
setCustomFieldType: (state, action: PayloadAction<CustomFieldsTypes>) => {
state.customFieldType = action.payload;
@@ -98,7 +101,19 @@ const taskListCustomColumnsSlice = createSlice({
state.selectionsList = action.payload;
},
resetCustomFieldValues: state => {
state = initialState;
// Reset all field values to initial state while keeping modal state
state.customFieldType = initialState.customFieldType;
state.customFieldNumberType = initialState.customFieldNumberType;
state.decimals = initialState.decimals;
state.label = initialState.label;
state.labelPosition = initialState.labelPosition;
state.previewValue = initialState.previewValue;
state.expression = initialState.expression;
state.firstNumericColumn = initialState.firstNumericColumn;
state.secondNumericColumn = initialState.secondNumericColumn;
state.labelsList = initialState.labelsList;
state.selectionsList = initialState.selectionsList;
state.currentColumnData = initialState.currentColumnData;
},
},
});

View File

@@ -137,6 +137,14 @@ export const selectTaskGroups = createSelector(
tasks.map(task => {
if (currentGrouping === 'status') return task.status;
if (currentGrouping === 'priority') return task.priority;
if (currentGrouping === 'phase') {
// For phase grouping, use 'Unmapped' for tasks without a phase
if (!task.phase || task.phase.trim() === '') {
return 'Unmapped';
} else {
return task.phase;
}
}
return task.phase;
})
));
@@ -148,6 +156,13 @@ export const selectTaskGroups = createSelector(
.filter(task => {
if (currentGrouping === 'status') return task.status === value;
if (currentGrouping === 'priority') return task.priority === value;
if (currentGrouping === 'phase') {
if (value === 'Unmapped') {
return !task.phase || task.phase.trim() === '';
} else {
return task.phase === value;
}
}
return task.phase === value;
})
.sort((a, b) => (a.order || 0) - (b.order || 0));
@@ -178,9 +193,20 @@ export const selectTasksByCurrentGrouping = createSelector(
tasks.forEach(task => {
let key: string;
if (currentGrouping === 'status') key = task.status;
else if (currentGrouping === 'priority') key = task.priority;
else key = task.phase || 'Development';
if (currentGrouping === 'status') {
key = task.status;
} else if (currentGrouping === 'priority') {
key = task.priority;
} else if (currentGrouping === 'phase') {
// For phase grouping, use 'Unmapped' for tasks without a phase
if (!task.phase || task.phase.trim() === '') {
key = 'Unmapped';
} else {
key = task.phase;
}
} else {
key = task.phase || 'Development';
}
if (!grouped[key]) grouped[key] = [];
grouped[key].push(task);
@@ -214,6 +240,7 @@ const getGroupColor = (groupType: GroupingType, value: string): string => {
Development: '#1890ff',
Testing: '#faad14',
Deployment: '#52c41a',
Unmapped: '#fbc84c69',
},
};

View File

@@ -7,12 +7,14 @@ import {
EntityId,
} from '@reduxjs/toolkit';
import { Task, TaskManagementState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
import { ITaskListColumn } from '@/types/tasks/taskList.types';
import { RootState } from '@/app/store';
import {
tasksApiService,
ITaskListConfigV2,
ITaskListV3Response,
} from '@/api/tasks/tasks.api.service';
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
import logger from '@/utils/errorLogger';
import { DEFAULT_TASK_NAME } from '@/shared/constants';
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
@@ -56,6 +58,10 @@ const initialState: TaskManagementState = {
selectedPriorities: [],
search: '',
loadingSubtasks: {},
// Add column-related state
loadingColumns: false,
columns: [],
customColumns: [],
};
// Async thunk to fetch tasks from API
@@ -435,6 +441,59 @@ export const updateTaskWithSubtasks = createAsyncThunk(
}
);
// Add async thunk to fetch task list columns
export const fetchTaskListColumns = createAsyncThunk(
'taskManagement/fetchTaskListColumns',
async (projectId: string, { dispatch }) => {
const [standardColumns, customColumns] = await Promise.all([
tasksApiService.fetchTaskListColumns(projectId),
dispatch(fetchCustomColumns(projectId)),
]);
return {
standard: standardColumns.body,
custom: customColumns.payload,
};
}
);
// Add async thunk to fetch custom columns
export const fetchCustomColumns = createAsyncThunk(
'taskManagement/fetchCustomColumns',
async (projectId: string, { rejectWithValue }) => {
try {
const response = await tasksCustomColumnsService.getCustomColumns(projectId);
return response.body;
} catch (error) {
logger.error('Fetch Custom Columns', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to fetch custom columns');
}
}
);
// Add async thunk to update column visibility
export const updateColumnVisibility = createAsyncThunk(
'taskManagement/updateColumnVisibility',
async (
{ projectId, item }: { projectId: string; item: ITaskListColumn },
{ rejectWithValue }
) => {
try {
const response = await tasksApiService.toggleColumnVisibility(projectId, item);
return response.body;
} catch (error) {
logger.error('Update Column Visibility', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to update column visibility');
}
}
);
// Create the slice
const taskManagementSlice = createSlice({
name: 'taskManagement',
@@ -627,7 +686,12 @@ const taskManagementSlice = createSlice({
updatedTask.priority = destinationGroup.id;
break;
case IGroupBy.PHASE:
updatedTask.phase = destinationGroup.id;
// Handle unmapped group specially
if (destinationGroup.id === 'Unmapped' || destinationGroup.title === 'Unmapped') {
updatedTask.phase = ''; // Clear phase for unmapped group
} else {
updatedTask.phase = destinationGroup.id;
}
break;
case IGroupBy.MEMBERS:
// If moving to a member group, ensure task is assigned to that member
@@ -782,7 +846,57 @@ const taskManagementSlice = createSlice({
};
}
},
// Add column-related reducers
toggleColumnVisibility: (state, action: PayloadAction<string>) => {
const column = state.columns.find(col => col.key === action.payload);
if (column) {
column.pinned = !column.pinned;
}
},
addCustomColumn: (state, action: PayloadAction<ITaskListColumn>) => {
state.customColumns.push(action.payload);
// Also add to columns array to maintain visibility
state.columns.push({
...action.payload,
pinned: true, // New columns are visible by default
});
},
updateCustomColumn: (
state,
action: PayloadAction<{ key: string; column: ITaskListColumn }>
) => {
const { key, column } = action.payload;
const index = state.customColumns.findIndex(col => col.key === key);
if (index !== -1) {
state.customColumns[index] = column;
// Update in columns array as well
const colIndex = state.columns.findIndex(col => col.key === key);
if (colIndex !== -1) {
state.columns[colIndex] = { ...column, pinned: state.columns[colIndex].pinned };
}
}
},
deleteCustomColumn: (state, action: PayloadAction<string>) => {
const key = action.payload;
state.customColumns = state.customColumns.filter(col => col.key !== key);
// Remove from columns array as well
state.columns = state.columns.filter(col => col.key !== key);
},
// Add action to sync backend columns with local fields
syncColumnsWithFields: (state, action: PayloadAction<{ projectId: string; fields: any[] }>) => {
const { fields } = action.payload;
// Update columns based on local fields
state.columns = state.columns.map(column => {
const field = fields.find(f => f.key === column.key);
if (field) {
return {
...column,
pinned: field.visible
};
}
return column;
});
},
},
extraReducers: builder => {
builder
@@ -885,6 +999,60 @@ const taskManagementSlice = createSlice({
state.ids = [];
state.entities = {};
state.groups = [];
})
// Add column-related extraReducers
.addCase(fetchTaskListColumns.pending, state => {
state.loadingColumns = true;
state.error = null;
})
.addCase(fetchTaskListColumns.fulfilled, (state, action) => {
state.loadingColumns = false;
// Process standard columns
const standardColumns = action.payload.standard;
standardColumns.splice(1, 0, {
key: 'TASK',
name: 'Task',
index: 1,
pinned: true,
});
// Process custom columns
const customColumns = (action.payload as { custom: any[] }).custom.map((col: any) => ({
...col,
isCustom: true,
}));
// Merge columns
state.columns = [...standardColumns, ...customColumns];
state.customColumns = customColumns;
})
.addCase(fetchTaskListColumns.rejected, (state, action) => {
state.loadingColumns = false;
state.error = action.error.message || 'Failed to fetch task list columns';
})
.addCase(fetchCustomColumns.pending, state => {
state.loadingColumns = true;
state.error = null;
})
.addCase(fetchCustomColumns.fulfilled, (state, action) => {
state.loadingColumns = false;
state.customColumns = action.payload;
// Add custom columns to the columns array
const customColumnsForVisibility = action.payload;
state.columns = [...state.columns, ...customColumnsForVisibility];
})
.addCase(fetchCustomColumns.rejected, (state, action) => {
state.loadingColumns = false;
state.error = action.error.message || 'Failed to fetch custom columns';
})
.addCase(updateColumnVisibility.fulfilled, (state, action) => {
const column = state.columns.find(col => col.key === action.payload.key);
if (column) {
column.pinned = action.payload.pinned;
}
})
.addCase(updateColumnVisibility.rejected, (state, action) => {
state.error = action.payload as string;
});
},
});
@@ -913,6 +1081,12 @@ export const {
updateTaskAssignees,
createSubtask,
removeTemporarySubtask,
// Add column-related actions
toggleColumnVisibility,
addCustomColumn,
updateCustomColumn,
deleteCustomColumn,
syncColumnsWithFields,
} = taskManagementSlice.actions;
// Export the selectors
@@ -944,3 +1118,24 @@ export default taskManagementSlice.reducer;
// V3 API selectors - no processing needed, data is pre-processed by backend
export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups;
export const selectCurrentGroupingV3 = (state: RootState) => state.taskManagement.grouping;
// Column-related selectors
export const selectColumns = (state: RootState) => state.taskManagement.columns;
export const selectCustomColumns = (state: RootState) => state.taskManagement.customColumns;
export const selectLoadingColumns = (state: RootState) => state.taskManagement.loadingColumns;
// Helper selector to check if columns are in sync with local fields
export const selectColumnsInSync = (state: RootState) => {
const columns = state.taskManagement.columns;
const fields = state.taskManagementFields || [];
if (columns.length === 0 || fields.length === 0) return true;
return !fields.some(field => {
const backendColumn = columns.find(c => c.key === field.key);
if (backendColumn) {
return (backendColumn.pinned ?? false) !== field.visible;
}
return false;
});
};

View File

@@ -59,13 +59,21 @@ const taskListFieldsSlice = createSlice({
const field = state.find(f => f.key === action.payload);
if (field) {
field.visible = !field.visible;
// Save to localStorage immediately after toggle
saveFields(state);
}
},
setFields(state, action: PayloadAction<TaskListField[]>) {
return action.payload;
const newState = action.payload;
// Save to localStorage when fields are set
saveFields(newState);
return newState;
},
resetFields() {
return DEFAULT_FIELDS;
const defaultFields = DEFAULT_FIELDS;
// Save to localStorage when fields are reset
saveFields(defaultFields);
return defaultFields;
},
},
});