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:
@@ -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' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user