Merge pull request #186 from shancds/refact/board-task-card-performance
feat(task-filters): enhance ImprovedTaskFilters for Kanban integration
This commit is contained in:
@@ -25,8 +25,24 @@ import { toggleField } from '@/features/task-management/taskListFields.slice';
|
|||||||
// Import Redux actions
|
// Import Redux actions
|
||||||
import { fetchTasksV3, setSearch as setTaskManagementSearch } 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 { setMembers, setLabels, setSearch, setPriorities } from '@/features/tasks/tasks.slice';
|
|
||||||
import { setBoardSearch } from '@/features/board/board-slice';
|
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
|
||||||
|
import { fetchLabelsByProject, fetchTaskAssignees, setMembers, setLabels, setSearch, setPriorities } from '@/features/tasks/tasks.slice';
|
||||||
|
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||||
|
import { ITaskPriority } from '@/types/tasks/taskPriority.types';
|
||||||
|
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||||
|
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
||||||
|
// --- Enhanced Kanban imports ---
|
||||||
|
import {
|
||||||
|
setGroupBy as setKanbanGroupBy,
|
||||||
|
setSearch as setKanbanSearch,
|
||||||
|
setArchived as setKanbanArchived,
|
||||||
|
setTaskAssignees as setKanbanTaskAssignees,
|
||||||
|
setLabels as setKanbanLabels,
|
||||||
|
setPriorities as setKanbanPriorities,
|
||||||
|
setMembers as setKanbanMembers,
|
||||||
|
fetchEnhancedKanbanGroups,
|
||||||
|
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
|
||||||
// Performance constants
|
// Performance constants
|
||||||
const FILTER_DEBOUNCE_DELAY = 300; // ms
|
const FILTER_DEBOUNCE_DELAY = 300; // ms
|
||||||
@@ -121,7 +137,7 @@ function createDebouncedFunction<T extends (...args: any[]) => void>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get real filter data from Redux state
|
// Get real filter data from Redux state
|
||||||
const useFilterData = (): FilterSection[] => {
|
const useFilterData = (position: 'board' | 'list'): FilterSection[] => {
|
||||||
const { t } = useTranslation('task-list-filters');
|
const { t } = useTranslation('task-list-filters');
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { projectView } = useTabSearchParam();
|
const { projectView } = useTabSearchParam();
|
||||||
@@ -129,80 +145,146 @@ const useFilterData = (): FilterSection[] => {
|
|||||||
// Use optimized selector to get all filter data at once
|
// Use optimized selector to get all filter data at once
|
||||||
const filterData = useAppSelector(selectFilterData);
|
const filterData = useAppSelector(selectFilterData);
|
||||||
const currentGrouping = useAppSelector(selectCurrentGrouping);
|
const currentGrouping = useAppSelector(selectCurrentGrouping);
|
||||||
|
// Enhanced Kanban selectors
|
||||||
|
const kanbanState = useAppSelector((state: RootState) => state.enhancedKanbanReducer);
|
||||||
|
const kanbanProject = useAppSelector((state: RootState) => state.projectReducer.project);
|
||||||
|
// Determine which state to use
|
||||||
|
const isBoard = position === 'board';
|
||||||
const tab = searchParams.get('tab');
|
const tab = searchParams.get('tab');
|
||||||
const currentProjectView = tab === 'tasks-list' ? 'list' : 'kanban';
|
const currentProjectView = tab === 'tasks-list' ? 'list' : 'kanban';
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const currentPriorities = currentProjectView === 'list' ? filterData.taskPriorities : filterData.boardPriorities;
|
if (isBoard) {
|
||||||
const currentLabels = currentProjectView === 'list' ? filterData.taskLabels : filterData.boardLabels;
|
// Use enhanced kanban state
|
||||||
const currentAssignees = currentProjectView === 'list' ? filterData.taskAssignees : filterData.boardAssignees;
|
const currentPriorities = kanbanState.priorities || [];
|
||||||
const groupByValue = currentGrouping || 'status';
|
const currentLabels = kanbanState.labels || [];
|
||||||
|
const currentAssignees = kanbanState.taskAssignees || [];
|
||||||
|
const groupByValue = kanbanState.groupBy || 'status';
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'priority',
|
||||||
|
label: 'Priority',
|
||||||
|
options: (kanbanProject?.priorities || []).map((p: any) => ({
|
||||||
|
value: p.id,
|
||||||
|
label: p.name,
|
||||||
|
color: p.color_code,
|
||||||
|
})),
|
||||||
|
selectedValues: currentPriorities,
|
||||||
|
multiSelect: true,
|
||||||
|
searchable: false,
|
||||||
|
icon: FlagOutlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'assignees',
|
||||||
|
label: t('membersText'),
|
||||||
|
icon: TeamOutlined,
|
||||||
|
multiSelect: true,
|
||||||
|
searchable: true,
|
||||||
|
selectedValues: currentAssignees.filter((m: any) => m.selected && m.id).map((m: any) => m.id || ''),
|
||||||
|
options: currentAssignees.map((assignee: any) => ({
|
||||||
|
id: assignee.id || '',
|
||||||
|
label: assignee.name || '',
|
||||||
|
value: assignee.id || '',
|
||||||
|
avatar: assignee.avatar_url,
|
||||||
|
selected: assignee.selected,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'labels',
|
||||||
|
label: t('labelsText'),
|
||||||
|
icon: TagOutlined,
|
||||||
|
multiSelect: true,
|
||||||
|
searchable: true,
|
||||||
|
selectedValues: currentLabels.filter((l: any) => l.selected && l.id).map((l: any) => l.id || ''),
|
||||||
|
options: currentLabels.map((label: any) => ({
|
||||||
|
id: label.id || '',
|
||||||
|
label: label.name || '',
|
||||||
|
value: label.id || '',
|
||||||
|
color: label.color_code,
|
||||||
|
selected: label.selected,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'groupBy',
|
||||||
|
label: t('groupByText'),
|
||||||
|
icon: GroupOutlined,
|
||||||
|
multiSelect: false,
|
||||||
|
searchable: false,
|
||||||
|
selectedValues: [groupByValue],
|
||||||
|
options: [
|
||||||
|
{ id: 'status', label: t('statusText'), value: 'status' },
|
||||||
|
{ id: 'priority', label: t('priorityText'), value: 'priority' },
|
||||||
|
{ id: 'phase', label: kanbanProject?.phase_label || t('phaseText'), value: 'phase' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Use task management/board state
|
||||||
|
const currentPriorities = currentProjectView === 'list' ? filterData.taskPriorities : filterData.boardPriorities;
|
||||||
|
const currentLabels = currentProjectView === 'list' ? filterData.taskLabels : filterData.boardLabels;
|
||||||
|
const currentAssignees = currentProjectView === 'list' ? filterData.taskAssignees : filterData.boardAssignees;
|
||||||
|
const groupByValue = currentGrouping || 'status';
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'priority',
|
||||||
|
label: 'Priority',
|
||||||
|
options: filterData.priorities.map((p: any) => ({
|
||||||
|
value: p.id,
|
||||||
|
label: p.name,
|
||||||
|
color: p.color_code,
|
||||||
|
})),
|
||||||
|
selectedValues: filterData.selectedPriorities,
|
||||||
|
multiSelect: true,
|
||||||
|
searchable: false,
|
||||||
|
icon: FlagOutlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'assignees',
|
||||||
|
label: t('membersText'),
|
||||||
|
icon: TeamOutlined,
|
||||||
|
multiSelect: true,
|
||||||
|
searchable: true,
|
||||||
|
selectedValues: currentAssignees.filter((m: any) => m.selected && m.id).map((m: any) => m.id || ''),
|
||||||
|
options: currentAssignees.map((assignee: any) => ({
|
||||||
|
id: assignee.id || '',
|
||||||
|
label: assignee.name || '',
|
||||||
|
value: assignee.id || '',
|
||||||
|
avatar: assignee.avatar_url,
|
||||||
|
selected: assignee.selected,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'labels',
|
||||||
|
label: t('labelsText'),
|
||||||
|
icon: TagOutlined,
|
||||||
|
multiSelect: true,
|
||||||
|
searchable: true,
|
||||||
|
selectedValues: currentLabels.filter((l: any) => l.selected && l.id).map((l: any) => l.id || ''),
|
||||||
|
options: currentLabels.map((label: any) => ({
|
||||||
|
id: label.id || '',
|
||||||
|
label: label.name || '',
|
||||||
|
value: label.id || '',
|
||||||
|
color: label.color_code,
|
||||||
|
selected: label.selected,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'groupBy',
|
||||||
|
label: t('groupByText'),
|
||||||
|
icon: GroupOutlined,
|
||||||
|
multiSelect: false,
|
||||||
|
searchable: false,
|
||||||
|
selectedValues: [groupByValue],
|
||||||
|
options: [
|
||||||
|
{ id: 'status', label: t('statusText'), value: 'status' },
|
||||||
|
{ id: 'priority', label: t('priorityText'), value: 'priority' },
|
||||||
|
{ id: 'phase', label: filterData.project?.phase_label || t('phaseText'), value: 'phase' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}, [isBoard, kanbanState, kanbanProject, filterData, currentProjectView, t, currentGrouping]);
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'priority',
|
|
||||||
label: 'Priority',
|
|
||||||
options: filterData.priorities.slice(0, MAX_FILTER_OPTIONS).map((p: any) => ({
|
|
||||||
value: p.id,
|
|
||||||
label: p.name,
|
|
||||||
color: p.color_code,
|
|
||||||
})),
|
|
||||||
selectedValues: filterData.selectedPriorities,
|
|
||||||
multiSelect: true,
|
|
||||||
searchable: false,
|
|
||||||
icon: FlagOutlined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'assignees',
|
|
||||||
label: t('membersText'),
|
|
||||||
icon: TeamOutlined,
|
|
||||||
multiSelect: true,
|
|
||||||
searchable: true,
|
|
||||||
selectedValues: currentAssignees.filter((m: any) => m.selected && m.id).map((m: any) => m.id || ''),
|
|
||||||
options: currentAssignees.slice(0, MAX_FILTER_OPTIONS).map((assignee: any) => ({
|
|
||||||
id: assignee.id || '',
|
|
||||||
label: assignee.name || '',
|
|
||||||
value: assignee.id || '',
|
|
||||||
avatar: assignee.avatar_url,
|
|
||||||
selected: assignee.selected,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'labels',
|
|
||||||
label: t('labelsText'),
|
|
||||||
icon: TagOutlined,
|
|
||||||
multiSelect: true,
|
|
||||||
searchable: true,
|
|
||||||
selectedValues: currentLabels.filter((l: any) => l.selected && l.id).map((l: any) => l.id || ''),
|
|
||||||
options: currentLabels.slice(0, MAX_FILTER_OPTIONS).map((label: any) => ({
|
|
||||||
id: label.id || '',
|
|
||||||
label: label.name || '',
|
|
||||||
value: label.id || '',
|
|
||||||
color: label.color_code,
|
|
||||||
selected: label.selected,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'groupBy',
|
|
||||||
label: t('groupByText'),
|
|
||||||
icon: GroupOutlined,
|
|
||||||
multiSelect: false,
|
|
||||||
searchable: false,
|
|
||||||
selectedValues: [groupByValue],
|
|
||||||
options: [
|
|
||||||
{ id: 'status', label: t('statusText'), value: 'status' },
|
|
||||||
{ id: 'priority', label: t('priorityText'), value: 'priority' },
|
|
||||||
{ id: 'phase', label: filterData.project?.phase_label || t('phaseText'), value: 'phase' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, [
|
|
||||||
filterData,
|
|
||||||
currentProjectView,
|
|
||||||
t,
|
|
||||||
currentGrouping
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter Dropdown Component
|
// Filter Dropdown Component
|
||||||
@@ -640,7 +722,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
const debouncedSearchChangeRef = useRef<((projectId: string, value: 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();
|
const filterSectionsData = useFilterData(position);
|
||||||
|
|
||||||
// Check if data is loaded - memoize this computation
|
// Check if data is loaded - memoize this computation
|
||||||
const isDataLoaded = useMemo(() => {
|
const isDataLoaded = useMemo(() => {
|
||||||
@@ -735,64 +817,83 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
|
|
||||||
const handleSelectionChange = useCallback((sectionId: string, values: string[]) => {
|
const handleSelectionChange = useCallback((sectionId: string, values: string[]) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
if (position === 'board') {
|
||||||
|
// Enhanced Kanban logic
|
||||||
|
if (sectionId === 'groupBy' && values.length > 0) {
|
||||||
|
dispatch(setKanbanGroupBy(values[0] as any));
|
||||||
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sectionId === 'priority') {
|
||||||
|
dispatch(setKanbanPriorities(values));
|
||||||
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sectionId === 'assignees') {
|
||||||
|
dispatch(setKanbanTaskAssignees(
|
||||||
|
// Map to {id, selected, ...}
|
||||||
|
values.map(id => ({ id, selected: true }))
|
||||||
|
));
|
||||||
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sectionId === 'labels') {
|
||||||
|
dispatch(setKanbanLabels(
|
||||||
|
values.map(id => ({ id, selected: true }))
|
||||||
|
));
|
||||||
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ... existing list logic ...
|
||||||
|
if (sectionId === 'groupBy' && values.length > 0) {
|
||||||
|
dispatch(setCurrentGrouping(values[0] as 'status' | 'priority' | 'phase'));
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sectionId === 'priority') {
|
||||||
|
dispatch(setSelectedPriorities(values));
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sectionId === 'assignees') {
|
||||||
|
const updatedAssignees = currentTaskAssignees.map(member => ({
|
||||||
|
...member,
|
||||||
|
selected: values.includes(member.id || '')
|
||||||
|
}));
|
||||||
|
dispatch(setMembers(updatedAssignees));
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sectionId === 'labels') {
|
||||||
|
const updatedLabels = currentTaskLabels.map(label => ({
|
||||||
|
...label,
|
||||||
|
selected: values.includes(label.id || '')
|
||||||
|
}));
|
||||||
|
dispatch(setLabels(updatedLabels));
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent clearing all group by options
|
|
||||||
if (sectionId === 'groupBy' && values.length === 0) {
|
|
||||||
return; // Do nothing
|
|
||||||
}
|
}
|
||||||
|
}, [dispatch, projectId, position, currentTaskAssignees, currentTaskLabels]);
|
||||||
// Update local state first for immediate UI feedback
|
|
||||||
setFilterSections(prev => prev.map(section =>
|
|
||||||
section.id === sectionId
|
|
||||||
? { ...section, selectedValues: values }
|
|
||||||
: section
|
|
||||||
));
|
|
||||||
|
|
||||||
// Use task management slices for groupBy (immediate, no debounce)
|
|
||||||
if (sectionId === 'groupBy' && values.length > 0) {
|
|
||||||
dispatch(setCurrentGrouping(values[0] as 'status' | 'priority' | 'phase'));
|
|
||||||
dispatch(fetchTasksV3(projectId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle priorities (with debounce)
|
|
||||||
if (sectionId === 'priority') {
|
|
||||||
dispatch(setPriorities(values));
|
|
||||||
debouncedFilterChangeRef.current?.(projectId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle assignees (members) (with debounce)
|
|
||||||
if (sectionId === 'assignees') {
|
|
||||||
const updatedAssignees = currentTaskAssignees.map(member => ({
|
|
||||||
...member,
|
|
||||||
selected: values.includes(member.id || '')
|
|
||||||
}));
|
|
||||||
dispatch(setMembers(updatedAssignees));
|
|
||||||
debouncedFilterChangeRef.current?.(projectId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle labels (with debounce)
|
|
||||||
if (sectionId === 'labels') {
|
|
||||||
const updatedLabels = currentTaskLabels.map(label => ({
|
|
||||||
...label,
|
|
||||||
selected: values.includes(label.id || '')
|
|
||||||
}));
|
|
||||||
dispatch(setLabels(updatedLabels));
|
|
||||||
debouncedFilterChangeRef.current?.(projectId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [dispatch, projectId, currentTaskAssignees, currentTaskLabels]);
|
|
||||||
|
|
||||||
const handleSearchChange = useCallback((value: string) => {
|
const handleSearchChange = useCallback((value: string) => {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
|
|
||||||
|
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
||||||
|
if (position === 'board') {
|
||||||
|
dispatch(setKanbanSearch(value));
|
||||||
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
|
} else {
|
||||||
|
// Use debounced search
|
||||||
|
debouncedSearchChangeRef.current?.(projectId, value);
|
||||||
|
}
|
||||||
|
|
||||||
// Use debounced search
|
|
||||||
debouncedSearchChangeRef.current?.(projectId, value);
|
}, [dispatch, projectId, position]);
|
||||||
}, [projectId]);
|
|
||||||
|
|
||||||
const clearAllFilters = useCallback(async () => {
|
const clearAllFilters = useCallback(async () => {
|
||||||
if (!projectId || clearingFilters) return;
|
if (!projectId || clearingFilters) return;
|
||||||
@@ -866,9 +967,13 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
|||||||
|
|
||||||
const toggleArchived = useCallback(() => {
|
const toggleArchived = useCallback(() => {
|
||||||
setShowArchived(!showArchived);
|
setShowArchived(!showArchived);
|
||||||
// TODO: Implement proper archived toggle
|
if (position === 'board') {
|
||||||
console.log('Toggle archived:', !showArchived);
|
dispatch(setKanbanArchived(!showArchived));
|
||||||
}, [showArchived]);
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
|
} else {
|
||||||
|
// ... existing logic ...
|
||||||
|
}
|
||||||
|
}, [dispatch, projectId, position, showArchived]);
|
||||||
|
|
||||||
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}`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user