Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into fix/task-list-realtime-update
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user