Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into fix/task-list-realtime-update

This commit is contained in:
chamikaJ
2025-06-30 16:30:51 +05:30
6 changed files with 312 additions and 96 deletions

View File

@@ -30,6 +30,8 @@ import {
setDragState, setDragState,
reorderTasks, reorderTasks,
reorderGroups, reorderGroups,
fetchEnhancedKanbanTaskAssignees,
fetchEnhancedKanbanLabels,
} from '@/features/enhanced-kanban/enhanced-kanban.slice'; } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import EnhancedKanbanGroup from './EnhancedKanbanGroup'; import EnhancedKanbanGroup from './EnhancedKanbanGroup';
import EnhancedKanbanTaskCard from './EnhancedKanbanTaskCard'; import EnhancedKanbanTaskCard from './EnhancedKanbanTaskCard';
@@ -46,6 +48,7 @@ import { IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection'; import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection';
import ImprovedTaskFilters from '../task-management/improved-task-filters'; import ImprovedTaskFilters from '../task-management/improved-task-filters';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice'; import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
// Import the TaskListFilters component // Import the TaskListFilters component
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters')); const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
@@ -68,6 +71,10 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy); const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy);
const project = useAppSelector((state: RootState) => state.projectReducer.project); const project = useAppSelector((state: RootState) => state.projectReducer.project);
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer); const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
// Load filter data
useFilterDataLoader();
// Local state for drag overlay // Local state for drag overlay
const [activeTask, setActiveTask] = useState<any>(null); const [activeTask, setActiveTask] = useState<any>(null);
const [activeGroup, setActiveGroup] = useState<any>(null); const [activeGroup, setActiveGroup] = useState<any>(null);
@@ -86,6 +93,9 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
useEffect(() => { useEffect(() => {
if (projectId) { if (projectId) {
dispatch(fetchEnhancedKanbanGroups(projectId) as any); dispatch(fetchEnhancedKanbanGroups(projectId) as any);
// Load filter data for enhanced kanban
dispatch(fetchEnhancedKanbanTaskAssignees(projectId) as any);
dispatch(fetchEnhancedKanbanLabels(projectId) as any);
} }
if (!statusCategories.length) { if (!statusCategories.length) {
dispatch(fetchStatusesCategories() as any); dispatch(fetchStatusesCategories() as any);

View File

@@ -114,7 +114,6 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
const handleSubTaskExpand = useCallback(() => { const handleSubTaskExpand = useCallback(() => {
console.log('handleSubTaskExpand', task, projectId);
if (task && task.id && projectId) { if (task && task.id && projectId) {
if (task.show_sub_tasks) { if (task.show_sub_tasks) {
// If subtasks are already loaded, just toggle visibility // If subtasks are already loaded, just toggle visibility

View File

@@ -38,14 +38,12 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = ({
onTaskRender?.(task, index); onTaskRender?.(task, index);
return ( return (
<div className="virtualized-task-row" style={style}>
<EnhancedKanbanTaskCard <EnhancedKanbanTaskCard
task={task} task={task}
isActive={task.id === activeTaskId} isActive={task.id === activeTaskId}
isDropTarget={overId === task.id} isDropTarget={overId === task.id}
sectionId={task.status || 'default'} sectionId={task.status || 'default'}
/> />
</div>
); );
}, [tasks, activeTaskId, overId, onTaskRender]); }, [tasks, activeTaskId, overId, onTaskRender]);

View File

@@ -13,6 +13,7 @@ import { ITaskStatus } from '@/types/tasks/taskStatus.types';
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status'; import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
import { Select } from 'antd'; import { Select } from 'antd';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { updateEnhancedKanbanTaskStatus } from '@/features/enhanced-kanban/enhanced-kanban.slice';
interface TaskDrawerStatusDropdownProps { interface TaskDrawerStatusDropdownProps {
statuses: ITaskStatus[]; statuses: ITaskStatus[];
@@ -52,7 +53,7 @@ const TaskDrawerStatusDropdown = ({ statuses, task, teamId }: TaskDrawerStatusDr
dispatch(updateTaskStatus(data)); dispatch(updateTaskStatus(data));
} }
if (tab === 'board') { if (tab === 'board') {
dispatch(updateBoardTaskStatus(data)); dispatch(updateEnhancedKanbanTaskStatus(data));
} }
if (data.parent_task) getTaskProgress(data.parent_task); if (data.parent_task) getTaskProgress(data.parent_task);
} }

View File

@@ -42,8 +42,20 @@ import {
setPriorities as setKanbanPriorities, setPriorities as setKanbanPriorities,
setMembers as setKanbanMembers, setMembers as setKanbanMembers,
fetchEnhancedKanbanGroups, fetchEnhancedKanbanGroups,
setSelectedPriorities as setKanbanSelectedPriorities,
setBoardSearch as setKanbanBoardSearch,
setTaskAssigneeSelection,
setLabelSelection,
} from '@/features/enhanced-kanban/enhanced-kanban.slice'; } from '@/features/enhanced-kanban/enhanced-kanban.slice';
// Board slice imports for compatibility
import {
setBoardSearch,
setBoardPriorities,
setBoardMembers,
setBoardLabels,
} from '@/features/board/board-slice';
// Performance constants // Performance constants
const FILTER_DEBOUNCE_DELAY = 300; // ms const FILTER_DEBOUNCE_DELAY = 300; // ms
const SEARCH_DEBOUNCE_DELAY = 500; // ms const SEARCH_DEBOUNCE_DELAY = 500; // ms
@@ -60,6 +72,10 @@ const selectFilterData = createSelector(
(state: any) => state.taskReducer.taskAssignees, (state: any) => state.taskReducer.taskAssignees,
(state: any) => state.boardReducer.taskAssignees, (state: any) => state.boardReducer.taskAssignees,
(state: any) => state.projectReducer.project, (state: any) => state.projectReducer.project,
// Enhanced kanban data - use original data for filter options
(state: any) => state.enhancedKanbanReducer.originalTaskAssignees,
(state: any) => state.enhancedKanbanReducer.originalLabels,
(state: any) => state.enhancedKanbanReducer.priorities,
], ],
( (
priorities, priorities,
@@ -69,7 +85,10 @@ const selectFilterData = createSelector(
boardLabels, boardLabels,
taskAssignees, taskAssignees,
boardAssignees, boardAssignees,
project project,
kanbanOriginalTaskAssignees,
kanbanOriginalLabels,
kanbanPriorities
) => ({ ) => ({
priorities: priorities || [], priorities: priorities || [],
taskPriorities: taskPriorities || [], taskPriorities: taskPriorities || [],
@@ -80,6 +99,10 @@ const selectFilterData = createSelector(
boardAssignees: boardAssignees || [], boardAssignees: boardAssignees || [],
project, project,
selectedPriorities: taskPriorities || [], // Use taskReducer.priorities as selected priorities selectedPriorities: taskPriorities || [], // Use taskReducer.priorities as selected priorities
// Enhanced kanban data - use original data for filter options
kanbanTaskAssignees: kanbanOriginalTaskAssignees || [],
kanbanLabels: kanbanOriginalLabels || [],
kanbanPriorities: kanbanPriorities || [],
}) })
); );
@@ -160,11 +183,15 @@ const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
const currentLabels = kanbanState.labels || []; const currentLabels = kanbanState.labels || [];
const currentAssignees = kanbanState.taskAssignees || []; const currentAssignees = kanbanState.taskAssignees || [];
const groupByValue = kanbanState.groupBy || 'status'; const groupByValue = kanbanState.groupBy || 'status';
// Get priorities from the project or use empty array as fallback
const projectPriorities = (kanbanProject as any)?.priorities || [];
return [ return [
{ {
id: 'priority', id: 'priority',
label: 'Priority', label: 'Priority',
options: (kanbanProject?.priorities || []).map((p: any) => ({ options: filterData.priorities.map((p: any) => ({
value: p.id, value: p.id,
label: p.name, label: p.name,
color: p.color_code, color: p.color_code,
@@ -181,7 +208,7 @@ const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
multiSelect: true, multiSelect: true,
searchable: true, searchable: true,
selectedValues: currentAssignees.filter((m: any) => m.selected && m.id).map((m: any) => m.id || ''), selectedValues: currentAssignees.filter((m: any) => m.selected && m.id).map((m: any) => m.id || ''),
options: currentAssignees.map((assignee: any) => ({ options: filterData.kanbanTaskAssignees.map((assignee: any) => ({
id: assignee.id || '', id: assignee.id || '',
label: assignee.name || '', label: assignee.name || '',
value: assignee.id || '', value: assignee.id || '',
@@ -196,7 +223,7 @@ const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
multiSelect: true, multiSelect: true,
searchable: true, searchable: true,
selectedValues: currentLabels.filter((l: any) => l.selected && l.id).map((l: any) => l.id || ''), selectedValues: currentLabels.filter((l: any) => l.selected && l.id).map((l: any) => l.id || ''),
options: currentLabels.map((label: any) => ({ options: filterData.kanbanLabels.map((label: any) => ({
id: label.id || '', id: label.id || '',
label: label.name || '', label: label.name || '',
value: label.id || '', value: label.id || '',
@@ -214,7 +241,7 @@ const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
options: [ options: [
{ id: 'status', label: t('statusText'), value: 'status' }, { id: 'status', label: t('statusText'), value: 'status' },
{ id: 'priority', label: t('priorityText'), value: 'priority' }, { id: 'priority', label: t('priorityText'), value: 'priority' },
{ id: 'phase', label: kanbanProject?.phase_label || t('phaseText'), value: 'phase' }, { id: 'phase', label: (kanbanProject as any)?.phase_label || t('phaseText'), value: 'phase' },
], ],
}, },
]; ];
@@ -397,9 +424,8 @@ const FilterDropdown: React.FC<{
value={searchTerm} value={searchTerm}
onChange={e => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
placeholder={`Search ${section.label.toLowerCase()}...`} placeholder={`Search ${section.label.toLowerCase()}...`}
className={`w-full pl-8 pr-2 py-1 rounded border focus:outline-hidden focus:ring-2 focus:ring-blue-500 transition-colors duration-150 ${ className={`w-full pl-8 pr-2 py-1 rounded border focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-150 ${isDarkMode
isDarkMode ? 'bg-gray-700 text-gray-100 placeholder-gray-400 border-gray-600'
? 'bg-[#141414] text-[#d9d9d9] placeholder-gray-400 border-[#303030]'
: 'bg-white text-gray-900 placeholder-gray-400 border-gray-300' : 'bg-white text-gray-900 placeholder-gray-400 border-gray-300'
}`} }`}
/> />
@@ -520,8 +546,7 @@ const SearchFilter: React.FC<{
{!isExpanded ? ( {!isExpanded ? (
<button <button
onClick={handleToggle} onClick={handleToggle}
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md border transition-all duration-200 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 ${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText} ${ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 ${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText} ${themeClasses.containerBg === 'bg-gray-800' ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'
themeClasses.containerBg === 'bg-gray-800' ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'
}`} }`}
> >
<SearchOutlined className="w-3.5 h-3.5" /> <SearchOutlined className="w-3.5 h-3.5" />
@@ -537,9 +562,8 @@ const SearchFilter: React.FC<{
value={localValue} value={localValue}
onChange={(e) => setLocalValue(e.target.value)} onChange={(e) => setLocalValue(e.target.value)}
placeholder={placeholder} placeholder={placeholder}
className={`w-full pr-4 pl-8 py-1 rounded border focus:outline-hidden focus:ring-2 focus:ring-blue-500 transition-colors duration-150 ${ className={`w-full pr-4 pl-8 py-1 rounded border focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-150 ${isDarkMode
isDarkMode ? 'bg-gray-700 text-gray-100 placeholder-gray-400 border-gray-600'
? 'bg-[#141414] text-[#d9d9d9] placeholder-gray-400 border-[#303030]'
: 'bg-white text-gray-900 placeholder-gray-400 border-gray-300' : 'bg-white text-gray-900 placeholder-gray-400 border-gray-300'
}`} }`}
/> />
@@ -706,6 +730,9 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
const currentTaskAssignees = useAppSelector(state => state.taskReducer.taskAssignees); const currentTaskAssignees = useAppSelector(state => state.taskReducer.taskAssignees);
const currentTaskLabels = useAppSelector(state => state.taskReducer.labels); const currentTaskLabels = useAppSelector(state => state.taskReducer.labels);
// Enhanced Kanban state
const kanbanState = useAppSelector((state: RootState) => state.enhancedKanbanReducer);
// Use the filter data loader hook // Use the filter data loader hook
useFilterDataLoader(); useFilterDataLoader();
@@ -783,7 +810,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
if (projectView === 'list') { if (projectView === 'list') {
dispatch(setSearch(value)); dispatch(setSearch(value));
} else { } else {
dispatch(setBoardSearch(value)); dispatch(setKanbanSearch(value));
} }
// Trigger task refetch with new search value // Trigger task refetch with new search value
@@ -831,17 +858,42 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
return; return;
} }
if (sectionId === 'assignees') { if (sectionId === 'assignees') {
dispatch(setKanbanTaskAssignees( // Update individual assignee selections using the new action
// Map to {id, selected, ...} const currentAssignees = kanbanState.taskAssignees || [];
values.map(id => ({ id, selected: true })) const currentSelectedIds = currentAssignees.filter((m: any) => m.selected).map((m: any) => m.id);
));
// First, clear all selections
currentAssignees.forEach((assignee: any) => {
if (assignee.selected) {
dispatch(setTaskAssigneeSelection({ id: assignee.id, selected: false }));
}
});
// Then set the new selections
values.forEach(id => {
dispatch(setTaskAssigneeSelection({ id, selected: true }));
});
dispatch(fetchEnhancedKanbanGroups(projectId)); dispatch(fetchEnhancedKanbanGroups(projectId));
return; return;
} }
if (sectionId === 'labels') { if (sectionId === 'labels') {
dispatch(setKanbanLabels( // Update individual label selections using the new action
values.map(id => ({ id, selected: true })) const currentLabels = kanbanState.labels || [];
)); const currentSelectedIds = currentLabels.filter((l: any) => l.selected).map((l: any) => l.id);
// First, clear all selections
currentLabels.forEach((label: any) => {
if (label.selected) {
dispatch(setLabelSelection({ id: label.id, selected: false }));
}
});
// Then set the new selections
values.forEach(id => {
dispatch(setLabelSelection({ id, selected: true }));
});
dispatch(fetchEnhancedKanbanGroups(projectId)); dispatch(fetchEnhancedKanbanGroups(projectId));
return; return;
} }
@@ -853,7 +905,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
return; return;
} }
if (sectionId === 'priority') { if (sectionId === 'priority') {
dispatch(setSelectedPriorities(values)); dispatch(setPriorities(values));
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
return; return;
} }
@@ -877,7 +929,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
} }
} }
}, [dispatch, projectId, position, currentTaskAssignees, currentTaskLabels]); }, [dispatch, projectId, position, currentTaskAssignees, currentTaskLabels, kanbanState]);
const handleSearchChange = useCallback((value: string) => { const handleSearchChange = useCallback((value: string) => {
setSearchValue(value); setSearchValue(value);
@@ -887,11 +939,15 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
if (position === 'board') { if (position === 'board') {
dispatch(setKanbanSearch(value)); dispatch(setKanbanSearch(value));
if (projectId) {
dispatch(fetchEnhancedKanbanGroups(projectId)); dispatch(fetchEnhancedKanbanGroups(projectId));
}
} else { } else {
// Use debounced search // Use debounced search
if (projectId) {
debouncedSearchChangeRef.current?.(projectId, value); debouncedSearchChangeRef.current?.(projectId, value);
} }
}
}, [dispatch, projectId, position]); }, [dispatch, projectId, position]);
@@ -929,7 +985,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
if (projectView === 'list') { if (projectView === 'list') {
dispatch(setSearch('')); dispatch(setSearch(''));
} else { } else {
dispatch(setBoardSearch('')); dispatch(setKanbanBoardSearch(''));
} }
// Clear label filters // Clear label filters
@@ -956,7 +1012,9 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
// Use a short timeout to batch Redux state updates before API call // Use a short timeout to batch Redux state updates before API call
// This ensures all filter state is updated before the API call // This ensures all filter state is updated before the API call
setTimeout(() => { setTimeout(() => {
if (projectId) {
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
}
// Reset loading state after API call is initiated // Reset loading state after API call is initiated
setTimeout(() => setClearingFilters(false), 100); setTimeout(() => setClearingFilters(false), 100);
}, 0); }, 0);
@@ -970,7 +1028,9 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
setShowArchived(!showArchived); setShowArchived(!showArchived);
if (position === 'board') { if (position === 'board') {
dispatch(setKanbanArchived(!showArchived)); dispatch(setKanbanArchived(!showArchived));
if (projectId) {
dispatch(fetchEnhancedKanbanGroups(projectId)); dispatch(fetchEnhancedKanbanGroups(projectId));
}
} else { } else {
// ... existing logic ... // ... existing logic ...
} }
@@ -1022,8 +1082,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
<button <button
onClick={clearAllFilters} onClick={clearAllFilters}
disabled={clearingFilters} disabled={clearingFilters}
className={`text-xs font-medium transition-colors duration-150 ${ className={`text-xs font-medium transition-colors duration-150 ${clearingFilters
clearingFilters
? 'text-gray-400 cursor-not-allowed' ? 'text-gray-400 cursor-not-allowed'
: isDarkMode : isDarkMode
? 'text-blue-400 hover:text-blue-300' ? 'text-blue-400 hover:text-blue-300'
@@ -1073,16 +1132,20 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
if (projectId) { if (projectId) {
// Cancel pending search and immediately clear // Cancel pending search and immediately clear
debouncedSearchChangeRef.current?.cancel(); debouncedSearchChangeRef.current?.cancel();
if (position === 'board') {
dispatch(setKanbanSearch(''));
dispatch(fetchEnhancedKanbanGroups(projectId));
} else {
if (projectView === 'list') { if (projectView === 'list') {
dispatch(setSearch('')); dispatch(setSearch(''));
} else { } else {
dispatch(setBoardSearch('')); dispatch(setKanbanBoardSearch(''));
} }
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId));
} }
}
}} }}
className={`ml-1 rounded-full p-0.5 transition-colors duration-150 ${ className={`ml-1 rounded-full p-0.5 transition-colors duration-150 ${isDarkMode ? 'hover:bg-blue-800' : 'hover:bg-blue-200'
isDarkMode ? 'hover:bg-blue-800' : 'hover:bg-blue-200'
}`} }`}
> >
<CloseOutlined className="w-2.5 h-2.5" /> <CloseOutlined className="w-2.5 h-2.5" />
@@ -1122,8 +1185,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
const newValues = section.selectedValues.filter(v => v !== value); const newValues = section.selectedValues.filter(v => v !== value);
handleSelectionChange(section.id, newValues); handleSelectionChange(section.id, newValues);
}} }}
className={`ml-1 rounded-full p-0.5 transition-colors duration-150 ${ className={`ml-1 rounded-full p-0.5 transition-colors duration-150 ${isDarkMode ? 'hover:bg-gray-600' : 'hover:bg-gray-200'
isDarkMode ? 'hover:bg-gray-600' : 'hover:bg-gray-200'
}`} }`}
> >
<CloseOutlined className="w-2.5 h-2.5" /> <CloseOutlined className="w-2.5 h-2.5" />

View File

@@ -13,6 +13,7 @@ import { ITaskStatusViewModel } from '@/types/tasks/taskStatusGetResponse.types'
import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.types'; import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.types';
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types'; import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types'; import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service';
export enum IGroupBy { export enum IGroupBy {
STATUS = 'status', STATUS = 'status',
@@ -55,7 +56,11 @@ interface EnhancedKanbanState {
loadingGroups: boolean; loadingGroups: boolean;
error: string | null; error: string | null;
// Filters // Filters - Original data (should not be filtered)
originalTaskAssignees: ITaskListMemberFilter[];
originalLabels: ITaskLabelFilter[];
// Filters - Current filtered data
taskAssignees: ITaskListMemberFilter[]; taskAssignees: ITaskListMemberFilter[];
loadingAssignees: boolean; loadingAssignees: boolean;
statuses: ITaskStatusViewModel[]; statuses: ITaskStatusViewModel[];
@@ -102,6 +107,8 @@ const initialState: EnhancedKanbanState = {
taskGroups: [], taskGroups: [],
loadingGroups: false, loadingGroups: false,
error: null, error: null,
originalTaskAssignees: [],
originalLabels: [],
taskAssignees: [], taskAssignees: [],
loadingAssignees: false, loadingAssignees: false,
statuses: [], statuses: [],
@@ -155,7 +162,6 @@ export const fetchEnhancedKanbanGroups = createAsyncThunk(
try { try {
const state = getState() as { enhancedKanbanReducer: EnhancedKanbanState }; const state = getState() as { enhancedKanbanReducer: EnhancedKanbanState };
const { enhancedKanbanReducer } = state; const { enhancedKanbanReducer } = state;
const selectedMembers = enhancedKanbanReducer.taskAssignees const selectedMembers = enhancedKanbanReducer.taskAssignees
.filter(member => member.selected) .filter(member => member.selected)
.map(member => member.id) .map(member => member.id)
@@ -317,6 +323,40 @@ export const fetchBoardSubTasks = createAsyncThunk(
} }
); );
// Async thunk for loading task assignees
export const fetchEnhancedKanbanTaskAssignees = createAsyncThunk(
'enhancedKanban/fetchTaskAssignees',
async (projectId: string, { rejectWithValue }) => {
try {
const response = await tasksApiService.fetchTaskAssignees(projectId);
return response.body;
} catch (error) {
logger.error('Fetch Enhanced Kanban Task Assignees', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to fetch task assignees');
}
}
);
// Async thunk for loading labels
export const fetchEnhancedKanbanLabels = createAsyncThunk(
'enhancedKanban/fetchLabels',
async (projectId: string, { rejectWithValue }) => {
try {
const response = await labelsApiService.getPriorityByProject(projectId);
return response.body;
} catch (error) {
logger.error('Fetch Enhanced Kanban Labels', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to fetch project labels');
}
}
);
const enhancedKanbanSlice = createSlice({ const enhancedKanbanSlice = createSlice({
name: 'enhancedKanbanReducer', name: 'enhancedKanbanReducer',
initialState, initialState,
@@ -407,6 +447,38 @@ const enhancedKanbanSlice = createSlice({
state.members = action.payload; state.members = action.payload;
}, },
// New actions for filter selection that work with original data
setTaskAssigneeSelection: (state, action: PayloadAction<{ id: string; selected: boolean }>) => {
const { id, selected } = action.payload;
// Update both original and current data
state.originalTaskAssignees = state.originalTaskAssignees.map(assignee =>
assignee.id === id ? { ...assignee, selected } : assignee
);
state.taskAssignees = state.originalTaskAssignees;
},
setLabelSelection: (state, action: PayloadAction<{ id: string; selected: boolean }>) => {
const { id, selected } = action.payload;
// Update both original and current data
state.originalLabels = state.originalLabels.map(label =>
label.id === id ? { ...label, selected } : label
);
state.labels = state.originalLabels;
},
// Add missing actions for filter compatibility
setSelectedPriorities: (state, action: PayloadAction<string[]>) => {
state.priorities = action.payload;
},
setBoardSearch: (state, action: PayloadAction<string | null>) => {
state.search = action.payload;
},
setBoardArchived: (state, action: PayloadAction<boolean>) => {
state.archived = action.payload;
},
// Status updates // Status updates
updateTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => { updateTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
const { id: task_id, status_id } = action.payload; const { id: task_id, status_id } = action.payload;
@@ -423,6 +495,42 @@ const enhancedKanbanSlice = createSlice({
}); });
}, },
// Enhanced Kanban external status update (for use in task drawer dropdown)
updateEnhancedKanbanTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
const { id: task_id, status_id } = action.payload;
let oldGroupId: string | null = null;
let foundTask: IProjectTask | null = null;
// Find the task and its group
for (const group of state.taskGroups) {
const task = group.tasks.find(t => t.id === task_id);
if (task) {
foundTask = task;
oldGroupId = group.id;
break;
}
}
if (!foundTask) return;
// If grouped by status and the group changes, move the task
if (state.groupBy === IGroupBy.STATUS && oldGroupId && oldGroupId !== status_id) {
// Remove from old group
const oldGroup = state.taskGroups.find(g => g.id === oldGroupId);
if (oldGroup) {
oldGroup.tasks = oldGroup.tasks.filter(t => t.id !== task_id);
}
// Add to new group at the top
const newGroup = state.taskGroups.find(g => g.id === status_id);
if (newGroup) {
foundTask.status_id = status_id;
newGroup.tasks.unshift(foundTask);
}
} else {
// Just update the status_id
foundTask.status_id = status_id;
}
// Update cache
state.taskCache[task_id] = foundTask;
},
updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => { updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
const { id: task_id, priority_id } = action.payload; const { id: task_id, priority_id } = action.payload;
@@ -562,6 +670,38 @@ const enhancedKanbanSlice = createSlice({
// Update column order // Update column order
state.columnOrder = reorderedGroups.map(group => group.id); state.columnOrder = reorderedGroups.map(group => group.id);
})
// Fetch Task Assignees
.addCase(fetchEnhancedKanbanTaskAssignees.pending, (state) => {
state.loadingAssignees = true;
state.error = null;
})
.addCase(fetchEnhancedKanbanTaskAssignees.fulfilled, (state, action) => {
state.loadingAssignees = false;
// Store original data and current data
state.originalTaskAssignees = action.payload;
state.taskAssignees = action.payload;
})
.addCase(fetchEnhancedKanbanTaskAssignees.rejected, (state, action) => {
state.loadingAssignees = false;
state.error = action.payload as string;
})
// Fetch Labels
.addCase(fetchEnhancedKanbanLabels.pending, (state) => {
state.loadingLabels = true;
state.error = null;
})
.addCase(fetchEnhancedKanbanLabels.fulfilled, (state, action) => {
state.loadingLabels = false;
// Transform labels to include selected property
const newLabels = action.payload.map((label: any) => ({ ...label, selected: false }));
// Store original data and current data
state.originalLabels = newLabels;
state.labels = newLabels;
})
.addCase(fetchEnhancedKanbanLabels.rejected, (state, action) => {
state.loadingLabels = false;
state.error = action.payload as string;
}); });
}, },
}); });
@@ -584,6 +724,11 @@ export const {
setLabels, setLabels,
setPriorities, setPriorities,
setMembers, setMembers,
setTaskAssigneeSelection,
setLabelSelection,
setSelectedPriorities,
setBoardSearch,
setBoardArchived,
updateTaskStatus, updateTaskStatus,
updateTaskPriority, updateTaskPriority,
deleteTask, deleteTask,
@@ -591,6 +736,7 @@ export const {
reorderTasks, reorderTasks,
reorderGroups, reorderGroups,
addTaskToGroup, addTaskToGroup,
updateEnhancedKanbanTaskStatus,
} = enhancedKanbanSlice.actions; } = enhancedKanbanSlice.actions;
export default enhancedKanbanSlice.reducer; export default enhancedKanbanSlice.reducer;