feat(task-management): enhance Checkbox component and task selection functionality
- Added `indeterminate` state to Checkbox component for better visual representation of partial selections. - Updated TaskGroup and VirtualizedTaskList components to utilize the new Checkbox features, allowing for group selection with indeterminate states. - Implemented custom debounce function for saving task fields to localStorage, improving performance during user interactions. - Enhanced task row styling for better visibility and user experience, particularly in dark mode.
This commit is contained in:
@@ -6,6 +6,7 @@ interface CheckboxProps {
|
||||
isDarkMode?: boolean;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<CheckboxProps> = ({
|
||||
@@ -13,7 +14,8 @@ const Checkbox: React.FC<CheckboxProps> = ({
|
||||
onChange,
|
||||
isDarkMode = false,
|
||||
className = '',
|
||||
disabled = false
|
||||
disabled = false,
|
||||
indeterminate = false
|
||||
}) => {
|
||||
return (
|
||||
<label className={`inline-flex items-center cursor-pointer ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}>
|
||||
@@ -25,15 +27,20 @@ const Checkbox: React.FC<CheckboxProps> = ({
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 border-2 rounded transition-all duration-200 flex items-center justify-center ${
|
||||
checked
|
||||
checked || indeterminate
|
||||
? `${isDarkMode ? 'bg-blue-600 border-blue-600' : 'bg-blue-500 border-blue-500'}`
|
||||
: `${isDarkMode ? 'bg-gray-800 border-gray-600 hover:border-gray-500' : 'bg-white border-gray-300 hover:border-gray-400'}`
|
||||
} ${disabled ? 'cursor-not-allowed' : ''}`}>
|
||||
{checked && (
|
||||
{checked && !indeterminate && (
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
{indeterminate && (
|
||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -509,6 +509,17 @@ 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 FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({ themeClasses, isDarkMode }) => {
|
||||
const dispatch = useDispatch();
|
||||
const fieldsRaw = useSelector((state: RootState) => state.taskManagementFields);
|
||||
@@ -518,6 +529,17 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Debounced save to localStorage using custom debounce
|
||||
const debouncedSaveFields = useMemo(() => debounce((fieldsToSave: typeof fields) => {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fieldsToSave));
|
||||
}, 300), []);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedSaveFields(fields);
|
||||
// Cleanup debounce on unmount
|
||||
return () => { /* no cancel needed for custom debounce */ };
|
||||
}, [fields, debouncedSaveFields]);
|
||||
|
||||
// Close dropdown on outside click
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RootState } from '@/app/store';
|
||||
import TaskRow from './task-row';
|
||||
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
||||
import { TaskListField } from '@/features/task-management/taskListFields.slice';
|
||||
import { Checkbox } from '@/components';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -136,6 +137,19 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
};
|
||||
}, [groupTasks]);
|
||||
|
||||
// Calculate selection state for the group checkbox
|
||||
const { isAllSelected, isIndeterminate } = useMemo(() => {
|
||||
if (groupTasks.length === 0) {
|
||||
return { isAllSelected: false, isIndeterminate: false };
|
||||
}
|
||||
|
||||
const selectedTasksInGroup = groupTasks.filter(task => selectedTaskIds.includes(task.id));
|
||||
const isAllSelected = selectedTasksInGroup.length === groupTasks.length;
|
||||
const isIndeterminate = selectedTasksInGroup.length > 0 && selectedTasksInGroup.length < groupTasks.length;
|
||||
|
||||
return { isAllSelected, isIndeterminate };
|
||||
}, [groupTasks, selectedTaskIds]);
|
||||
|
||||
// Get group color based on grouping type - memoized
|
||||
const groupColor = useMemo(() => {
|
||||
if (group.color) return group.color;
|
||||
@@ -163,6 +177,25 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
onAddTask?.(group.id);
|
||||
}, [onAddTask, group.id]);
|
||||
|
||||
// Handle select all tasks in group
|
||||
const handleSelectAllInGroup = useCallback((checked: boolean) => {
|
||||
if (checked) {
|
||||
// Select all tasks in the group
|
||||
groupTasks.forEach(task => {
|
||||
if (!selectedTaskIds.includes(task.id)) {
|
||||
onSelectTask?.(task.id, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Deselect all tasks in the group
|
||||
groupTasks.forEach(task => {
|
||||
if (selectedTaskIds.includes(task.id)) {
|
||||
onSelectTask?.(task.id, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [groupTasks, selectedTaskIds, onSelectTask]);
|
||||
|
||||
// Memoized style object
|
||||
const containerStyle = useMemo(() => ({
|
||||
backgroundColor: isOver
|
||||
@@ -212,7 +245,18 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
className="task-table-cell task-table-header-cell"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
{col.label && <Text className="column-header-text">{col.label}</Text>}
|
||||
{col.key === 'select' ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onChange={handleSelectAllInGroup}
|
||||
isDarkMode={isDarkMode}
|
||||
indeterminate={isIndeterminate}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
col.label && <Text className="column-header-text">{col.label}</Text>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -451,6 +495,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||
border-bottom: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
height: 40px;
|
||||
@@ -465,6 +510,49 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Add row border styling for task rows */
|
||||
.task-group-tasks > div {
|
||||
border-bottom: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.task-group-tasks > div:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Ensure fixed columns also have bottom borders */
|
||||
.fixed-columns-row > div {
|
||||
border-bottom: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.scrollable-columns-row > div {
|
||||
border-bottom: 1px solid var(--task-border-secondary, #f0f0f0);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark mode border adjustments */
|
||||
.dark .task-table-cell,
|
||||
[data-theme="dark"] .task-table-cell {
|
||||
border-right-color: var(--task-border-secondary, #374151);
|
||||
border-bottom-color: var(--task-border-secondary, #374151);
|
||||
}
|
||||
|
||||
.dark .task-group-tasks > div,
|
||||
[data-theme="dark"] .task-group-tasks > div {
|
||||
border-bottom-color: var(--task-border-secondary, #374151);
|
||||
}
|
||||
|
||||
.dark .fixed-columns-row > div,
|
||||
[data-theme="dark"] .fixed-columns-row > div {
|
||||
border-bottom-color: var(--task-border-secondary, #374151);
|
||||
}
|
||||
|
||||
.dark .scrollable-columns-row > div,
|
||||
[data-theme="dark"] .scrollable-columns-row > div {
|
||||
border-bottom-color: var(--task-border-secondary, #374151);
|
||||
}
|
||||
|
||||
.drag-over {
|
||||
background-color: var(--task-drag-over-bg, #f0f8ff) !important;
|
||||
border-color: var(--task-drag-over-border, #40a9ff) !important;
|
||||
|
||||
@@ -65,7 +65,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
// Edit task name state
|
||||
const [editTaskName, setEditTaskName] = useState(false);
|
||||
const [taskName, setTaskName] = useState(task.title || '');
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
@@ -108,7 +108,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
|
||||
// Handle task name save
|
||||
const handleTaskNameSave = useCallback(() => {
|
||||
const newTaskName = inputRef.current?.input?.value;
|
||||
const newTaskName = inputRef.current?.value;
|
||||
if (newTaskName?.trim() !== '' && connected && newTaskName !== task.title) {
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_NAME_CHANGE.toString(),
|
||||
@@ -284,12 +284,41 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
</div>
|
||||
);
|
||||
case 'task':
|
||||
// Compute the style for the cell
|
||||
const cellStyle = editTaskName
|
||||
? { width: col.width, border: '1px solid #1890ff', background: isDarkMode ? '#232b3a' : '#f0f7ff', transition: 'border 0.2s' }
|
||||
: { width: col.width };
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2" style={{ width: col.width }}>
|
||||
<div
|
||||
key={col.key}
|
||||
className={`flex items-center px-2${editTaskName ? ' task-name-edit-active' : ''}`}
|
||||
style={cellStyle}
|
||||
>
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
|
||||
<div className="flex items-center gap-2 h-5 overflow-hidden">
|
||||
<div ref={wrapperRef} className="flex-1 min-w-0">
|
||||
{!editTaskName ? (
|
||||
{editTaskName ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="task-name-input"
|
||||
value={taskName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTaskName(e.target.value)}
|
||||
onBlur={handleTaskNameSave}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleTaskNameSave();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
width: '100%',
|
||||
color: isDarkMode ? '#ffffff' : '#262626'
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: task.title }}
|
||||
onClick={() => setEditTaskName(true)}
|
||||
@@ -298,21 +327,6 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
>
|
||||
{task.title}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
variant="borderless"
|
||||
value={taskName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTaskName(e.target.value)}
|
||||
onPressEnter={handleTaskNameSave}
|
||||
className={`${isDarkMode ? 'bg-gray-800 text-gray-100 border-gray-600' : 'bg-white text-gray-900 border-gray-300'}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '2px 4px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -459,4 +473,50 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
|
||||
TaskRow.displayName = 'TaskRow';
|
||||
|
||||
// Add styles for better border visibility
|
||||
const taskRowStyles = `
|
||||
.task-row-container {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .task-row-container,
|
||||
[data-theme="dark"] .task-row-container {
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
.task-row-container:hover {
|
||||
border-bottom-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.dark .task-row-container:hover,
|
||||
[data-theme="dark"] .task-row-container:hover {
|
||||
border-bottom-color: #4b5563;
|
||||
}
|
||||
|
||||
.fixed-columns-row > div,
|
||||
.scrollable-columns-row > div {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .fixed-columns-row > div,
|
||||
.dark .scrollable-columns-row > div,
|
||||
[data-theme="dark"] .fixed-columns-row > div,
|
||||
[data-theme="dark"] .scrollable-columns-row > div {
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
`;
|
||||
|
||||
// Inject styles
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleId = 'task-row-styles';
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = taskRowStyles;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
export default TaskRow;
|
||||
@@ -8,6 +8,7 @@ import TaskRow from './task-row';
|
||||
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
||||
import { RootState } from '@/app/store';
|
||||
import { TaskListField } from '@/features/task-management/taskListFields.slice';
|
||||
import { Checkbox } from '@/components';
|
||||
|
||||
interface VirtualizedTaskListProps {
|
||||
group: any;
|
||||
@@ -32,6 +33,9 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
}) => {
|
||||
const allTasks = useSelector(taskManagementSelectors.selectAll);
|
||||
|
||||
// Get theme from Redux store
|
||||
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||
|
||||
// Get field visibility from taskListFields slice
|
||||
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
||||
|
||||
@@ -51,98 +55,99 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
.filter((task: Task | undefined): task is Task => task !== undefined);
|
||||
}, [group.taskIds, allTasks]);
|
||||
|
||||
// Calculate selection state for the group checkbox
|
||||
const { isAllSelected, isIndeterminate } = useMemo(() => {
|
||||
if (groupTasks.length === 0) {
|
||||
return { isAllSelected: false, isIndeterminate: false };
|
||||
}
|
||||
|
||||
const selectedTasksInGroup = groupTasks.filter(task => selectedTaskIds.includes(task.id));
|
||||
const isAllSelected = selectedTasksInGroup.length === groupTasks.length;
|
||||
const isIndeterminate = selectedTasksInGroup.length > 0 && selectedTasksInGroup.length < groupTasks.length;
|
||||
|
||||
return { isAllSelected, isIndeterminate };
|
||||
}, [groupTasks, selectedTaskIds]);
|
||||
|
||||
// Handle select all tasks in group
|
||||
const handleSelectAllInGroup = useCallback((checked: boolean) => {
|
||||
if (checked) {
|
||||
// Select all tasks in the group
|
||||
groupTasks.forEach(task => {
|
||||
if (!selectedTaskIds.includes(task.id)) {
|
||||
onSelectTask(task.id, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Deselect all tasks in the group
|
||||
groupTasks.forEach(task => {
|
||||
if (selectedTaskIds.includes(task.id)) {
|
||||
onSelectTask(task.id, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [groupTasks, selectedTaskIds, onSelectTask]);
|
||||
|
||||
const TASK_ROW_HEIGHT = 40;
|
||||
const HEADER_HEIGHT = 40;
|
||||
const COLUMN_HEADER_HEIGHT = 40;
|
||||
const ADD_TASK_ROW_HEIGHT = 40;
|
||||
|
||||
// Calculate the actual height needed for the virtualized list
|
||||
const actualContentHeight = HEADER_HEIGHT + COLUMN_HEADER_HEIGHT + (groupTasks.length * TASK_ROW_HEIGHT);
|
||||
const listHeight = Math.min(height - 40, actualContentHeight);
|
||||
|
||||
// Calculate item count - only include actual content
|
||||
const getItemCount = () => {
|
||||
return groupTasks.length + 2; // +2 for header and column headers only
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
console.log('VirtualizedTaskList:', {
|
||||
groupId: group.id,
|
||||
groupTasks: groupTasks.length,
|
||||
height,
|
||||
listHeight,
|
||||
itemCount: getItemCount(),
|
||||
isVirtualized: groupTasks.length > 10, // Show if virtualization should be active
|
||||
minHeight: 300,
|
||||
maxHeight: 600
|
||||
});
|
||||
}, [group.id, groupTasks.length, height, listHeight]);
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const headerScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Synchronize header scroll with body scroll
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (headerScrollRef.current && scrollContainerRef.current) {
|
||||
headerScrollRef.current.scrollLeft = scrollContainerRef.current.scrollLeft;
|
||||
}
|
||||
};
|
||||
const scrollDiv = scrollContainerRef.current;
|
||||
if (scrollDiv) {
|
||||
scrollDiv.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
return () => {
|
||||
if (scrollDiv) {
|
||||
scrollDiv.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
// Calculate dynamic height for the group
|
||||
const taskRowsHeight = groupTasks.length * TASK_ROW_HEIGHT;
|
||||
const groupHeight = HEADER_HEIGHT + COLUMN_HEADER_HEIGHT + taskRowsHeight + ADD_TASK_ROW_HEIGHT;
|
||||
|
||||
// Define all possible columns
|
||||
const allColumns = [
|
||||
{ key: 'drag', label: '', width: 40, fixed: true, alwaysVisible: true },
|
||||
{ key: 'select', label: '', width: 40, fixed: true, alwaysVisible: true },
|
||||
{ key: 'key', label: 'KEY', width: 80, fixed: true, fieldKey: 'KEY' },
|
||||
{ key: 'task', label: 'TASK', width: 475, fixed: true, alwaysVisible: true },
|
||||
{ key: 'progress', label: 'PROGRESS', width: 90, fieldKey: 'PROGRESS' },
|
||||
{ key: 'members', label: 'MEMBERS', width: 150, fieldKey: 'ASSIGNEES' },
|
||||
{ key: 'labels', label: 'LABELS', width: 200, fieldKey: 'LABELS' },
|
||||
{ key: 'status', label: 'STATUS', width: 100, fieldKey: 'STATUS' },
|
||||
{ key: 'priority', label: 'PRIORITY', width: 100, fieldKey: 'PRIORITY' },
|
||||
{ key: 'timeTracking', label: 'TIME TRACKING', width: 120, fieldKey: 'TIME_TRACKING' },
|
||||
const allFixedColumns = [
|
||||
{ key: 'drag', label: '', width: 40, alwaysVisible: true },
|
||||
{ key: 'select', label: '', width: 40, alwaysVisible: true },
|
||||
{ key: 'key', label: 'KEY', width: 80, fieldKey: 'KEY' },
|
||||
{ key: 'task', label: 'TASK', width: 474, alwaysVisible: true },
|
||||
];
|
||||
|
||||
const allScrollableColumns = [
|
||||
{ key: 'progress', label: 'Progress', width: 90, fieldKey: 'PROGRESS' },
|
||||
{ key: 'members', label: 'Members', width: 150, fieldKey: 'ASSIGNEES' },
|
||||
{ key: 'labels', label: 'Labels', width: 200, fieldKey: 'LABELS' },
|
||||
{ key: 'status', label: 'Status', width: 100, fieldKey: 'STATUS' },
|
||||
{ key: 'priority', label: 'Priority', width: 100, fieldKey: 'PRIORITY' },
|
||||
{ key: 'timeTracking', label: 'Time Tracking', width: 120, fieldKey: 'TIME_TRACKING' },
|
||||
];
|
||||
|
||||
// Filter columns based on field visibility
|
||||
const visibleColumns = useMemo(() => {
|
||||
const filtered = allColumns.filter(col => {
|
||||
const fixedColumns = useMemo(() => {
|
||||
return allFixedColumns.filter(col => {
|
||||
// Always show columns marked as alwaysVisible
|
||||
if (col.alwaysVisible) return true;
|
||||
|
||||
// For other columns, check field visibility
|
||||
if (col.fieldKey) {
|
||||
const field = taskListFields.find(f => f.key === col.fieldKey);
|
||||
const isVisible = field?.visible ?? false;
|
||||
console.log(`Column ${col.key} (fieldKey: ${col.fieldKey}):`, { field, isVisible });
|
||||
return isVisible;
|
||||
return field?.visible ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}, [taskListFields, allFixedColumns]);
|
||||
|
||||
console.log('Visible columns after filtering:', filtered.map(c => ({ key: c.key, fieldKey: c.fieldKey })));
|
||||
return filtered;
|
||||
}, [taskListFields, allColumns]);
|
||||
const scrollableColumns = useMemo(() => {
|
||||
return allScrollableColumns.filter(col => {
|
||||
// For scrollable columns, check field visibility
|
||||
if (col.fieldKey) {
|
||||
const field = taskListFields.find(f => f.key === col.fieldKey);
|
||||
return field?.visible ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}, [taskListFields, allScrollableColumns]);
|
||||
|
||||
const fixedColumns = visibleColumns.filter(col => col.fixed);
|
||||
const scrollableColumns = visibleColumns.filter(col => !col.fixed);
|
||||
const fixedWidth = fixedColumns.reduce((sum, col) => sum + col.width, 0);
|
||||
const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0);
|
||||
const totalTableWidth = fixedWidth + scrollableWidth;
|
||||
|
||||
// Row renderer for virtualization (remove header/column header rows)
|
||||
// Row renderer for virtualization (only task rows)
|
||||
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
|
||||
const task = groupTasks[index];
|
||||
const task: Task | undefined = groupTasks[index];
|
||||
if (!task) return null;
|
||||
return (
|
||||
<div
|
||||
@@ -168,10 +173,34 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
);
|
||||
}, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks, fixedColumns, scrollableColumns]);
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const headerScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Synchronize header scroll with body scroll
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (headerScrollRef.current && scrollContainerRef.current) {
|
||||
headerScrollRef.current.scrollLeft = scrollContainerRef.current.scrollLeft;
|
||||
}
|
||||
};
|
||||
const scrollDiv = scrollContainerRef.current;
|
||||
if (scrollDiv) {
|
||||
scrollDiv.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
return () => {
|
||||
if (scrollDiv) {
|
||||
scrollDiv.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const VIRTUALIZATION_THRESHOLD = 20;
|
||||
const shouldVirtualize = groupTasks.length > VIRTUALIZATION_THRESHOLD;
|
||||
|
||||
return (
|
||||
<div className="virtualized-task-list" style={{ height: height }}>
|
||||
<div className="virtualized-task-list" style={{ height: groupHeight }}>
|
||||
{/* Group Header */}
|
||||
<div className="task-group-header">
|
||||
<div className="task-group-header" style={{ height: HEADER_HEIGHT }}>
|
||||
<div className="task-group-header-row">
|
||||
<div
|
||||
className="task-group-header-content"
|
||||
@@ -190,7 +219,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
<div
|
||||
className="task-group-column-headers-scroll"
|
||||
ref={headerScrollRef}
|
||||
style={{ overflowX: 'auto', overflowY: 'hidden' }}
|
||||
style={{ overflowX: 'auto', overflowY: 'hidden', height: COLUMN_HEADER_HEIGHT }}
|
||||
>
|
||||
<div
|
||||
className="task-group-column-headers"
|
||||
@@ -203,7 +232,18 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
className="task-table-cell task-table-header-cell fixed-column"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
<span className="column-header-text">{col.label}</span>
|
||||
{col.key === 'select' ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onChange={handleSelectAllInGroup}
|
||||
isDarkMode={isDarkMode}
|
||||
indeterminate={isIndeterminate}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="column-header-text">{col.label}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -220,30 +260,62 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Scrollable List */}
|
||||
{/* Scrollable List - only task rows */}
|
||||
<div
|
||||
className="task-list-scroll-container"
|
||||
ref={scrollContainerRef}
|
||||
style={{ overflowX: 'auto', overflowY: 'hidden', width: '100%', minWidth: totalTableWidth }}
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
minWidth: totalTableWidth,
|
||||
height: groupTasks.length > 0 ? taskRowsHeight : 'auto',
|
||||
}}
|
||||
>
|
||||
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
||||
<List
|
||||
height={listHeight}
|
||||
width={width}
|
||||
itemCount={groupTasks.length}
|
||||
itemSize={TASK_ROW_HEIGHT}
|
||||
overscanCount={15}
|
||||
className="react-window-list"
|
||||
style={{ minWidth: totalTableWidth }}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
{shouldVirtualize ? (
|
||||
<List
|
||||
height={taskRowsHeight}
|
||||
width={width}
|
||||
itemCount={groupTasks.length}
|
||||
itemSize={TASK_ROW_HEIGHT}
|
||||
overscanCount={50}
|
||||
className="react-window-list"
|
||||
style={{ minWidth: totalTableWidth }}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
) : (
|
||||
groupTasks.map((task: Task, index: number) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="task-row-container"
|
||||
style={{
|
||||
height: TASK_ROW_HEIGHT,
|
||||
'--group-color': group.color || '#f0f0f0',
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<TaskRow
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id)}
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
fixedColumns={fixedColumns}
|
||||
scrollableColumns={scrollableColumns}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</SortableContext>
|
||||
</div>
|
||||
{/* Add Task Row - Always show at the bottom */}
|
||||
<div
|
||||
className="task-group-add-task"
|
||||
style={{ borderLeft: `4px solid ${group.color || '#f0f0f0'}` }}
|
||||
style={{ borderLeft: `4px solid ${group.color || '#f0f0f0'}`, height: ADD_TASK_ROW_HEIGHT }}
|
||||
>
|
||||
<AddTaskListRow groupId={group.id} />
|
||||
</div>
|
||||
@@ -275,7 +347,10 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
min-width: 1200px;
|
||||
}
|
||||
.task-list-scroll-container {
|
||||
width: 100%;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
.task-list-scroll-container::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
.react-window-list {
|
||||
outline: none;
|
||||
@@ -312,7 +387,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
/* Task group header styles */
|
||||
.task-group-header-row {
|
||||
display: inline-flex;
|
||||
height: auto;
|
||||
height: inherit;
|
||||
max-height: none;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
|
||||
@@ -69,15 +69,12 @@ const taskListFieldsSlice = createSlice({
|
||||
const field = state.find(f => f.key === action.payload);
|
||||
if (field) {
|
||||
field.visible = !field.visible;
|
||||
saveFields(state);
|
||||
}
|
||||
},
|
||||
setFields(state, action: PayloadAction<TaskListField[]>) {
|
||||
saveFields(action.payload);
|
||||
return action.payload;
|
||||
},
|
||||
resetFields() {
|
||||
saveFields(DEFAULT_FIELDS);
|
||||
return DEFAULT_FIELDS;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
|
||||
@@ -15,13 +15,11 @@ import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||
export const useFilterDataLoader = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { priorities } = useAppSelector(state => ({
|
||||
priorities: state.priorityReducer.priorities,
|
||||
}));
|
||||
// Memoize the priorities selector to prevent unnecessary re-renders
|
||||
const priorities = useAppSelector(state => state.priorityReducer.priorities);
|
||||
|
||||
const { projectId } = useAppSelector(state => ({
|
||||
projectId: state.projectReducer.projectId,
|
||||
}));
|
||||
// Memoize the projectId selector to prevent unnecessary re-renders
|
||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
|
||||
// Load filter data asynchronously
|
||||
const loadFilterData = useCallback(async () => {
|
||||
|
||||
Reference in New Issue
Block a user