init
This commit is contained in:
872
worklenz-frontend/src/features/board/board-slice.ts
Normal file
872
worklenz-frontend/src/features/board/board-slice.ts
Normal file
@@ -0,0 +1,872 @@
|
||||
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 { IProjectTask, ITaskAssignee } from '@/types/project/projectTasksViewModel.types';
|
||||
import { ITaskStatusViewModel } from '@/types/tasks/taskStatusGetResponse.types';
|
||||
import { ITaskAssigneesUpdateResponse } from '@/types/tasks/task-assignee-update-response';
|
||||
import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
|
||||
import { ITaskLabel } from '@/types/label.type';
|
||||
import { ITeamMemberViewModel } from '../taskAttributes/taskMemberSlice';
|
||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||
import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.types';
|
||||
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
||||
|
||||
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_BOARD_GROUP_KEY = 'worklenz.board.group_by';
|
||||
|
||||
export const getCurrentGroupBoard = (): IGroupByOption => {
|
||||
const key = localStorage.getItem(LOCALSTORAGE_BOARD_GROUP_KEY);
|
||||
if (key) {
|
||||
const group = GROUP_BY_OPTIONS.find(option => option.value === key);
|
||||
if (group) return group;
|
||||
}
|
||||
setCurrentBoardGroup(GROUP_BY_STATUS_VALUE);
|
||||
return GROUP_BY_OPTIONS[0];
|
||||
};
|
||||
|
||||
export const setCurrentBoardGroup = (groupBy: IGroupBy): void => {
|
||||
localStorage.setItem(LOCALSTORAGE_BOARD_GROUP_KEY, groupBy);
|
||||
};
|
||||
|
||||
interface BoardState {
|
||||
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[];
|
||||
editableSectionId: string | null;
|
||||
}
|
||||
|
||||
const initialState: BoardState = {
|
||||
search: null,
|
||||
archived: false,
|
||||
groupBy: getCurrentGroupBoard().value as IGroupBy,
|
||||
isSubtasksInclude: false,
|
||||
fields: [],
|
||||
tasks: [],
|
||||
loadingColumns: false,
|
||||
columns: [],
|
||||
taskGroups: [],
|
||||
loadingGroups: false,
|
||||
error: null,
|
||||
taskAssignees: [],
|
||||
loadingAssignees: false,
|
||||
statuses: [],
|
||||
labels: [],
|
||||
loadingLabels: false,
|
||||
priorities: [],
|
||||
members: [],
|
||||
editableSectionId: 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);
|
||||
}
|
||||
};
|
||||
|
||||
// Async thunk for fetching members data
|
||||
export const fetchTaskData = createAsyncThunk('board/fetchTaskData', async (endpoint: string) => {
|
||||
const response = await fetch(endpoint);
|
||||
if (!response.ok) throw new Error(`Response error: ${response.status}`);
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
export const fetchBoardTaskGroups = createAsyncThunk(
|
||||
'board/fetchBoardTaskGroups',
|
||||
async (projectId: string, { rejectWithValue, getState }) => {
|
||||
try {
|
||||
const state = getState() as { boardReducer: BoardState };
|
||||
const { boardReducer } = state;
|
||||
|
||||
const selectedMembers = boardReducer.taskAssignees
|
||||
.filter(member => member.selected)
|
||||
.map(member => member.id)
|
||||
.join(' ');
|
||||
|
||||
const selectedLabels = boardReducer.labels
|
||||
.filter(label => label.selected)
|
||||
.map(label => label.id)
|
||||
.join(' ');
|
||||
|
||||
const config: ITaskListConfigV2 = {
|
||||
id: projectId,
|
||||
archived: boardReducer.archived,
|
||||
group: boardReducer.groupBy,
|
||||
field: boardReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','),
|
||||
order: '',
|
||||
search: boardReducer.search || '',
|
||||
statuses: '',
|
||||
members: selectedMembers,
|
||||
projects: '',
|
||||
isSubtasksInclude: boardReducer.isSubtasksInclude,
|
||||
labels: selectedLabels,
|
||||
priorities: boardReducer.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 fetchBoardSubTasks = createAsyncThunk(
|
||||
'board/fetchBoardSubTasks',
|
||||
async (
|
||||
{ taskId, projectId }: { taskId: string; projectId: string },
|
||||
{ rejectWithValue, getState }
|
||||
) => {
|
||||
try {
|
||||
const state = getState() as { boardReducer: BoardState };
|
||||
const { boardReducer } = state;
|
||||
|
||||
// Check if the task is already expanded
|
||||
const task = boardReducer.taskGroups.flatMap(group => group.tasks).find(t => t.id === taskId);
|
||||
|
||||
if (task?.show_sub_tasks) {
|
||||
// If already expanded, just return without fetching
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedMembers = boardReducer.taskAssignees
|
||||
.filter(member => member.selected)
|
||||
.map(member => member.id)
|
||||
.join(' ');
|
||||
|
||||
const selectedLabels = boardReducer.labels
|
||||
.filter(label => label.selected)
|
||||
.map(label => label.id)
|
||||
.join(' ');
|
||||
|
||||
const config: ITaskListConfigV2 = {
|
||||
id: projectId,
|
||||
archived: boardReducer.archived,
|
||||
group: boardReducer.groupBy,
|
||||
field: boardReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','),
|
||||
order: '',
|
||||
search: boardReducer.search || '',
|
||||
statuses: '',
|
||||
members: selectedMembers,
|
||||
projects: '',
|
||||
isSubtasksInclude: false,
|
||||
labels: selectedLabels,
|
||||
priorities: boardReducer.priorities.join(' '),
|
||||
parent_task: taskId,
|
||||
};
|
||||
|
||||
const response = await tasksApiService.getTaskList(config);
|
||||
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');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Helper functions for common operations
|
||||
const findTaskInAllGroups = (
|
||||
taskGroups: ITaskListGroup[],
|
||||
taskId: string
|
||||
): { task: IProjectTask; group: ITaskListGroup; groupId: string } | null => {
|
||||
for (const group of taskGroups) {
|
||||
const task = group.tasks.find(t => t.id === taskId);
|
||||
if (task) return { task, group, groupId: group.id };
|
||||
|
||||
// Check in subtasks
|
||||
for (const parentTask of group.tasks) {
|
||||
if (!parentTask.sub_tasks) continue;
|
||||
const subtask = parentTask.sub_tasks.find(st => st.id === taskId);
|
||||
if (subtask) return { task: subtask, group, groupId: group.id };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const findParentTaskInAllGroups = (
|
||||
taskGroups: ITaskListGroup[],
|
||||
parentTaskId: string
|
||||
): { task: IProjectTask; group: ITaskListGroup } | null => {
|
||||
for (const group of taskGroups) {
|
||||
const task = group.tasks.find(t => t.id === parentTaskId);
|
||||
if (task) return { task, group };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getTaskListConfig = (
|
||||
state: BoardState,
|
||||
projectId: string,
|
||||
parentTaskId?: string
|
||||
): ITaskListConfigV2 => {
|
||||
const selectedMembers = state.taskAssignees
|
||||
.filter(member => member.selected)
|
||||
.map(member => member.id)
|
||||
.join(' ');
|
||||
|
||||
const selectedLabels = state.labels
|
||||
.filter(label => label.selected)
|
||||
.map(label => label.id)
|
||||
.join(' ');
|
||||
|
||||
return {
|
||||
id: projectId,
|
||||
archived: state.archived,
|
||||
group: state.groupBy,
|
||||
field: state.fields.map(field => `${field.key} ${field.sort_order}`).join(','),
|
||||
order: '',
|
||||
search: state.search || '',
|
||||
statuses: '',
|
||||
members: selectedMembers,
|
||||
projects: '',
|
||||
isSubtasksInclude: state.isSubtasksInclude,
|
||||
labels: selectedLabels,
|
||||
priorities: state.priorities.join(' '),
|
||||
parent_task: parentTaskId,
|
||||
};
|
||||
};
|
||||
|
||||
const boardSlice = createSlice({
|
||||
name: 'boardReducer',
|
||||
initialState,
|
||||
reducers: {
|
||||
setBoardGroupBy: (state, action: PayloadAction<BoardState['groupBy']>) => {
|
||||
state.groupBy = action.payload;
|
||||
setCurrentBoardGroup(action.payload);
|
||||
},
|
||||
|
||||
addBoardSectionCard: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
name: string;
|
||||
colorCode: string;
|
||||
colorCodeDark: string;
|
||||
}>
|
||||
) => {
|
||||
const newSection = {
|
||||
id: action.payload.id,
|
||||
name: action.payload.name,
|
||||
color_code: action.payload.colorCode,
|
||||
color_code_dark: action.payload.colorCodeDark,
|
||||
progress: { todo: 0, doing: 0, done: 0 },
|
||||
tasks: [],
|
||||
};
|
||||
state.taskGroups.push(newSection as ITaskListGroup);
|
||||
state.editableSectionId = newSection.id;
|
||||
},
|
||||
|
||||
setEditableSection: (state, action: PayloadAction<string | null>) => {
|
||||
state.editableSectionId = action.payload;
|
||||
},
|
||||
|
||||
addTaskCardToTheTop: (
|
||||
state,
|
||||
action: PayloadAction<{ sectionId: string; task: IProjectTask }>
|
||||
) => {
|
||||
const section = state.taskGroups.find(sec => sec.id === action.payload.sectionId);
|
||||
if (section) {
|
||||
section.tasks.unshift(action.payload.task);
|
||||
}
|
||||
},
|
||||
|
||||
addTaskCardToTheBottom: (
|
||||
state,
|
||||
action: PayloadAction<{ sectionId: string; task: IProjectTask }>
|
||||
) => {
|
||||
const section = state.taskGroups.find(sec => sec.id === action.payload.sectionId);
|
||||
if (section) {
|
||||
section.tasks.push(action.payload.task);
|
||||
}
|
||||
},
|
||||
|
||||
addSubtask: (
|
||||
state,
|
||||
action: PayloadAction<{ sectionId: string; taskId: string; subtask: IProjectTask }>
|
||||
) => {
|
||||
const section = state.taskGroups.find(sec => sec.id === action.payload.sectionId);
|
||||
if (section) {
|
||||
const task = section.tasks.find(task => task.id === action.payload.taskId);
|
||||
|
||||
if (task) {
|
||||
if (!task.sub_tasks) {
|
||||
task.sub_tasks = [];
|
||||
}
|
||||
task.sub_tasks.push(action.payload.subtask);
|
||||
task.sub_tasks_count = task.sub_tasks.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
deleteBoardTask: (state, action: PayloadAction<{ sectionId: string; taskId: string }>) => {
|
||||
const { sectionId, taskId } = action.payload;
|
||||
|
||||
if (sectionId) {
|
||||
const section = state.taskGroups.find(sec => sec.id === sectionId);
|
||||
if (section) {
|
||||
// Check if task is in the main task list
|
||||
const taskIndex = section.tasks.findIndex(task => task.id === taskId);
|
||||
if (taskIndex !== -1) {
|
||||
section.tasks.splice(taskIndex, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if task is in subtasks
|
||||
for (const parentTask of section.tasks) {
|
||||
if (!parentTask.sub_tasks) continue;
|
||||
|
||||
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(0, (parentTask.sub_tasks_count || 1) - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If section not found or task not in section, search all groups
|
||||
for (const group of state.taskGroups) {
|
||||
// Check main tasks
|
||||
const taskIndex = group.tasks.findIndex(task => task.id === taskId);
|
||||
if (taskIndex !== -1) {
|
||||
group.tasks.splice(taskIndex, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check subtasks
|
||||
for (const parentTask of group.tasks) {
|
||||
if (!parentTask.sub_tasks) continue;
|
||||
|
||||
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(0, (parentTask.sub_tasks_count || 1) - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
deleteSection: (state, action: PayloadAction<{ sectionId: string }>) => {
|
||||
state.taskGroups = state.taskGroups.filter(
|
||||
section => section.id !== action.payload.sectionId
|
||||
);
|
||||
|
||||
if (state.editableSectionId === action.payload.sectionId) {
|
||||
state.editableSectionId = null;
|
||||
}
|
||||
},
|
||||
|
||||
updateBoardTaskAssignee: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
body: ITaskAssigneesUpdateResponse;
|
||||
sectionId: string;
|
||||
taskId: string;
|
||||
}>
|
||||
) => {
|
||||
const { body, sectionId, taskId } = action.payload;
|
||||
const section = state.taskGroups.find(sec => sec.id === sectionId);
|
||||
if (section) {
|
||||
const task = section.tasks.find(task => task.id === taskId);
|
||||
if (task) {
|
||||
task.assignees = body.assignees;
|
||||
task.names = body.names;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reorderTaskGroups: (state, action: PayloadAction<ITaskListGroup[]>) => {
|
||||
state.taskGroups = action.payload;
|
||||
},
|
||||
|
||||
moveTaskBetweenGroups: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
taskId: string;
|
||||
sourceGroupId: string;
|
||||
targetGroupId: string;
|
||||
targetIndex: number;
|
||||
}>
|
||||
) => {
|
||||
const { taskId, sourceGroupId, targetGroupId, targetIndex } = action.payload;
|
||||
|
||||
// Find source and target groups
|
||||
const sourceGroup = state.taskGroups.find(group => group.id === sourceGroupId);
|
||||
const targetGroup = state.taskGroups.find(group => group.id === targetGroupId);
|
||||
|
||||
if (!sourceGroup || !targetGroup) return;
|
||||
|
||||
// Find the task to move
|
||||
const taskIndex = sourceGroup.tasks.findIndex(task => task.id === taskId);
|
||||
if (taskIndex === -1) return;
|
||||
|
||||
// Get the task and remove it from source
|
||||
const task = { ...sourceGroup.tasks[taskIndex], status_id: targetGroupId };
|
||||
sourceGroup.tasks.splice(taskIndex, 1);
|
||||
|
||||
// Insert task at the target position
|
||||
if (targetIndex >= 0 && targetIndex <= targetGroup.tasks.length) {
|
||||
targetGroup.tasks.splice(targetIndex, 0, task);
|
||||
} else {
|
||||
// If target index is invalid, append to the end
|
||||
targetGroup.tasks.push(task);
|
||||
}
|
||||
},
|
||||
|
||||
resetBoardData: state => {
|
||||
state.taskGroups = [];
|
||||
state.columns = [];
|
||||
state.loadingGroups = false;
|
||||
state.loadingColumns = false;
|
||||
state.error = null;
|
||||
},
|
||||
|
||||
setBoardLabels: (state, action: PayloadAction<ITaskLabelFilter[]>) => {
|
||||
state.labels = action.payload;
|
||||
},
|
||||
|
||||
setBoardMembers: (state, action: PayloadAction<ITaskListMemberFilter[]>) => {
|
||||
state.taskAssignees = action.payload;
|
||||
},
|
||||
|
||||
setBoardPriorities: (state, action: PayloadAction<string[]>) => {
|
||||
state.priorities = action.payload;
|
||||
},
|
||||
|
||||
setBoardStatuses: (state, action: PayloadAction<ITaskStatusViewModel[]>) => {
|
||||
state.statuses = action.payload;
|
||||
},
|
||||
|
||||
setBoardSearch: (state, action: PayloadAction<string | null>) => {
|
||||
state.search = action.payload;
|
||||
},
|
||||
|
||||
toggleSubtasksInclude: state => {
|
||||
state.isSubtasksInclude = !state.isSubtasksInclude;
|
||||
},
|
||||
|
||||
setBoardGroupName: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
groupId: string;
|
||||
name: string;
|
||||
colorCode: string;
|
||||
colorCodeDark: string;
|
||||
categoryId: string;
|
||||
}>
|
||||
) => {
|
||||
const { groupId, name, colorCode, colorCodeDark, categoryId } = action.payload;
|
||||
const group = state.taskGroups.find(group => group.id === groupId);
|
||||
if (group) {
|
||||
group.name = name;
|
||||
group.color_code = colorCode;
|
||||
group.color_code_dark = colorCodeDark;
|
||||
group.category_id = categoryId;
|
||||
}
|
||||
},
|
||||
|
||||
updateTaskAssignees: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
groupId: string;
|
||||
taskId: string;
|
||||
assignees: ITeamMemberViewModel[];
|
||||
names: ITeamMemberViewModel[];
|
||||
}>
|
||||
) => {
|
||||
const { groupId, taskId, assignees, names } = action.payload;
|
||||
|
||||
// Find the task in the specified group
|
||||
const group = state.taskGroups.find(group => group.id === groupId);
|
||||
if (!group) return;
|
||||
|
||||
// Try to find the task directly in the group
|
||||
const task = group.tasks.find(task => task.id === taskId);
|
||||
if (task) {
|
||||
task.assignees = assignees as ITaskAssignee[];
|
||||
task.names = names as InlineMember[];
|
||||
return;
|
||||
}
|
||||
|
||||
// If not found, look in subtasks
|
||||
for (const parentTask of group.tasks) {
|
||||
if (!parentTask.sub_tasks) continue;
|
||||
|
||||
const subtask = parentTask.sub_tasks.find(subtask => subtask.id === taskId);
|
||||
if (subtask) {
|
||||
subtask.assignees = assignees as ITaskAssignee[];
|
||||
subtask.names = names as InlineMember[];
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateTaskName: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
task: IProjectTask;
|
||||
}>
|
||||
) => {
|
||||
const { task } = action.payload;
|
||||
|
||||
// Find the task and update it
|
||||
const result = findTaskInAllGroups(state.taskGroups, task.id || '');
|
||||
if (result) {
|
||||
result.task.name = task.name;
|
||||
}
|
||||
},
|
||||
|
||||
updateTaskEndDate: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
task: IProjectTask;
|
||||
}>
|
||||
) => {
|
||||
const { task } = action.payload;
|
||||
|
||||
// Find the task and update it
|
||||
const result = findTaskInAllGroups(state.taskGroups, task.id || '');
|
||||
if (result) {
|
||||
result.task.end_date = task.end_date;
|
||||
}
|
||||
},
|
||||
|
||||
updateSubtask: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
sectionId: string;
|
||||
subtask: IProjectTask;
|
||||
mode: 'add' | 'delete';
|
||||
}>
|
||||
) => {
|
||||
const { sectionId, subtask, mode } = action.payload;
|
||||
const parentTaskId = subtask?.parent_task_id || null;
|
||||
|
||||
if (!parentTaskId) return;
|
||||
|
||||
// Function to update a task with a new subtask
|
||||
const updateTaskWithSubtask = (task: IProjectTask): boolean => {
|
||||
if (!task) return false;
|
||||
|
||||
// Initialize sub_tasks array if it doesn't exist
|
||||
if (!task.sub_tasks) {
|
||||
task.sub_tasks = [];
|
||||
}
|
||||
|
||||
if (mode === 'add') {
|
||||
// Increment subtask count
|
||||
task.sub_tasks_count = (task.sub_tasks_count || 0) + 1;
|
||||
|
||||
// Add the subtask
|
||||
task.sub_tasks.push({ ...subtask });
|
||||
} else {
|
||||
// Remove the subtask
|
||||
task.sub_tasks = task.sub_tasks.filter(t => t.id !== subtask.id);
|
||||
task.sub_tasks_count = Math.max(0, (task.sub_tasks_count || 1) - 1);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// First try to find the task in the specified section
|
||||
if (sectionId) {
|
||||
const section = state.taskGroups.find(sec => sec.id === sectionId);
|
||||
if (section) {
|
||||
const task = section.tasks.find(task => task.id === parentTaskId);
|
||||
if (task && updateTaskWithSubtask(task)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in the specified section, try all groups
|
||||
const result = findParentTaskInAllGroups(state.taskGroups, parentTaskId);
|
||||
if (result) {
|
||||
updateTaskWithSubtask(result.task);
|
||||
}
|
||||
},
|
||||
|
||||
toggleTaskExpansion: (state, action: PayloadAction<string>) => {
|
||||
const taskId = action.payload;
|
||||
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||
|
||||
if (result) {
|
||||
result.task.show_sub_tasks = !result.task.show_sub_tasks;
|
||||
}
|
||||
},
|
||||
updateBoardTaskStatus: (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 = findTaskInAllGroups(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);
|
||||
}
|
||||
},
|
||||
updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
|
||||
const { id, priority_id, color_code, color_code_dark } = action.payload;
|
||||
|
||||
// Find the task in any group
|
||||
const taskInfo = findTaskInAllGroups(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);
|
||||
}
|
||||
},
|
||||
updateBoardTaskLabel: (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;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTaskProgress: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
complete_ratio: number;
|
||||
completed_count: number;
|
||||
total_tasks_count: number;
|
||||
parent_task: string;
|
||||
}>
|
||||
) => {
|
||||
const { id, complete_ratio, completed_count, total_tasks_count, parent_task } =
|
||||
action.payload;
|
||||
|
||||
// Find the task in any group
|
||||
const taskInfo = findTaskInAllGroups(state.taskGroups, parent_task || id);
|
||||
|
||||
// Check if taskInfo exists before destructuring
|
||||
if (!taskInfo) return;
|
||||
|
||||
const { task } = taskInfo;
|
||||
|
||||
// Update the task properties
|
||||
task.complete_ratio = +complete_ratio;
|
||||
task.completed_count = completed_count;
|
||||
task.total_tasks_count = total_tasks_count;
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addCase(fetchBoardTaskGroups.pending, state => {
|
||||
state.loadingGroups = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchBoardTaskGroups.fulfilled, (state, action) => {
|
||||
state.loadingGroups = false;
|
||||
state.taskGroups = action.payload;
|
||||
})
|
||||
.addCase(fetchBoardTaskGroups.rejected, (state, action) => {
|
||||
state.loadingGroups = false;
|
||||
state.error = action.error.message || 'Failed to fetch task groups';
|
||||
})
|
||||
.addCase(fetchBoardSubTasks.pending, (state, action) => {
|
||||
state.error = null;
|
||||
// Find the task and set sub_tasks_loading to true
|
||||
const taskId = action.meta.arg.taskId;
|
||||
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||
if (result) {
|
||||
result.task.sub_tasks_loading = true;
|
||||
}
|
||||
})
|
||||
.addCase(fetchBoardSubTasks.fulfilled, (state, action: PayloadAction<IProjectTask[]>) => {
|
||||
if (action.payload.length > 0) {
|
||||
const taskId = action.payload[0].parent_task_id;
|
||||
if (taskId) {
|
||||
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||
if (result) {
|
||||
result.task.sub_tasks = action.payload;
|
||||
result.task.show_sub_tasks = true;
|
||||
result.task.sub_tasks_loading = false;
|
||||
result.task.sub_tasks_count = action.payload.length;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no subtasks were returned, we still need to set loading to false
|
||||
const taskId = (action as any).meta?.arg?.taskId;
|
||||
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||
if (result) {
|
||||
result.task.sub_tasks_loading = false;
|
||||
result.task.sub_tasks = [];
|
||||
result.task.sub_tasks_count = 0;
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(fetchBoardSubTasks.rejected, (state, action) => {
|
||||
state.error = action.error.message || 'Failed to fetch sub tasks';
|
||||
// Set loading to false on rejection
|
||||
const taskId = action.meta.arg.taskId;
|
||||
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||
if (result) {
|
||||
result.task.sub_tasks_loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setBoardGroupBy,
|
||||
addBoardSectionCard,
|
||||
setEditableSection,
|
||||
addTaskCardToTheTop,
|
||||
addTaskCardToTheBottom,
|
||||
addSubtask,
|
||||
deleteSection,
|
||||
deleteBoardTask,
|
||||
updateBoardTaskAssignee,
|
||||
reorderTaskGroups,
|
||||
moveTaskBetweenGroups,
|
||||
resetBoardData,
|
||||
setBoardLabels,
|
||||
setBoardMembers,
|
||||
setBoardPriorities,
|
||||
setBoardStatuses,
|
||||
setBoardSearch,
|
||||
setBoardGroupName,
|
||||
updateTaskAssignees,
|
||||
updateTaskEndDate,
|
||||
updateTaskName,
|
||||
updateSubtask,
|
||||
toggleSubtasksInclude,
|
||||
toggleTaskExpansion,
|
||||
updateBoardTaskStatus,
|
||||
updateTaskPriority,
|
||||
updateBoardTaskLabel,
|
||||
updateTaskProgress,
|
||||
} = boardSlice.actions;
|
||||
export default boardSlice.reducer;
|
||||
35
worklenz-frontend/src/features/board/create-card.slice.ts
Normal file
35
worklenz-frontend/src/features/board/create-card.slice.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
interface CreateCardState {
|
||||
taskCardDisabledStatus: { [group: string]: { top: boolean; bottom: boolean } };
|
||||
}
|
||||
|
||||
const initialState: CreateCardState = {
|
||||
taskCardDisabledStatus: {},
|
||||
};
|
||||
|
||||
const createCardSlice = createSlice({
|
||||
name: 'createCard',
|
||||
initialState,
|
||||
reducers: {
|
||||
initializeGroup(state, action: PayloadAction<string>) {
|
||||
const group = action.payload;
|
||||
if (!state.taskCardDisabledStatus[group]) {
|
||||
state.taskCardDisabledStatus[group] = { top: true, bottom: true };
|
||||
}
|
||||
},
|
||||
setTaskCardDisabled: (
|
||||
state,
|
||||
action: PayloadAction<{ group: string; position: 'top' | 'bottom'; disabled: boolean }>
|
||||
) => {
|
||||
const { group, position, disabled } = action.payload;
|
||||
if (!state.taskCardDisabledStatus[group]) {
|
||||
state.taskCardDisabledStatus[group] = { top: true, bottom: true };
|
||||
}
|
||||
state.taskCardDisabledStatus[group][position] = disabled;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setTaskCardDisabled, initializeGroup } = createCardSlice.actions;
|
||||
export default createCardSlice.reducer;
|
||||
Reference in New Issue
Block a user