Merge branch 'release/v2.0.4' into refact/board-task-card-performance
This commit is contained in:
@@ -10,6 +10,8 @@ import { SocketEvents } from '@/shared/socket-events';
|
|||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
import { Avatar, Button, Checkbox } from '@/components';
|
import { Avatar, Button, Checkbox } from '@/components';
|
||||||
import { sortTeamMembers } from '@/utils/sort-team-members';
|
import { sortTeamMembers } from '@/utils/sort-team-members';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
|
||||||
|
|
||||||
interface AssigneeSelectorProps {
|
interface AssigneeSelectorProps {
|
||||||
task: IProjectTask;
|
task: IProjectTask;
|
||||||
@@ -34,6 +36,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
const members = useSelector((state: RootState) => state.teamMembersReducer.teamMembers);
|
const members = useSelector((state: RootState) => state.teamMembersReducer.teamMembers);
|
||||||
const currentSession = useAuthService().getCurrentSession();
|
const currentSession = useAuthService().getCurrentSession();
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const filteredMembers = useMemo(() => {
|
const filteredMembers = useMemo(() => {
|
||||||
return teamMembers?.data?.filter(member =>
|
return teamMembers?.data?.filter(member =>
|
||||||
@@ -149,6 +152,11 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
return assignees?.includes(memberId) || false;
|
return assignees?.includes(memberId) || false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInviteProjectMemberDrawer = () => {
|
||||||
|
setIsOpen(false); // Close the assignee dropdown first
|
||||||
|
dispatch(toggleProjectMemberDrawer()); // Then open the invite drawer
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -271,10 +279,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
: 'text-blue-600 hover:bg-blue-50'
|
: 'text-blue-600 hover:bg-blue-50'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
onClick={() => {
|
onClick={handleInviteProjectMemberDrawer}
|
||||||
// TODO: Implement invite member functionality
|
|
||||||
console.log('Invite member clicked');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<UserAddOutlined />
|
<UserAddOutlined />
|
||||||
Invite member
|
Invite member
|
||||||
|
|||||||
@@ -1,50 +1,42 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { colors } from '@/styles/colors';
|
import { Spin } from 'antd';
|
||||||
import { getInitialTheme } from '@/utils/get-initial-theme';
|
|
||||||
import { ConfigProvider, theme, Layout, Spin } from 'antd';
|
|
||||||
|
|
||||||
// Memoized loading component with theme awareness
|
// Lightweight loading component - removed heavy theme calculations
|
||||||
export const SuspenseFallback = memo(() => {
|
export const SuspenseFallback = memo(() => {
|
||||||
const currentTheme = getInitialTheme();
|
|
||||||
const isDark = currentTheme === 'dark';
|
|
||||||
|
|
||||||
// Memoize theme configuration to prevent unnecessary re-renders
|
|
||||||
const themeConfig = React.useMemo(() => ({
|
|
||||||
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
|
||||||
components: {
|
|
||||||
Layout: {
|
|
||||||
colorBgLayout: isDark ? colors.darkGray : '#fafafa',
|
|
||||||
},
|
|
||||||
Spin: {
|
|
||||||
colorPrimary: isDark ? '#fff' : '#1890ff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}), [isDark]);
|
|
||||||
|
|
||||||
// Memoize layout style to prevent object recreation
|
|
||||||
const layoutStyle = React.useMemo(() => ({
|
|
||||||
position: 'fixed' as const,
|
|
||||||
width: '100vw',
|
|
||||||
height: '100vh',
|
|
||||||
background: 'transparent',
|
|
||||||
transition: 'none',
|
|
||||||
}), []);
|
|
||||||
|
|
||||||
// Memoize spin style to prevent object recreation
|
|
||||||
const spinStyle = React.useMemo(() => ({
|
|
||||||
position: 'absolute' as const,
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
}), []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={themeConfig}>
|
<div
|
||||||
<Layout className="app-loading-container" style={layoutStyle}>
|
style={{
|
||||||
<Spin size="large" style={spinStyle} />
|
position: 'fixed',
|
||||||
</Layout>
|
width: '100vw',
|
||||||
</ConfigProvider>
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'transparent',
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lightweight fallback for internal components that doesn't cover the screen
|
||||||
|
export const InlineSuspenseFallback = memo(() => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '40px 20px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: '200px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
SuspenseFallback.displayName = 'SuspenseFallback';
|
SuspenseFallback.displayName = 'SuspenseFallback';
|
||||||
|
InlineSuspenseFallback.displayName = 'InlineSuspenseFallback';
|
||||||
|
|||||||
@@ -14,33 +14,20 @@ import {
|
|||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
InboxOutlined,
|
InboxOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
SettingOutlined,
|
|
||||||
MoreOutlined,
|
|
||||||
} from './antd-imports';
|
} from './antd-imports';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import { AppDispatch } from '@/app/store';
|
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
|
||||||
import { colors } from '@/styles/colors';
|
|
||||||
import SingleAvatar from '@components/common/single-avatar/single-avatar';
|
|
||||||
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
|
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
|
||||||
import {
|
import { toggleField } from '@/features/task-management/taskListFields.slice';
|
||||||
Dropdown,
|
|
||||||
Checkbox,
|
|
||||||
Button,
|
|
||||||
Space,
|
|
||||||
taskManagementAntdConfig
|
|
||||||
} from './antd-imports';
|
|
||||||
import { toggleField, TaskListField } from '@/features/task-management/taskListFields.slice';
|
|
||||||
|
|
||||||
// Import Redux actions
|
// Import Redux actions
|
||||||
import { fetchTasksV3, setSelectedPriorities } from '@/features/task-management/task-management.slice';
|
import { fetchTasksV3, setSearch as setTaskManagementSearch } from '@/features/task-management/task-management.slice';
|
||||||
import { setCurrentGrouping, selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
import { setCurrentGrouping, selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
||||||
|
|
||||||
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
|
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
|
||||||
import { fetchLabelsByProject, fetchTaskAssignees, setMembers, setLabels } from '@/features/tasks/tasks.slice';
|
import { fetchLabelsByProject, fetchTaskAssignees, setMembers, setLabels, setSearch, setPriorities } from '@/features/tasks/tasks.slice';
|
||||||
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||||
import { ITaskPriority } from '@/types/tasks/taskPriority.types';
|
import { ITaskPriority } from '@/types/tasks/taskPriority.types';
|
||||||
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||||
@@ -57,6 +44,11 @@ import {
|
|||||||
fetchEnhancedKanbanGroups,
|
fetchEnhancedKanbanGroups,
|
||||||
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
|
||||||
|
// Performance constants
|
||||||
|
const FILTER_DEBOUNCE_DELAY = 300; // ms
|
||||||
|
const SEARCH_DEBOUNCE_DELAY = 500; // ms
|
||||||
|
const MAX_FILTER_OPTIONS = 100; // Limit options to prevent UI lag
|
||||||
|
|
||||||
// Optimized selectors with proper transformation logic
|
// Optimized selectors with proper transformation logic
|
||||||
const selectFilterData = createSelector(
|
const selectFilterData = createSelector(
|
||||||
[
|
[
|
||||||
@@ -68,7 +60,6 @@ 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,
|
||||||
(state: any) => state.taskManagement.selectedPriorities,
|
|
||||||
],
|
],
|
||||||
(
|
(
|
||||||
priorities,
|
priorities,
|
||||||
@@ -78,8 +69,7 @@ const selectFilterData = createSelector(
|
|||||||
boardLabels,
|
boardLabels,
|
||||||
taskAssignees,
|
taskAssignees,
|
||||||
boardAssignees,
|
boardAssignees,
|
||||||
project,
|
project
|
||||||
selectedPriorities
|
|
||||||
) => ({
|
) => ({
|
||||||
priorities: priorities || [],
|
priorities: priorities || [],
|
||||||
taskPriorities: taskPriorities || [],
|
taskPriorities: taskPriorities || [],
|
||||||
@@ -89,7 +79,7 @@ const selectFilterData = createSelector(
|
|||||||
taskAssignees: taskAssignees || [],
|
taskAssignees: taskAssignees || [],
|
||||||
boardAssignees: boardAssignees || [],
|
boardAssignees: boardAssignees || [],
|
||||||
project,
|
project,
|
||||||
selectedPriorities: selectedPriorities || [],
|
selectedPriorities: taskPriorities || [], // Use taskReducer.priorities as selected priorities
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -119,6 +109,33 @@ interface ImprovedTaskFiltersProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced debounce with cancellation support
|
||||||
|
function createDebouncedFunction<T extends (...args: any[]) => void>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): T & { cancel: () => void } {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const debouncedFunc = ((...args: any[]) => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
func(...args);
|
||||||
|
timeoutId = null;
|
||||||
|
}, delay);
|
||||||
|
}) as T & { cancel: () => void };
|
||||||
|
|
||||||
|
debouncedFunc.cancel = () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return debouncedFunc;
|
||||||
|
}
|
||||||
|
|
||||||
// Get real filter data from Redux state
|
// Get real filter data from Redux state
|
||||||
const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
|
const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
|
||||||
const { t } = useTranslation('task-list-filters');
|
const { t } = useTranslation('task-list-filters');
|
||||||
@@ -267,6 +284,7 @@ const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}, [isBoard, kanbanState, kanbanProject, filterData, currentProjectView, t, currentGrouping]);
|
}, [isBoard, kanbanState, kanbanProject, filterData, currentProjectView, t, currentGrouping]);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter Dropdown Component
|
// Filter Dropdown Component
|
||||||
@@ -283,19 +301,23 @@ const FilterDropdown: React.FC<{
|
|||||||
const [filteredOptions, setFilteredOptions] = useState(section.options);
|
const [filteredOptions, setFilteredOptions] = useState(section.options);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Filter options based on search term
|
// Memoized filter function to prevent unnecessary recalculations
|
||||||
useEffect(() => {
|
const filteredOptionsMemo = useMemo(() => {
|
||||||
if (!section.searchable || !searchTerm.trim()) {
|
if (!section.searchable || !searchTerm.trim()) {
|
||||||
setFilteredOptions(section.options);
|
return section.options;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = section.options.filter(option =>
|
const searchLower = searchTerm.toLowerCase();
|
||||||
option.label.toLowerCase().includes(searchTerm.toLowerCase())
|
return section.options.filter(option =>
|
||||||
|
option.label.toLowerCase().includes(searchLower)
|
||||||
);
|
);
|
||||||
setFilteredOptions(filtered);
|
|
||||||
}, [searchTerm, section.options, section.searchable]);
|
}, [searchTerm, section.options, section.searchable]);
|
||||||
|
|
||||||
|
// Update filtered options when memo changes
|
||||||
|
useEffect(() => {
|
||||||
|
setFilteredOptions(filteredOptionsMemo);
|
||||||
|
}, [filteredOptionsMemo]);
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@@ -550,35 +572,28 @@ const SearchFilter: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom debounce implementation
|
|
||||||
function debounce(func: (...args: any[]) => void, wait: number) {
|
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
|
||||||
return (...args: any[]) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => func(...args), wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = 'worklenz.taskManagement.fields';
|
const LOCAL_STORAGE_KEY = 'worklenz.taskManagement.fields';
|
||||||
|
|
||||||
const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({ themeClasses, isDarkMode }) => {
|
const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({ themeClasses, isDarkMode }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const fieldsRaw = useSelector((state: RootState) => state.taskManagementFields);
|
const fieldsRaw = useSelector((state: RootState) => state.taskManagementFields);
|
||||||
const fields = Array.isArray(fieldsRaw) ? fieldsRaw : [];
|
const fields = Array.isArray(fieldsRaw) ? fieldsRaw : [];
|
||||||
const sortedFields = [...fields].sort((a, b) => a.order - b.order);
|
const sortedFields = useMemo(() => [...fields].sort((a, b) => a.order - b.order), [fields]);
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Debounced save to localStorage using custom debounce
|
// Debounced save to localStorage using enhanced debounce
|
||||||
const debouncedSaveFields = useMemo(() => debounce((fieldsToSave: typeof fields) => {
|
const debouncedSaveFields = useMemo(() =>
|
||||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fieldsToSave));
|
createDebouncedFunction((fieldsToSave: typeof fields) => {
|
||||||
}, 300), []);
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fieldsToSave));
|
||||||
|
}, 300),
|
||||||
|
[]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
debouncedSaveFields(fields);
|
debouncedSaveFields(fields);
|
||||||
// Cleanup debounce on unmount
|
// Cleanup debounce on unmount
|
||||||
return () => { /* no cancel needed for custom debounce */ };
|
return () => debouncedSaveFields.cancel();
|
||||||
}, [fields, debouncedSaveFields]);
|
}, [fields, debouncedSaveFields]);
|
||||||
|
|
||||||
// Close dropdown on outside click
|
// Close dropdown on outside click
|
||||||
@@ -593,7 +608,7 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
return () => document.removeEventListener('mousedown', handleClick);
|
return () => document.removeEventListener('mousedown', handleClick);
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const visibleCount = sortedFields.filter(field => field.visible).length;
|
const visibleCount = useMemo(() => sortedFields.filter(field => field.visible).length, [sortedFields]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
@@ -686,13 +701,10 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('task-list-filters');
|
const { t } = useTranslation('task-list-filters');
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { socket, connected } = useSocket();
|
|
||||||
|
|
||||||
// Get current state values for filter updates
|
// Get current state values for filter updates
|
||||||
const currentTaskAssignees = useAppSelector(state => state.taskReducer.taskAssignees);
|
const currentTaskAssignees = useAppSelector(state => state.taskReducer.taskAssignees);
|
||||||
const currentBoardAssignees = useAppSelector(state => state.boardReducer.taskAssignees);
|
|
||||||
const currentTaskLabels = useAppSelector(state => state.taskReducer.labels);
|
const currentTaskLabels = useAppSelector(state => state.taskReducer.labels);
|
||||||
const currentBoardLabels = useAppSelector(state => state.boardReducer.labels);
|
|
||||||
|
|
||||||
// Use the filter data loader hook
|
// Use the filter data loader hook
|
||||||
useFilterDataLoader();
|
useFilterDataLoader();
|
||||||
@@ -703,6 +715,11 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||||
const [activeFiltersCount, setActiveFiltersCount] = useState(0);
|
const [activeFiltersCount, setActiveFiltersCount] = useState(0);
|
||||||
|
const [clearingFilters, setClearingFilters] = useState(false);
|
||||||
|
|
||||||
|
// Refs for debounced functions
|
||||||
|
const debouncedFilterChangeRef = useRef<((projectId: string) => void) & { cancel: () => void } | null>(null);
|
||||||
|
const debouncedSearchChangeRef = useRef<((projectId: string, value: string) => void) & { cancel: () => void } | null>(null);
|
||||||
|
|
||||||
// Get real filter data
|
// Get real filter data
|
||||||
const filterSectionsData = useFilterData(position);
|
const filterSectionsData = useFilterData(position);
|
||||||
@@ -717,15 +734,18 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
return filterSectionsData;
|
return filterSectionsData;
|
||||||
}, [filterSectionsData]);
|
}, [filterSectionsData]);
|
||||||
|
|
||||||
|
// Only update filter sections if they have actually changed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilterSections(memoizedFilterSections);
|
const hasChanged = JSON.stringify(filterSections) !== JSON.stringify(memoizedFilterSections);
|
||||||
}, [memoizedFilterSections]);
|
if (hasChanged && memoizedFilterSections.length > 0) {
|
||||||
|
setFilterSections(memoizedFilterSections);
|
||||||
|
}
|
||||||
|
}, [memoizedFilterSections, filterSections]);
|
||||||
|
|
||||||
// Redux selectors for theme and other state
|
// Redux selectors for theme and other state
|
||||||
const isDarkMode = useAppSelector(state => state.themeReducer?.mode === 'dark');
|
const isDarkMode = useAppSelector(state => state.themeReducer?.mode === 'dark');
|
||||||
const { projectId } = useAppSelector(state => state.projectReducer);
|
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||||
const { projectView } = useTabSearchParam();
|
const { projectView } = useTabSearchParam();
|
||||||
const { columns } = useAppSelector(state => state.taskReducer);
|
|
||||||
|
|
||||||
// Theme-aware class names - memoize to prevent unnecessary re-renders
|
// Theme-aware class names - memoize to prevent unnecessary re-renders
|
||||||
const themeClasses = useMemo(() => ({
|
const themeClasses = useMemo(() => ({
|
||||||
@@ -749,12 +769,47 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
searchText: isDarkMode ? 'text-gray-200' : 'text-gray-900',
|
searchText: isDarkMode ? 'text-gray-200' : 'text-gray-900',
|
||||||
}), [isDarkMode]);
|
}), [isDarkMode]);
|
||||||
|
|
||||||
// Calculate active filters count
|
// Initialize debounced functions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const count = filterSections.reduce((acc, section) => acc + section.selectedValues.length, 0);
|
// Debounced filter change function
|
||||||
setActiveFiltersCount(count + (searchValue ? 1 : 0));
|
debouncedFilterChangeRef.current = createDebouncedFunction((projectId: string) => {
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}, FILTER_DEBOUNCE_DELAY);
|
||||||
|
|
||||||
|
// Debounced search change function
|
||||||
|
debouncedSearchChangeRef.current = createDebouncedFunction((projectId: string, value: string) => {
|
||||||
|
// Dispatch search action based on current view
|
||||||
|
if (projectView === 'list') {
|
||||||
|
dispatch(setSearch(value));
|
||||||
|
} else {
|
||||||
|
dispatch(setBoardSearch(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger task refetch with new search value
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}, SEARCH_DEBOUNCE_DELAY);
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
debouncedFilterChangeRef.current?.cancel();
|
||||||
|
debouncedSearchChangeRef.current?.cancel();
|
||||||
|
};
|
||||||
|
}, [dispatch, projectView]);
|
||||||
|
|
||||||
|
// Calculate active filters count - memoized to prevent unnecessary recalculations
|
||||||
|
const calculatedActiveFiltersCount = useMemo(() => {
|
||||||
|
const count = filterSections.reduce((acc, section) =>
|
||||||
|
section.id === 'groupBy' ? acc : acc + section.selectedValues.length, 0
|
||||||
|
);
|
||||||
|
return count + (searchValue ? 1 : 0);
|
||||||
}, [filterSections, searchValue]);
|
}, [filterSections, searchValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeFiltersCount !== calculatedActiveFiltersCount) {
|
||||||
|
setActiveFiltersCount(calculatedActiveFiltersCount);
|
||||||
|
}
|
||||||
|
}, [calculatedActiveFiltersCount, activeFiltersCount]);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const handleDropdownToggle = useCallback((sectionId: string) => {
|
const handleDropdownToggle = useCallback((sectionId: string) => {
|
||||||
setOpenDropdown(current => current === sectionId ? null : sectionId);
|
setOpenDropdown(current => current === sectionId ? null : sectionId);
|
||||||
@@ -819,26 +874,96 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
dispatch(fetchTasksV3(projectId));
|
dispatch(fetchTasksV3(projectId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}, [dispatch, projectId, position, currentTaskAssignees, currentTaskLabels]);
|
}, [dispatch, projectId, position, currentTaskAssignees, currentTaskLabels]);
|
||||||
|
|
||||||
const handleSearchChange = useCallback((value: string) => {
|
const handleSearchChange = useCallback((value: string) => {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
|
|
||||||
|
|
||||||
|
if (!projectId) return;
|
||||||
|
|
||||||
if (position === 'board') {
|
if (position === 'board') {
|
||||||
dispatch(setKanbanSearch(value));
|
dispatch(setKanbanSearch(value));
|
||||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
} else {
|
} else {
|
||||||
// ... existing logic ...
|
// Use debounced search
|
||||||
// TODO: Implement proper search dispatch for list
|
debouncedSearchChangeRef.current?.(projectId, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}, [dispatch, projectId, position]);
|
}, [dispatch, projectId, position]);
|
||||||
|
|
||||||
const clearAllFilters = useCallback(() => {
|
const clearAllFilters = useCallback(async () => {
|
||||||
// TODO: Implement clear all filters
|
if (!projectId || clearingFilters) return;
|
||||||
console.log('Clear all filters');
|
|
||||||
setSearchValue('');
|
// Set loading state to prevent multiple clicks
|
||||||
setShowArchived(false);
|
setClearingFilters(true);
|
||||||
}, []);
|
|
||||||
|
try {
|
||||||
|
// Cancel any pending debounced calls
|
||||||
|
debouncedFilterChangeRef.current?.cancel();
|
||||||
|
debouncedSearchChangeRef.current?.cancel();
|
||||||
|
|
||||||
|
// Batch all state updates together to prevent multiple re-renders
|
||||||
|
const batchUpdates = () => {
|
||||||
|
// Clear local state immediately for UI feedback
|
||||||
|
setSearchValue('');
|
||||||
|
setShowArchived(false);
|
||||||
|
|
||||||
|
// Update local filter sections state immediately
|
||||||
|
setFilterSections(prev => prev.map(section => ({
|
||||||
|
...section,
|
||||||
|
selectedValues: section.id === 'groupBy' ? section.selectedValues : [] // Keep groupBy, clear others
|
||||||
|
})));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute all local state updates in a batch
|
||||||
|
batchUpdates();
|
||||||
|
|
||||||
|
// Prepare all Redux actions to be dispatched together
|
||||||
|
const reduxUpdates = () => {
|
||||||
|
// Clear search based on view
|
||||||
|
if (projectView === 'list') {
|
||||||
|
dispatch(setSearch(''));
|
||||||
|
} else {
|
||||||
|
dispatch(setBoardSearch(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear label filters
|
||||||
|
const clearedLabels = currentTaskLabels.map(label => ({
|
||||||
|
...label,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
|
dispatch(setLabels(clearedLabels));
|
||||||
|
|
||||||
|
// Clear assignee filters
|
||||||
|
const clearedAssignees = currentTaskAssignees.map(member => ({
|
||||||
|
...member,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
|
dispatch(setMembers(clearedAssignees));
|
||||||
|
|
||||||
|
// Clear priority filters
|
||||||
|
dispatch(setPriorities([]));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute Redux updates
|
||||||
|
reduxUpdates();
|
||||||
|
|
||||||
|
// Use a short timeout to batch Redux state updates before API call
|
||||||
|
// This ensures all filter state is updated before the API call
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
// Reset loading state after API call is initiated
|
||||||
|
setTimeout(() => setClearingFilters(false), 100);
|
||||||
|
}, 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing filters:', error);
|
||||||
|
setClearingFilters(false);
|
||||||
|
}
|
||||||
|
}, [projectId, projectView, dispatch, currentTaskLabels, currentTaskAssignees, clearingFilters]);
|
||||||
|
|
||||||
const toggleArchived = useCallback(() => {
|
const toggleArchived = useCallback(() => {
|
||||||
setShowArchived(!showArchived);
|
setShowArchived(!showArchived);
|
||||||
@@ -850,13 +975,6 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
}
|
}
|
||||||
}, [dispatch, projectId, position, showArchived]);
|
}, [dispatch, projectId, position, showArchived]);
|
||||||
|
|
||||||
// Show fields dropdown functionality
|
|
||||||
const handleColumnVisibilityChange = useCallback(async (col: ITaskListColumn) => {
|
|
||||||
if (!projectId) return;
|
|
||||||
console.log('Column visibility change:', col);
|
|
||||||
// TODO: Implement column visibility change
|
|
||||||
}, [projectId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${themeClasses.containerBg} border ${themeClasses.containerBorder} rounded-md p-3 shadow-sm ${className}`}>
|
<div className={`${themeClasses.containerBg} border ${themeClasses.containerBorder} rounded-md p-3 shadow-sm ${className}`}>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@@ -902,11 +1020,16 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={clearAllFilters}
|
onClick={clearAllFilters}
|
||||||
className={`text-xs text-blue-600 hover:text-blue-700 font-medium transition-colors duration-150 ${
|
disabled={clearingFilters}
|
||||||
isDarkMode ? 'text-blue-400 hover:text-blue-300' : ''
|
className={`text-xs font-medium transition-colors duration-150 ${
|
||||||
|
clearingFilters
|
||||||
|
? 'text-gray-400 cursor-not-allowed'
|
||||||
|
: isDarkMode
|
||||||
|
? 'text-blue-400 hover:text-blue-300'
|
||||||
|
: 'text-blue-600 hover:text-blue-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Clear all
|
{clearingFilters ? 'Clearing...' : 'Clear all'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -944,7 +1067,19 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
<SearchOutlined className="w-2.5 h-2.5" />
|
<SearchOutlined className="w-2.5 h-2.5" />
|
||||||
<span>"{searchValue}"</span>
|
<span>"{searchValue}"</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSearchValue('')}
|
onClick={() => {
|
||||||
|
setSearchValue('');
|
||||||
|
if (projectId) {
|
||||||
|
// Cancel pending search and immediately clear
|
||||||
|
debouncedSearchChangeRef.current?.cancel();
|
||||||
|
if (projectView === 'list') {
|
||||||
|
dispatch(setSearch(''));
|
||||||
|
} else {
|
||||||
|
dispatch(setBoardSearch(''));
|
||||||
|
}
|
||||||
|
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'
|
||||||
}`}
|
}`}
|
||||||
@@ -975,6 +1110,14 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// Update local state immediately for UI feedback
|
||||||
|
setFilterSections(prev => prev.map(s =>
|
||||||
|
s.id === section.id
|
||||||
|
? { ...s, selectedValues: s.selectedValues.filter(v => v !== value) }
|
||||||
|
: s
|
||||||
|
));
|
||||||
|
|
||||||
|
// Use debounced API call
|
||||||
const newValues = section.selectedValues.filter(v => v !== value);
|
const newValues = section.selectedValues.filter(v => v !== value);
|
||||||
handleSelectionChange(section.id, newValues);
|
handleSelectionChange(section.id, newValues);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const initialState: TaskManagementState = {
|
|||||||
groups: [],
|
groups: [],
|
||||||
grouping: null,
|
grouping: null,
|
||||||
selectedPriorities: [],
|
selectedPriorities: [],
|
||||||
|
search: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Async thunk to fetch tasks from API
|
// Async thunk to fetch tasks from API
|
||||||
@@ -140,20 +141,24 @@ export const fetchTasksV3 = createAsyncThunk(
|
|||||||
|
|
||||||
// Get selected labels from taskReducer
|
// Get selected labels from taskReducer
|
||||||
const selectedLabels = state.taskReducer.labels
|
const selectedLabels = state.taskReducer.labels
|
||||||
? state.taskReducer.labels.filter(l => l.selected).map(l => l.id).join(',')
|
? state.taskReducer.labels.filter(l => l.selected).map(l => l.id).join(' ')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Get selected assignees from taskReducer
|
// Get selected assignees from taskReducer
|
||||||
const selectedAssignees = state.taskReducer.taskAssignees
|
const selectedAssignees = state.taskReducer.taskAssignees
|
||||||
? state.taskReducer.taskAssignees.filter(m => m.selected).map(m => m.id).join(',')
|
? state.taskReducer.taskAssignees.filter(m => m.selected).map(m => m.id).join(' ')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Get selected priorities from taskManagement slice
|
// Get selected priorities from taskReducer (consistent with other slices)
|
||||||
const selectedPriorities = state.taskManagement.selectedPriorities
|
const selectedPriorities = state.taskReducer.priorities
|
||||||
? state.taskManagement.selectedPriorities.join(',')
|
? state.taskReducer.priorities.join(' ')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
// Get search value from taskReducer
|
||||||
|
const searchValue = state.taskReducer.search || '';
|
||||||
|
|
||||||
console.log('fetchTasksV3 - selectedPriorities:', selectedPriorities);
|
console.log('fetchTasksV3 - selectedPriorities:', selectedPriorities);
|
||||||
|
console.log('fetchTasksV3 - searchValue:', searchValue);
|
||||||
|
|
||||||
const config: ITaskListConfigV2 = {
|
const config: ITaskListConfigV2 = {
|
||||||
id: projectId,
|
id: projectId,
|
||||||
@@ -161,7 +166,7 @@ export const fetchTasksV3 = createAsyncThunk(
|
|||||||
group: currentGrouping,
|
group: currentGrouping,
|
||||||
field: '',
|
field: '',
|
||||||
order: '',
|
order: '',
|
||||||
search: '',
|
search: searchValue,
|
||||||
statuses: '',
|
statuses: '',
|
||||||
members: selectedAssignees,
|
members: selectedAssignees,
|
||||||
projects: '',
|
projects: '',
|
||||||
@@ -328,6 +333,11 @@ const taskManagementSlice = createSlice({
|
|||||||
setSelectedPriorities: (state, action: PayloadAction<string[]>) => {
|
setSelectedPriorities: (state, action: PayloadAction<string[]>) => {
|
||||||
state.selectedPriorities = action.payload;
|
state.selectedPriorities = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Search action
|
||||||
|
setSearch: (state, action: PayloadAction<string>) => {
|
||||||
|
state.search = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
@@ -387,6 +397,7 @@ export const {
|
|||||||
setLoading,
|
setLoading,
|
||||||
setError,
|
setError,
|
||||||
setSelectedPriorities,
|
setSelectedPriorities,
|
||||||
|
setSearch,
|
||||||
} = taskManagementSlice.actions;
|
} = taskManagementSlice.actions;
|
||||||
|
|
||||||
export default taskManagementSlice.reducer;
|
export default taskManagementSlice.reducer;
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, Suspense } from 'react';
|
||||||
|
import { InlineSuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||||
|
|
||||||
// Lazy load all project view components to reduce initial bundle size
|
// Import core components synchronously to avoid suspense in main tabs
|
||||||
|
import ProjectViewEnhancedTasks from '@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks';
|
||||||
|
import ProjectViewEnhancedBoard from '@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board';
|
||||||
|
|
||||||
|
// Lazy load less critical components
|
||||||
const ProjectViewInsights = React.lazy(() => import('@/pages/projects/projectView/insights/project-view-insights'));
|
const ProjectViewInsights = React.lazy(() => import('@/pages/projects/projectView/insights/project-view-insights'));
|
||||||
const ProjectViewFiles = React.lazy(() => import('@/pages/projects/projectView/files/project-view-files'));
|
const ProjectViewFiles = React.lazy(() => import('@/pages/projects/projectView/files/project-view-files'));
|
||||||
const ProjectViewMembers = React.lazy(() => import('@/pages/projects/projectView/members/project-view-members'));
|
const ProjectViewMembers = React.lazy(() => import('@/pages/projects/projectView/members/project-view-members'));
|
||||||
const ProjectViewUpdates = React.lazy(() => import('@/pages/projects/project-view-1/updates/project-view-updates'));
|
const ProjectViewUpdates = React.lazy(() => import('@/pages/projects/project-view-1/updates/project-view-updates'));
|
||||||
const ProjectViewEnhancedTasks = React.lazy(() => import('@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks'));
|
|
||||||
const ProjectViewEnhancedBoard = React.lazy(() => import('@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board'));
|
|
||||||
|
|
||||||
// type of a tab items
|
// type of a tab items
|
||||||
type TabItems = {
|
type TabItems = {
|
||||||
@@ -37,24 +40,36 @@ export const tabItems: TabItems[] = [
|
|||||||
index: 2,
|
index: 2,
|
||||||
key: 'project-insights-member-overview',
|
key: 'project-insights-member-overview',
|
||||||
label: 'Insights',
|
label: 'Insights',
|
||||||
element: React.createElement(ProjectViewInsights),
|
element: React.createElement(Suspense,
|
||||||
|
{ fallback: React.createElement(InlineSuspenseFallback) },
|
||||||
|
React.createElement(ProjectViewInsights)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 3,
|
index: 3,
|
||||||
key: 'all-attachments',
|
key: 'all-attachments',
|
||||||
label: 'Files',
|
label: 'Files',
|
||||||
element: React.createElement(ProjectViewFiles),
|
element: React.createElement(Suspense,
|
||||||
|
{ fallback: React.createElement(InlineSuspenseFallback) },
|
||||||
|
React.createElement(ProjectViewFiles)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 4,
|
index: 4,
|
||||||
key: 'members',
|
key: 'members',
|
||||||
label: 'Members',
|
label: 'Members',
|
||||||
element: React.createElement(ProjectViewMembers),
|
element: React.createElement(Suspense,
|
||||||
|
{ fallback: React.createElement(InlineSuspenseFallback) },
|
||||||
|
React.createElement(ProjectViewMembers)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 5,
|
index: 5,
|
||||||
key: 'updates',
|
key: 'updates',
|
||||||
label: 'Updates',
|
label: 'Updates',
|
||||||
element: React.createElement(ProjectViewUpdates),
|
element: React.createElement(Suspense,
|
||||||
|
{ fallback: React.createElement(InlineSuspenseFallback) },
|
||||||
|
React.createElement(ProjectViewUpdates)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState, useRef, useMemo, useCallback, lazy, Suspense } from 'react';
|
import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
|
||||||
const TaskListFilters = lazy(() => import('../taskList/task-list-filters/task-list-filters'));
|
import TaskListFilters from '../taskList/task-list-filters/task-list-filters';
|
||||||
import { Flex, Skeleton } from 'antd';
|
import { Flex, Skeleton } from 'antd';
|
||||||
import BoardSectionCardContainer from './board-section/board-section-container';
|
import BoardSectionCardContainer from './board-section/board-section-container';
|
||||||
import {
|
import {
|
||||||
@@ -44,6 +44,7 @@ import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status'
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
||||||
import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice';
|
import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice';
|
||||||
|
|
||||||
interface DroppableContainer {
|
interface DroppableContainer {
|
||||||
id: UniqueIdentifier;
|
id: UniqueIdentifier;
|
||||||
data: {
|
data: {
|
||||||
@@ -554,9 +555,7 @@ const ProjectViewBoard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical gap={16}>
|
<Flex vertical gap={16}>
|
||||||
<Suspense fallback={<div>Loading filters...</div>}>
|
<TaskListFilters position={'board'} />
|
||||||
<TaskListFilters position={'board'} />
|
|
||||||
</Suspense>
|
|
||||||
<Skeleton active loading={isLoading} className='mt-4 p-4'>
|
<Skeleton active loading={isLoading} className='mt-4 p-4'>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import useTabSearchParam from '@/hooks/useTabSearchParam';
|
|||||||
import { addTaskCardToTheTop, fetchBoardTaskGroups } from '@/features/board/board-slice';
|
import { addTaskCardToTheTop, fetchBoardTaskGroups } from '@/features/board/board-slice';
|
||||||
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
|
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
|
||||||
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
||||||
|
|
||||||
const ProjectViewHeader = () => {
|
const ProjectViewHeader = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -83,6 +84,8 @@ const ProjectViewHeader = () => {
|
|||||||
dispatch(fetchTaskListColumns(projectId));
|
dispatch(fetchTaskListColumns(projectId));
|
||||||
dispatch(fetchPhasesByProjectId(projectId))
|
dispatch(fetchPhasesByProjectId(projectId))
|
||||||
dispatch(fetchTaskGroups(projectId));
|
dispatch(fetchTaskGroups(projectId));
|
||||||
|
// Also refresh the enhanced tasks data
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
break;
|
break;
|
||||||
case 'board':
|
case 'board':
|
||||||
// dispatch(fetchBoardTaskGroups(projectId));
|
// dispatch(fetchBoardTaskGroups(projectId));
|
||||||
@@ -166,7 +169,7 @@ const ProjectViewHeader = () => {
|
|||||||
try {
|
try {
|
||||||
setCreatingTask(true);
|
setCreatingTask(true);
|
||||||
|
|
||||||
const body: ITaskCreateRequest = {
|
const body: Partial<ITaskCreateRequest> = {
|
||||||
name: DEFAULT_TASK_NAME,
|
name: DEFAULT_TASK_NAME,
|
||||||
project_id: selectedProject?.id,
|
project_id: selectedProject?.id,
|
||||||
reporter_id: currentSession?.id,
|
reporter_id: currentSession?.id,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useState, useMemo, lazy, Suspense } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
import { Empty } from '@/shared/antd-imports';
|
import { Empty } from '@/shared/antd-imports';
|
||||||
import Flex from 'antd/es/flex';
|
import Flex from 'antd/es/flex';
|
||||||
import Skeleton from 'antd/es/skeleton';
|
import Skeleton from 'antd/es/skeleton';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
const TaskListFilters = lazy(() => import('./task-list-filters/task-list-filters'));
|
import TaskListFilters from './task-list-filters/task-list-filters';
|
||||||
import TaskGroupWrapperOptimized from './task-group-wrapper-optimized';
|
import TaskGroupWrapperOptimized from './task-group-wrapper-optimized';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
@@ -17,7 +17,7 @@ const ProjectViewTaskList = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { projectView } = useTabSearchParam();
|
const { projectView } = useTabSearchParam();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
const [coreDataLoaded, setCoreDataLoaded] = useState(false);
|
||||||
|
|
||||||
// Split selectors to prevent unnecessary rerenders
|
// Split selectors to prevent unnecessary rerenders
|
||||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||||
@@ -33,11 +33,11 @@ const ProjectViewTaskList = () => {
|
|||||||
|
|
||||||
const loadingPhases = useAppSelector(state => state.phaseReducer.loadingPhases);
|
const loadingPhases = useAppSelector(state => state.phaseReducer.loadingPhases);
|
||||||
|
|
||||||
// Single source of truth for loading state - EXCLUDE labels loading from skeleton
|
// Simplified loading state - only wait for essential data
|
||||||
// Labels loading should not block the main task list display
|
// Remove dependency on phases and status categories for initial render
|
||||||
const isLoading = useMemo(() =>
|
const isLoading = useMemo(() =>
|
||||||
loadingGroups || loadingPhases || loadingStatusCategories || !initialLoadComplete,
|
loadingGroups || !coreDataLoaded,
|
||||||
[loadingGroups, loadingPhases, loadingStatusCategories, initialLoadComplete]
|
[loadingGroups, coreDataLoaded]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize the empty state check
|
// Memoize the empty state check
|
||||||
@@ -56,53 +56,63 @@ const ProjectViewTaskList = () => {
|
|||||||
}
|
}
|
||||||
}, [projectView, setSearchParams, searchParams]);
|
}, [projectView, setSearchParams, searchParams]);
|
||||||
|
|
||||||
// Batch initial data fetching - core data only
|
// Optimized parallel data fetching - don't wait for everything
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchInitialData = async () => {
|
const fetchCoreData = async () => {
|
||||||
if (!projectId || !groupBy || initialLoadComplete) return;
|
if (!projectId || !groupBy || coreDataLoaded) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Batch only essential API calls for initial load
|
// Start all requests in parallel, but only wait for task columns
|
||||||
// Filter data (labels, assignees, etc.) will load separately and not block the UI
|
// Other data can load in background without blocking UI
|
||||||
await Promise.allSettled([
|
const corePromises = [
|
||||||
dispatch(fetchTaskListColumns(projectId)),
|
dispatch(fetchTaskListColumns(projectId)),
|
||||||
dispatch(fetchPhasesByProjectId(projectId)),
|
dispatch(fetchTaskGroups(projectId)), // Start immediately
|
||||||
dispatch(fetchStatusesCategories()),
|
];
|
||||||
]);
|
|
||||||
setInitialLoadComplete(true);
|
// Background data - don't wait for these
|
||||||
|
dispatch(fetchPhasesByProjectId(projectId));
|
||||||
|
dispatch(fetchStatusesCategories());
|
||||||
|
|
||||||
|
// Only wait for essential data
|
||||||
|
await Promise.allSettled(corePromises);
|
||||||
|
setCoreDataLoaded(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching initial data:', error);
|
console.error('Error fetching core data:', error);
|
||||||
setInitialLoadComplete(true); // Still mark as complete to prevent infinite loading
|
setCoreDataLoaded(true); // Still mark as complete to prevent infinite loading
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchInitialData();
|
fetchCoreData();
|
||||||
}, [projectId, groupBy, dispatch, initialLoadComplete]);
|
}, [projectId, groupBy, dispatch, coreDataLoaded]);
|
||||||
|
|
||||||
// Fetch task groups with dependency on initial load completion
|
// Optimized task groups fetching - remove initialLoadComplete dependency
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTasks = async () => {
|
const fetchTasks = async () => {
|
||||||
if (!projectId || !groupBy || projectView !== 'list' || !initialLoadComplete) return;
|
if (!projectId || !groupBy || projectView !== 'list') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dispatch(fetchTaskGroups(projectId));
|
// Only refetch if filters change, not on initial load
|
||||||
|
if (coreDataLoaded) {
|
||||||
|
await dispatch(fetchTaskGroups(projectId));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching task groups:', error);
|
console.error('Error fetching task groups:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchTasks();
|
// Only refetch when filters change
|
||||||
}, [projectId, groupBy, projectView, dispatch, fields, search, archived, initialLoadComplete]);
|
if (coreDataLoaded) {
|
||||||
|
fetchTasks();
|
||||||
|
}
|
||||||
|
}, [projectId, groupBy, projectView, dispatch, fields, search, archived, coreDataLoaded]);
|
||||||
|
|
||||||
// Memoize the task groups to prevent unnecessary re-renders
|
// Memoize the task groups to prevent unnecessary re-renders
|
||||||
const memoizedTaskGroups = useMemo(() => taskGroups || [], [taskGroups]);
|
const memoizedTaskGroups = useMemo(() => taskGroups || [], [taskGroups]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||||
{/* Filters load independently and don't block the main content */}
|
{/* Filters load synchronously - no suspense boundary */}
|
||||||
<Suspense fallback={<div>Loading filters...</div>}>
|
<TaskListFilters position="list" />
|
||||||
<TaskListFilters position="list" />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{isEmptyState ? (
|
{isEmptyState ? (
|
||||||
<Empty description="No tasks group found" />
|
<Empty description="No tasks group found" />
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ import {
|
|||||||
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
|
|
||||||
const SearchDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/search-dropdown'));
|
// Import filter components synchronously for better performance
|
||||||
const SortFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/sort-filter-dropdown'));
|
import SearchDropdown from '@components/project-task-filters/filter-dropdowns/search-dropdown';
|
||||||
const LabelsFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/labels-filter-dropdown'));
|
import SortFilterDropdown from '@components/project-task-filters/filter-dropdowns/sort-filter-dropdown';
|
||||||
const MembersFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/members-filter-dropdown'));
|
import LabelsFilterDropdown from '@components/project-task-filters/filter-dropdowns/labels-filter-dropdown';
|
||||||
const GroupByFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/group-by-filter-dropdown'));
|
import MembersFilterDropdown from '@components/project-task-filters/filter-dropdowns/members-filter-dropdown';
|
||||||
const ShowFieldsFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown'));
|
import GroupByFilterDropdown from '@components/project-task-filters/filter-dropdowns/group-by-filter-dropdown';
|
||||||
const PriorityFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/priority-filter-dropdown'));
|
import ShowFieldsFilterDropdown from '@components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown';
|
||||||
|
import PriorityFilterDropdown from '@components/project-task-filters/filter-dropdowns/priority-filter-dropdown';
|
||||||
|
|
||||||
interface TaskListFiltersProps {
|
interface TaskListFiltersProps {
|
||||||
position: 'board' | 'list';
|
position: 'board' | 'list';
|
||||||
@@ -39,44 +40,46 @@ const TaskListFilters: React.FC<TaskListFiltersProps> = ({ position }) => {
|
|||||||
|
|
||||||
const handleShowArchivedChange = () => dispatch(toggleArchived());
|
const handleShowArchivedChange = () => dispatch(toggleArchived());
|
||||||
|
|
||||||
// Load filter data asynchronously and non-blocking
|
// Optimized filter data loading - staggered and non-blocking
|
||||||
// This runs independently of the main task list loading
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadFilterData = async () => {
|
const loadFilterData = () => {
|
||||||
try {
|
try {
|
||||||
// Load priorities first (usually cached/fast)
|
// Load priorities first (usually cached/fast) - immediate
|
||||||
if (!priorities.length) {
|
if (!priorities.length) {
|
||||||
dispatch(fetchPriorities());
|
dispatch(fetchPriorities());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load project-specific filter data in parallel, but don't await
|
|
||||||
// This allows the main task list to load while filters are still loading
|
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
// Fire and forget - these will update the UI when ready
|
// Stagger the loading to prevent overwhelming the server
|
||||||
dispatch(fetchLabelsByProject(projectId));
|
// Load project-specific data with delays
|
||||||
dispatch(fetchTaskAssignees(projectId));
|
setTimeout(() => {
|
||||||
}
|
dispatch(fetchLabelsByProject(projectId));
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// Load team members (usually needed for member filters)
|
setTimeout(() => {
|
||||||
dispatch(getTeamMembers({
|
dispatch(fetchTaskAssignees(projectId));
|
||||||
index: 0,
|
}, 200);
|
||||||
size: 100,
|
|
||||||
field: null,
|
// Load team members last (heaviest query)
|
||||||
order: null,
|
setTimeout(() => {
|
||||||
search: null,
|
dispatch(getTeamMembers({
|
||||||
all: true
|
index: 0,
|
||||||
}));
|
size: 50, // Reduce initial load size
|
||||||
|
field: null,
|
||||||
|
order: null,
|
||||||
|
search: null,
|
||||||
|
all: false // Don't load all at once
|
||||||
|
}));
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading filter data:', error);
|
console.error('Error loading filter data:', error);
|
||||||
// Don't throw - filter loading errors shouldn't break the main UI
|
// Don't throw - filter loading errors shouldn't break the main UI
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use setTimeout to ensure this runs after the main component render
|
// Load immediately without setTimeout to avoid additional delay
|
||||||
// This prevents filter loading from blocking the initial render
|
loadFilterData();
|
||||||
const timeoutId = setTimeout(loadFilterData, 0);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}, [dispatch, priorities.length, projectId]);
|
}, [dispatch, priorities.length, projectId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export interface TaskManagementState {
|
|||||||
groups: TaskGroup[]; // Pre-processed groups from V3 API
|
groups: TaskGroup[]; // Pre-processed groups from V3 API
|
||||||
grouping: string | null; // Current grouping from V3 API
|
grouping: string | null; // Current grouping from V3 API
|
||||||
selectedPriorities: string[]; // Selected priority filters
|
selectedPriorities: string[]; // Selected priority filters
|
||||||
|
search: string; // Search query for filtering tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskGroupsState {
|
export interface TaskGroupsState {
|
||||||
|
|||||||
Reference in New Issue
Block a user