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:
chamikaJ
2025-06-25 10:48:01 +05:30
parent a25fcf209a
commit cf5919a3a0
7 changed files with 367 additions and 120 deletions

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
},
},

View File

@@ -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 () => {