Enhance the TaskDrawerRecurringConfig component to include socket communication for handling recurring task changes. This update introduces the use of Redux for managing state updates related to recurring schedules, ensuring real-time synchronization of task configurations. Additionally, the code has been refactored for improved readability and maintainability.
1182 lines
37 KiB
TypeScript
1182 lines
37 KiB
TypeScript
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
|
|
import {
|
|
IGroupByOption,
|
|
ILabelsChangeResponse,
|
|
ITaskListColumn,
|
|
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 { ITaskAssignee, ITaskFormViewModel } from '@/types/tasks/task.types';
|
|
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.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 { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service';
|
|
import { ITaskLabel, ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
|
|
import { ITaskPhaseChangeResponse } from '@/types/tasks/task-phase-change-response';
|
|
import { produce } from 'immer';
|
|
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
|
|
import { SocketEvents } from '@/shared/socket-events';
|
|
import { ITaskRecurringScheduleData } from '@/types/tasks/task-recurring-schedule';
|
|
|
|
export enum IGroupBy {
|
|
STATUS = 'status',
|
|
PRIORITY = 'priority',
|
|
PHASE = 'phase',
|
|
MEMBERS = 'members',
|
|
}
|
|
|
|
export const GROUP_BY_STATUS_VALUE = IGroupBy.STATUS;
|
|
export const GROUP_BY_PRIORITY_VALUE = IGroupBy.PRIORITY;
|
|
export const GROUP_BY_PHASE_VALUE = IGroupBy.PHASE;
|
|
|
|
export const GROUP_BY_OPTIONS: IGroupByOption[] = [
|
|
{ label: 'Status', value: GROUP_BY_STATUS_VALUE },
|
|
{ label: 'Priority', value: GROUP_BY_PRIORITY_VALUE },
|
|
{ label: 'Phase', value: GROUP_BY_PHASE_VALUE },
|
|
];
|
|
|
|
const LOCALSTORAGE_GROUP_KEY = 'worklenz.tasklist.group_by';
|
|
|
|
export const getCurrentGroup = (): IGroupByOption => {
|
|
const key = localStorage.getItem(LOCALSTORAGE_GROUP_KEY);
|
|
if (key) {
|
|
const group = GROUP_BY_OPTIONS.find(option => option.value === key);
|
|
if (group) return group;
|
|
}
|
|
setCurrentGroup(GROUP_BY_STATUS_VALUE);
|
|
return GROUP_BY_OPTIONS[0];
|
|
};
|
|
|
|
export const setCurrentGroup = (groupBy: IGroupBy): void => {
|
|
localStorage.setItem(LOCALSTORAGE_GROUP_KEY, groupBy);
|
|
};
|
|
|
|
interface ITaskState {
|
|
search: string | null;
|
|
archived: boolean;
|
|
groupBy: IGroupBy;
|
|
isSubtasksInclude: boolean;
|
|
fields: ITaskListSortableColumn[];
|
|
tasks: IProjectTask[];
|
|
taskGroups: ITaskListGroup[];
|
|
loadingColumns: boolean;
|
|
columns: ITaskListColumn[];
|
|
loadingGroups: boolean;
|
|
error: string | null;
|
|
taskAssignees: ITaskListMemberFilter[];
|
|
loadingAssignees: boolean;
|
|
statuses: ITaskStatusViewModel[];
|
|
loadingLabels: boolean;
|
|
labels: ITaskLabelFilter[];
|
|
priorities: string[];
|
|
members: string[];
|
|
activeTimers: Record<string, number | null>;
|
|
convertToSubtaskDrawerOpen: boolean;
|
|
customColumns: ITaskListColumn[];
|
|
customColumnValues: Record<string, Record<string, any>>;
|
|
}
|
|
|
|
const initialState: ITaskState = {
|
|
search: null,
|
|
archived: false,
|
|
groupBy: getCurrentGroup().value as IGroupBy,
|
|
isSubtasksInclude: false,
|
|
fields: [],
|
|
tasks: [],
|
|
loadingColumns: false,
|
|
columns: [],
|
|
taskGroups: [],
|
|
loadingGroups: false,
|
|
error: null,
|
|
taskAssignees: [],
|
|
loadingAssignees: false,
|
|
statuses: [],
|
|
labels: [],
|
|
loadingLabels: false,
|
|
priorities: [],
|
|
members: [],
|
|
activeTimers: {},
|
|
convertToSubtaskDrawerOpen: false,
|
|
customColumns: [],
|
|
customColumnValues: {},
|
|
};
|
|
|
|
export const COLUMN_KEYS = {
|
|
KEY: 'KEY',
|
|
NAME: 'NAME',
|
|
DESCRIPTION: 'DESCRIPTION',
|
|
PROGRESS: 'PROGRESS',
|
|
ASSIGNEES: 'ASSIGNEES',
|
|
LABELS: 'LABELS',
|
|
STATUS: 'STATUS',
|
|
PRIORITY: 'PRIORITY',
|
|
TIME_TRACKING: 'TIME_TRACKING',
|
|
ESTIMATION: 'ESTIMATION',
|
|
START_DATE: 'START_DATE',
|
|
DUE_DATE: 'DUE_DATE',
|
|
DUE_TIME: 'DUE_TIME',
|
|
COMPLETED_DATE: 'COMPLETED_DATE',
|
|
CREATED_DATE: 'CREATED_DATE',
|
|
LAST_UPDATED: 'LAST_UPDATED',
|
|
REPORTER: 'REPORTER',
|
|
PHASE: 'PHASE',
|
|
} as const;
|
|
|
|
export const COLUMN_KEYS_LIST = Object.values(COLUMN_KEYS).map(key => ({
|
|
key,
|
|
show: true,
|
|
}));
|
|
|
|
export const fetchTaskGroups = createAsyncThunk(
|
|
'tasks/fetchTaskGroups',
|
|
async (projectId: string, { rejectWithValue, getState }) => {
|
|
try {
|
|
const state = getState() as { taskReducer: ITaskState };
|
|
const { taskReducer } = state;
|
|
|
|
const selectedMembers = taskReducer.taskAssignees
|
|
.filter(member => member.selected)
|
|
.map(member => member.id)
|
|
.join(' ');
|
|
|
|
const selectedLabels = taskReducer.labels
|
|
.filter(label => label.selected)
|
|
.map(label => label.id)
|
|
.join(' ');
|
|
|
|
const config: ITaskListConfigV2 = {
|
|
id: projectId,
|
|
archived: taskReducer.archived,
|
|
group: taskReducer.groupBy,
|
|
field: taskReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','),
|
|
order: '',
|
|
search: taskReducer.search || '',
|
|
statuses: '',
|
|
members: selectedMembers,
|
|
projects: '',
|
|
isSubtasksInclude: false,
|
|
labels: selectedLabels,
|
|
priorities: taskReducer.priorities.join(' '),
|
|
};
|
|
|
|
const response = await tasksApiService.getTaskList(config);
|
|
return response.body;
|
|
} catch (error) {
|
|
logger.error('Fetch Task Groups', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch task groups');
|
|
}
|
|
}
|
|
);
|
|
|
|
export const fetchSubTasks = createAsyncThunk(
|
|
'tasks/fetchSubTasks',
|
|
async (
|
|
{ taskId, projectId }: { taskId: string; projectId: string },
|
|
{ rejectWithValue, getState, dispatch }
|
|
) => {
|
|
const state = getState() as { taskReducer: ITaskState };
|
|
const { taskReducer } = state;
|
|
|
|
// Check if the task is already expanded
|
|
const task = taskReducer.taskGroups.flatMap(group => group.tasks).find(t => t.id === taskId);
|
|
|
|
if (task?.show_sub_tasks) {
|
|
// If already expanded, just return without fetching
|
|
return [];
|
|
}
|
|
|
|
// Request subtask progress data when expanding the task
|
|
// This will trigger the socket to emit TASK_PROGRESS_UPDATED events for all subtasks
|
|
try {
|
|
// Get access to the socket from the state
|
|
const socket = (getState() as any).socketReducer?.socket;
|
|
if (socket?.connected) {
|
|
// Request subtask count and progress information
|
|
socket.emit(SocketEvents.GET_TASK_SUBTASKS_COUNT.toString(), taskId);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error requesting subtask progress:', error);
|
|
// Non-critical error, continue with fetching subtasks
|
|
}
|
|
|
|
const selectedMembers = taskReducer.taskAssignees
|
|
.filter(member => member.selected)
|
|
.map(member => member.id)
|
|
.join(' ');
|
|
|
|
const selectedLabels = taskReducer.labels
|
|
.filter(label => label.selected)
|
|
.map(label => label.id)
|
|
.join(' ');
|
|
|
|
const config: ITaskListConfigV2 = {
|
|
id: projectId,
|
|
archived: taskReducer.archived,
|
|
group: taskReducer.groupBy,
|
|
field: taskReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','),
|
|
order: '',
|
|
search: taskReducer.search || '',
|
|
statuses: '',
|
|
members: selectedMembers,
|
|
projects: '',
|
|
isSubtasksInclude: false,
|
|
labels: selectedLabels,
|
|
priorities: taskReducer.priorities.join(' '),
|
|
parent_task: taskId,
|
|
};
|
|
try {
|
|
const response = await tasksApiService.getTaskList(config);
|
|
// Only expand if we actually fetched subtasks
|
|
if (response.body.length > 0) {
|
|
dispatch(toggleTaskRowExpansion(taskId));
|
|
}
|
|
return response.body;
|
|
} catch (error) {
|
|
logger.error('Fetch Sub Tasks', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch sub tasks');
|
|
}
|
|
}
|
|
);
|
|
|
|
export const fetchTaskListColumns = createAsyncThunk(
|
|
'tasks/fetTaskListColumns',
|
|
async (projectId: string, { dispatch }) => {
|
|
const [standardColumns, customColumns] = await Promise.all([
|
|
tasksApiService.fetchTaskListColumns(projectId),
|
|
dispatch(fetchCustomColumns(projectId))
|
|
]);
|
|
|
|
return {
|
|
standard: standardColumns.body,
|
|
custom: customColumns.payload
|
|
};
|
|
}
|
|
);
|
|
|
|
export const fetchTaskAssignees = createAsyncThunk(
|
|
'tasks/fetchTaskAssignees',
|
|
async (projectId: string, { rejectWithValue }) => {
|
|
try {
|
|
const response = await tasksApiService.fetchTaskAssignees(projectId);
|
|
return response.body;
|
|
} catch (error) {
|
|
logger.error('Fetch Task Assignees', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch task assignees');
|
|
}
|
|
}
|
|
);
|
|
|
|
export const fetchLabelsByProject = createAsyncThunk(
|
|
'taskLabel/fetchLabelsByProject',
|
|
async (projectId: string, { rejectWithValue }) => {
|
|
try {
|
|
const response = await labelsApiService.getPriorityByProject(projectId);
|
|
return response.body;
|
|
} catch (error) {
|
|
logger.error('Fetch Labels By Project', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch project labels');
|
|
}
|
|
}
|
|
);
|
|
|
|
export const fetchTask = createAsyncThunk(
|
|
'tasks/fetchTask',
|
|
async ({ taskId, projectId }: { taskId: string; projectId: string }, { rejectWithValue }) => {
|
|
try {
|
|
const response = await tasksApiService.getFormViewModel(taskId, projectId);
|
|
return response.body;
|
|
} catch (error) {
|
|
logger.error('Fetch Task', error);
|
|
if (error instanceof Error) {
|
|
return rejectWithValue(error.message);
|
|
}
|
|
return rejectWithValue('Failed to fetch task');
|
|
}
|
|
}
|
|
);
|
|
|
|
export const updateColumnVisibility = createAsyncThunk(
|
|
'tasks/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');
|
|
}
|
|
}
|
|
);
|
|
|
|
const getGroupIdByGroupedColumn = (task: IProjectTask): string | null => {
|
|
const groupBy = getCurrentGroup().value;
|
|
switch (groupBy) {
|
|
case GROUP_BY_STATUS_VALUE:
|
|
return task.status as string;
|
|
case GROUP_BY_PRIORITY_VALUE:
|
|
return task.priority as string;
|
|
case GROUP_BY_PHASE_VALUE:
|
|
return task.phase_id as string;
|
|
default:
|
|
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 addTaskToGroup = (
|
|
taskGroups: ITaskListGroup[],
|
|
task: IProjectTask,
|
|
groupId: string,
|
|
insert = false
|
|
): void => {
|
|
const group = taskGroups.find(g => g.id === groupId);
|
|
if (!group || !task.id) return;
|
|
|
|
if (task.parent_task_id) {
|
|
const parentTask = group.tasks.find(t => t.id === task.parent_task_id);
|
|
if (parentTask) {
|
|
parentTask.sub_tasks_count = (parentTask.sub_tasks_count || 0) + 1;
|
|
if (!parentTask.sub_tasks) parentTask.sub_tasks = [];
|
|
parentTask.sub_tasks.push({ ...task });
|
|
}
|
|
} else {
|
|
insert ? group.tasks.push(task) : group.tasks.unshift(task);
|
|
}
|
|
};
|
|
|
|
const updateTaskGroup = (taskGroups: ITaskListGroup[], task: IProjectTask, insert = true): void => {
|
|
if (!task.id) return;
|
|
|
|
const groupId = getGroupIdByGroupedColumn(task);
|
|
if (groupId) {
|
|
deleteTaskFromGroup(taskGroups, task, groupId);
|
|
addTaskToGroup(taskGroups, { ...task }, groupId, insert);
|
|
}
|
|
};
|
|
|
|
const findTaskInGroups = (
|
|
taskGroups: ITaskListGroup[],
|
|
taskId: string
|
|
): { task: IProjectTask; groupId: string; index: number } | null => {
|
|
for (const group of taskGroups) {
|
|
// Check main tasks
|
|
const taskIndex = group.tasks.findIndex(t => t.id === taskId);
|
|
if (taskIndex !== -1) {
|
|
return { task: group.tasks[taskIndex], groupId: group.id, index: taskIndex };
|
|
}
|
|
|
|
// Check subtasks
|
|
for (const task of group.tasks) {
|
|
if (task.sub_tasks) {
|
|
const subTaskIndex = task.sub_tasks.findIndex(subtask => subtask.id === taskId);
|
|
if (subTaskIndex !== -1) {
|
|
return { task: task.sub_tasks[subTaskIndex], groupId: group.id, index: subTaskIndex };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
export const fetchCustomColumns = createAsyncThunk(
|
|
'tasks/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');
|
|
}
|
|
}
|
|
);
|
|
|
|
const taskSlice = createSlice({
|
|
name: 'taskReducer',
|
|
initialState,
|
|
reducers: {
|
|
toggleArchived: state => {
|
|
state.archived = !state.archived;
|
|
},
|
|
|
|
setGroup: (state, action: PayloadAction<IGroupBy>) => {
|
|
state.groupBy = action.payload;
|
|
setCurrentGroup(action.payload);
|
|
},
|
|
|
|
setLabels: (state, action: PayloadAction<ITaskLabel[]>) => {
|
|
state.labels = action.payload;
|
|
},
|
|
|
|
setMembers: (state, action: PayloadAction<ITaskListMemberFilter[]>) => {
|
|
state.taskAssignees = action.payload;
|
|
},
|
|
|
|
setPriorities: (state, action: PayloadAction<string[]>) => {
|
|
state.priorities = action.payload;
|
|
},
|
|
|
|
setStatuses: (state, action: PayloadAction<ITaskStatusViewModel[]>) => {
|
|
state.statuses = action.payload;
|
|
},
|
|
|
|
setFields: (state, action: PayloadAction<ITaskListSortableColumn[]>) => {
|
|
state.fields = action.payload;
|
|
},
|
|
|
|
setSearch: (state, action: PayloadAction<string>) => {
|
|
state.search = action.payload;
|
|
},
|
|
|
|
setConvertToSubtaskDrawerOpen: (state, action: PayloadAction<boolean>) => {
|
|
state.convertToSubtaskDrawerOpen = action.payload;
|
|
},
|
|
|
|
addTask: (
|
|
state,
|
|
action: PayloadAction<{
|
|
task: IProjectTask;
|
|
groupId: string;
|
|
insert?: boolean;
|
|
}>
|
|
) => {
|
|
const { task, groupId, insert = false } = action.payload;
|
|
const group = state.taskGroups.find(g => g.id === groupId);
|
|
if (!group || !task.id) return;
|
|
|
|
// Handle subtask addition
|
|
if (task.parent_task_id) {
|
|
const parentTask = group.tasks.find(t => t.id === task.parent_task_id);
|
|
// if (parentTask) {
|
|
// if (!parentTask.sub_tasks) parentTask.sub_tasks = [];
|
|
// parentTask.sub_tasks.push({ ...task });
|
|
// parentTask.sub_tasks_count = parentTask.sub_tasks.length; // Update the sub_tasks_count based on the actual length
|
|
// Ensure sub-tasks are visible when adding a new one
|
|
// parentTask.show_sub_tasks = true;
|
|
// }
|
|
} else {
|
|
// Handle main task addition
|
|
if (insert) {
|
|
group.tasks.push(task);
|
|
} else {
|
|
group.tasks.unshift(task);
|
|
}
|
|
}
|
|
},
|
|
|
|
deleteTask: (
|
|
state,
|
|
action: PayloadAction<{
|
|
taskId: string;
|
|
index?: number | null;
|
|
}>
|
|
) => {
|
|
const { taskId, index } = action.payload;
|
|
|
|
for (const group of state.taskGroups) {
|
|
// Try to find task in subtasks first
|
|
let found = false;
|
|
for (const parentTask of group.tasks) {
|
|
if (parentTask.sub_tasks) {
|
|
const subTaskIndex = parentTask.sub_tasks.findIndex(st => st.id === taskId);
|
|
if (subTaskIndex !== -1) {
|
|
parentTask.sub_tasks.splice(subTaskIndex, 1);
|
|
parentTask.sub_tasks_count = Math.max((parentTask.sub_tasks_count || 0) - 1, 0);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (found) break;
|
|
|
|
// If not found in subtasks, try main tasks
|
|
const taskIndex = index ?? group.tasks.findIndex(t => t.id === taskId);
|
|
if (taskIndex !== -1) {
|
|
group.tasks.splice(taskIndex, 1);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateTaskName: (
|
|
state,
|
|
action: PayloadAction<{ id: string; parent_task: string; name: string }>
|
|
) => {
|
|
const { id, name } = action.payload;
|
|
|
|
for (const group of state.taskGroups) {
|
|
// Check main tasks
|
|
const task = group.tasks.find(task => task.id === id);
|
|
if (task) {
|
|
task.name = name;
|
|
break;
|
|
}
|
|
|
|
// Check subtasks
|
|
for (const task of group.tasks) {
|
|
if (task.sub_tasks) {
|
|
const subTask = task.sub_tasks.find(subtask => subtask.id === id);
|
|
if (subTask) {
|
|
subTask.name = name;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
updateTaskProgress: (
|
|
state,
|
|
action: PayloadAction<{
|
|
taskId: string;
|
|
progress: number;
|
|
totalTasksCount: number;
|
|
completedCount: number;
|
|
}>
|
|
) => {
|
|
const { taskId, progress, totalTasksCount, completedCount } = action.payload;
|
|
|
|
// Helper function to find and update a task at any nesting level
|
|
const findAndUpdateTask = (tasks: IProjectTask[]) => {
|
|
for (const task of tasks) {
|
|
if (task.id === taskId) {
|
|
task.complete_ratio = progress;
|
|
task.progress_value = progress;
|
|
task.total_tasks_count = totalTasksCount;
|
|
task.completed_count = completedCount;
|
|
return true;
|
|
}
|
|
|
|
// Check subtasks if they exist
|
|
if (task.sub_tasks && task.sub_tasks.length > 0) {
|
|
const found = findAndUpdateTask(task.sub_tasks);
|
|
if (found) return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Try to find and update the task in any task group
|
|
for (const group of state.taskGroups) {
|
|
const found = findAndUpdateTask(group.tasks);
|
|
if (found) break;
|
|
}
|
|
},
|
|
|
|
updateTaskAssignees: (
|
|
state,
|
|
action: PayloadAction<{
|
|
groupId: string;
|
|
taskId: string;
|
|
assignees: ITeamMemberViewModel[];
|
|
}>
|
|
) => {
|
|
const { groupId, taskId, assignees } = action.payload;
|
|
const group = state.taskGroups.find(group => group.id === groupId);
|
|
if (!group) return;
|
|
|
|
// Try to find the task in main tasks first
|
|
const mainTask = group.tasks.find(task => task.id === taskId);
|
|
if (mainTask) {
|
|
mainTask.assignees = assignees as ITaskAssignee[];
|
|
return;
|
|
}
|
|
|
|
// If not found in main tasks, look for it in subtasks
|
|
for (const parentTask of group.tasks) {
|
|
if (parentTask.sub_tasks) {
|
|
const subTask = parentTask.sub_tasks.find(st => st.id === taskId);
|
|
if (subTask) {
|
|
subTask.assignees = assignees as ITaskAssignee[];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
updateTaskLabel: (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 || [];
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
|
|
const { id, status_id, color_code, color_code_dark, complete_ratio, statusCategory } = action.payload;
|
|
|
|
// Find the task in any group
|
|
const taskInfo = findTaskInGroups(state.taskGroups, id);
|
|
if (!taskInfo || !status_id) return;
|
|
|
|
const { task, groupId } = taskInfo;
|
|
|
|
// Update the task properties
|
|
task.status_color = color_code;
|
|
task.status_color_dark = color_code_dark;
|
|
task.complete_ratio = +complete_ratio;
|
|
task.status = status_id;
|
|
task.status_category = statusCategory;
|
|
|
|
// If grouped by status and not a subtask, move the task to the new status group
|
|
if (state.groupBy === GROUP_BY_STATUS_VALUE && !task.is_sub_task && groupId !== status_id) {
|
|
// Remove from current group
|
|
deleteTaskFromGroup(state.taskGroups, task, groupId);
|
|
|
|
// Add to new status group
|
|
addTaskToGroup(state.taskGroups, task, status_id, false);
|
|
}
|
|
},
|
|
|
|
updateTaskEndDate: (
|
|
state,
|
|
action: PayloadAction<{
|
|
task: IProjectTask;
|
|
}>
|
|
) => {
|
|
const { task } = action.payload;
|
|
|
|
for (const group of state.taskGroups) {
|
|
const existingTask =
|
|
group.tasks.find(t => t.id === task.id) ||
|
|
group.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === task.id);
|
|
if (existingTask) {
|
|
existingTask.end_date = task.end_date;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateTaskStartDate: (
|
|
state,
|
|
action: PayloadAction<{
|
|
task: IProjectTask;
|
|
}>
|
|
) => {
|
|
const { task } = action.payload;
|
|
|
|
for (const group of state.taskGroups) {
|
|
const existingTask =
|
|
group.tasks.find(t => t.id === task.id) ||
|
|
group.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === task.id);
|
|
if (existingTask) {
|
|
existingTask.start_date = task.start_date;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateTaskEstimation: (
|
|
state,
|
|
action: PayloadAction<{
|
|
task: IProjectTask;
|
|
}>
|
|
) => {
|
|
const { task } = action.payload;
|
|
|
|
for (const group of state.taskGroups) {
|
|
const existingTask =
|
|
group.tasks.find(t => t.id === task.id) ||
|
|
group.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === task.id);
|
|
if (existingTask) {
|
|
existingTask.total_time_string = task.total_time_string;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateTaskPhase: (state, action: PayloadAction<ITaskPhaseChangeResponse>) => {
|
|
const { id: phase_id, task_id, color_code } = action.payload;
|
|
|
|
if (!task_id || !phase_id) return;
|
|
|
|
const taskInfo = findTaskInGroups(state.taskGroups, task_id);
|
|
if (!taskInfo) return;
|
|
|
|
const { task, groupId } = taskInfo;
|
|
|
|
task.phase_id = phase_id;
|
|
task.phase_color = color_code;
|
|
|
|
if (state.groupBy === GROUP_BY_PHASE_VALUE && !task.is_sub_task && groupId !== phase_id) {
|
|
deleteTaskFromGroup(state.taskGroups, task, groupId);
|
|
addTaskToGroup(state.taskGroups, task, phase_id, false);
|
|
}
|
|
},
|
|
|
|
updateTaskGroupColor: (
|
|
state,
|
|
action: PayloadAction<{ groupId: string; colorCode: string }>
|
|
) => {
|
|
const { colorCode, groupId } = action.payload;
|
|
|
|
if (groupId) {
|
|
const group = state.taskGroups.find(g => g.id === groupId);
|
|
|
|
if (group) {
|
|
group.color_code = colorCode;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateTaskStatusColor: (state, action: PayloadAction<{ taskId: string; color: string }>) => {
|
|
const { taskId, color } = action.payload;
|
|
const task =
|
|
state.tasks.find(t => t.id === taskId) ||
|
|
state.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === taskId);
|
|
if (task) {
|
|
task.status_color = color;
|
|
}
|
|
},
|
|
|
|
toggleColumnVisibility: (state, action: PayloadAction<string>) => {
|
|
const column = state.columns.find(col => col.key === action.payload);
|
|
if (column) {
|
|
column.pinned = !column.pinned;
|
|
}
|
|
},
|
|
|
|
updateTaskTimeTracking: (
|
|
state,
|
|
action: PayloadAction<{
|
|
taskId: string;
|
|
timeTracking: number | null;
|
|
}>
|
|
) => {
|
|
const { taskId, timeTracking } = action.payload;
|
|
state.activeTimers[taskId] = timeTracking;
|
|
},
|
|
|
|
updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
|
|
const { id, priority_id, color_code, color_code_dark } = action.payload;
|
|
|
|
// Find the task in any group
|
|
const taskInfo = findTaskInGroups(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 === GROUP_BY_PRIORITY_VALUE &&
|
|
!task.is_sub_task &&
|
|
groupId !== priority_id
|
|
) {
|
|
// Remove from current group
|
|
deleteTaskFromGroup(state.taskGroups, task, groupId);
|
|
|
|
// Add to new priority group
|
|
addTaskToGroup(state.taskGroups, task, priority_id, false);
|
|
}
|
|
},
|
|
|
|
updateTaskDescription: (
|
|
state,
|
|
action: PayloadAction<{
|
|
id: string;
|
|
parent_task: string;
|
|
description: string;
|
|
}>
|
|
) => {
|
|
const { id: taskId, description, parent_task } = action.payload;
|
|
for (const group of state.taskGroups) {
|
|
const existingTask =
|
|
group.tasks.find(t => t.id === taskId) ||
|
|
group.tasks.flatMap(t => t.sub_tasks || []).find(subtask => subtask.id === taskId);
|
|
if (existingTask) {
|
|
existingTask.description = description;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
toggleTaskRowExpansion: (state, action: PayloadAction<string>) => {
|
|
const taskId = action.payload;
|
|
for (const group of state.taskGroups) {
|
|
const task = group.tasks.find(t => t.id === taskId);
|
|
if (task) {
|
|
task.show_sub_tasks = !task.show_sub_tasks;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
resetTaskListData: state => {
|
|
return {
|
|
...initialState,
|
|
groupBy: state.groupBy, // Preserve the current grouping
|
|
};
|
|
},
|
|
|
|
reorderTasks: (
|
|
state,
|
|
action: PayloadAction<{
|
|
activeGroupId: string;
|
|
overGroupId: string;
|
|
fromIndex: number;
|
|
toIndex: number;
|
|
task: IProjectTask;
|
|
updatedSourceTasks: IProjectTask[];
|
|
updatedTargetTasks: IProjectTask[];
|
|
}>
|
|
) => {
|
|
return produce(state, draft => {
|
|
const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } =
|
|
action.payload;
|
|
|
|
const sourceGroup = draft.taskGroups.find(g => g.id === activeGroupId);
|
|
const targetGroup = draft.taskGroups.find(g => g.id === overGroupId);
|
|
|
|
if (!sourceGroup || !targetGroup) return;
|
|
|
|
// Simply replace the arrays with the updated ones
|
|
sourceGroup.tasks = updatedSourceTasks;
|
|
|
|
// Only update target if it's different from source
|
|
if (activeGroupId !== overGroupId) {
|
|
targetGroup.tasks = updatedTargetTasks;
|
|
}
|
|
});
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
updateSubTasks: (state, action: PayloadAction<IProjectTask>) => {
|
|
const { parent_task_id } = action.payload;
|
|
for (const group of state.taskGroups) {
|
|
const parentTask = group.tasks.find(t => t.id === parent_task_id);
|
|
if (parentTask) {
|
|
if (!parentTask.sub_tasks) {
|
|
parentTask.sub_tasks = [];
|
|
}
|
|
parentTask.sub_tasks.push({ ...action.payload });
|
|
// Always update sub_tasks_count based on actual subtasks array length
|
|
parentTask.sub_tasks_count = (parentTask.sub_tasks_count || 0) + 1;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateCustomColumnValue: (
|
|
state,
|
|
action: PayloadAction<{
|
|
taskId: string;
|
|
columnKey: string;
|
|
value: string;
|
|
}>
|
|
) => {
|
|
const { taskId, columnKey, value } = action.payload;
|
|
|
|
// Update in task groups
|
|
for (const group of state.taskGroups) {
|
|
// Check in main tasks
|
|
const taskIndex = group.tasks.findIndex(t => t.id === taskId);
|
|
if (taskIndex !== -1) {
|
|
if (!group.tasks[taskIndex].custom_column_values) {
|
|
group.tasks[taskIndex].custom_column_values = {};
|
|
}
|
|
group.tasks[taskIndex].custom_column_values[columnKey] = value;
|
|
break;
|
|
}
|
|
|
|
// Check in subtasks
|
|
for (const parentTask of group.tasks) {
|
|
if (parentTask.sub_tasks) {
|
|
const subtaskIndex = parentTask.sub_tasks.findIndex(st => st.id === taskId);
|
|
if (subtaskIndex !== -1) {
|
|
if (!parentTask.sub_tasks[subtaskIndex].custom_column_values) {
|
|
parentTask.sub_tasks[subtaskIndex].custom_column_values = {};
|
|
}
|
|
parentTask.sub_tasks[subtaskIndex].custom_column_values[columnKey] = value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also update in the customColumnValues state if needed
|
|
if (!state.customColumnValues[taskId]) {
|
|
state.customColumnValues[taskId] = {};
|
|
}
|
|
state.customColumnValues[taskId][columnKey] = value;
|
|
},
|
|
|
|
updateCustomColumnPinned: (state, action: PayloadAction<{ columnId: string; isVisible: boolean }>) => {
|
|
const { columnId, isVisible } = action.payload;
|
|
const customColumn = state.customColumns.find(col => col.id === columnId);
|
|
const column = state.columns.find(col => col.id === columnId);
|
|
|
|
if (customColumn) {
|
|
customColumn.pinned = isVisible;
|
|
}
|
|
|
|
if (column) {
|
|
column.pinned = isVisible;
|
|
}
|
|
},
|
|
|
|
updateRecurringChange: (state, action: PayloadAction<ITaskRecurringScheduleData>) => {
|
|
const {id, schedule_type, task_id} = action.payload;
|
|
const taskInfo = findTaskInGroups(state.taskGroups, task_id as string);
|
|
if (!taskInfo) return;
|
|
|
|
const { task } = taskInfo;
|
|
task.schedule_id = id;
|
|
}
|
|
},
|
|
|
|
extraReducers: builder => {
|
|
builder
|
|
.addCase(fetchTaskGroups.pending, state => {
|
|
state.loadingGroups = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchTaskGroups.fulfilled, (state, action) => {
|
|
state.loadingGroups = false;
|
|
state.taskGroups = action.payload;
|
|
})
|
|
.addCase(fetchTaskGroups.rejected, (state, action) => {
|
|
state.loadingGroups = false;
|
|
state.error = action.error.message || 'Failed to fetch task groups';
|
|
})
|
|
.addCase(fetchSubTasks.pending, state => {
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchSubTasks.fulfilled, (state, action: PayloadAction<IProjectTask[]>) => {
|
|
if (action.payload.length > 0) {
|
|
const taskId = action.payload[0].parent_task_id;
|
|
if (taskId) {
|
|
for (const group of state.taskGroups) {
|
|
const task = group.tasks.find(t => t.id === taskId);
|
|
if (task) {
|
|
task.sub_tasks = action.payload;
|
|
task.show_sub_tasks = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.addCase(fetchSubTasks.rejected, (state, action) => {
|
|
state.error = action.error.message || 'Failed to fetch sub tasks';
|
|
})
|
|
.addCase(fetchTaskAssignees.pending, state => {
|
|
state.loadingAssignees = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchTaskAssignees.fulfilled, (state, action) => {
|
|
state.loadingAssignees = false;
|
|
state.taskAssignees = action.payload;
|
|
})
|
|
.addCase(fetchTaskAssignees.rejected, (state, action) => {
|
|
state.loadingAssignees = false;
|
|
state.error = action.error.message || 'Failed to fetch task assignees';
|
|
})
|
|
.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';
|
|
})
|
|
// Fetch Labels By Project
|
|
.addCase(fetchLabelsByProject.pending, state => {
|
|
state.loadingLabels = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchLabelsByProject.fulfilled, (state, action: PayloadAction<ITaskLabel[]>) => {
|
|
const newLabels = action.payload.map(label => ({ ...label, selected: false }));
|
|
state.labels = newLabels;
|
|
state.loadingLabels = false;
|
|
})
|
|
.addCase(fetchLabelsByProject.rejected, (state, action) => {
|
|
state.loadingLabels = false;
|
|
state.error = action.payload as string;
|
|
})
|
|
.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;
|
|
})
|
|
.addCase(updateColumnVisibility.pending, state => {
|
|
state.loadingColumns = true;
|
|
state.error = null;
|
|
})
|
|
.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';
|
|
});
|
|
},
|
|
});
|
|
|
|
export const {
|
|
setGroup,
|
|
addTask,
|
|
deleteTask,
|
|
updateTaskName,
|
|
updateTaskProgress,
|
|
updateTaskAssignees,
|
|
updateTaskLabel,
|
|
toggleArchived,
|
|
setMembers,
|
|
setLabels,
|
|
setPriorities,
|
|
setStatuses,
|
|
setFields,
|
|
setSearch,
|
|
toggleColumnVisibility,
|
|
updateTaskStatus,
|
|
updateTaskPhase,
|
|
updateTaskPriority,
|
|
updateTaskEndDate,
|
|
updateTaskStartDate,
|
|
updateTaskEstimation,
|
|
updateTaskTimeTracking,
|
|
toggleTaskRowExpansion,
|
|
resetTaskListData,
|
|
updateTaskStatusColor,
|
|
updateTaskGroupColor,
|
|
setConvertToSubtaskDrawerOpen,
|
|
reorderTasks,
|
|
updateTaskDescription,
|
|
addCustomColumn,
|
|
updateCustomColumn,
|
|
deleteCustomColumn,
|
|
updateSubTasks,
|
|
updateCustomColumnValue,
|
|
updateCustomColumnPinned,
|
|
updateRecurringChange
|
|
} = taskSlice.actions;
|
|
|
|
export default taskSlice.reducer;
|