feat(task-management): implement customizable task list fields and configuration modal
- Added a new slice for managing task list fields, allowing users to toggle visibility and order of fields in the task list. - Introduced a ColumnConfigurationModal for users to configure which fields appear in the dropdown and their order. - Updated ShowFieldsFilterDropdown to integrate the new configuration modal and manage field visibility. - Enhanced task management components to utilize the new field visibility settings, improving the overall user experience and customization options.
This commit is contained in:
@@ -5,7 +5,6 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
CloseOutlined,
|
||||
DownOutlined,
|
||||
TeamOutlined,
|
||||
@@ -28,6 +27,8 @@ 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 { Dropdown, Checkbox, Button, Space } from 'antd';
|
||||
import { toggleField, TaskListField } from '@/features/task-management/taskListFields.slice';
|
||||
|
||||
// Import Redux actions
|
||||
import { fetchTasksV3, setSelectedPriorities } from '@/features/task-management/task-management.slice';
|
||||
@@ -508,6 +509,113 @@ const SearchFilter: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({ themeClasses, isDarkMode }) => {
|
||||
const dispatch = useDispatch();
|
||||
const fieldsRaw = useSelector((state: RootState) => state.taskManagementFields);
|
||||
const fields = Array.isArray(fieldsRaw) ? fieldsRaw : [];
|
||||
const sortedFields = [...fields].sort((a, b) => a.order - b.order);
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown on outside click
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [open]);
|
||||
|
||||
const visibleCount = sortedFields.filter(field => field.visible).length;
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
{/* Trigger Button - matching FilterDropdown style */}
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className={`
|
||||
inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md
|
||||
border transition-all duration-200 ease-in-out
|
||||
${visibleCount > 0
|
||||
? (isDarkMode ? 'bg-blue-600 text-white border-blue-500' : 'bg-blue-50 text-blue-800 border-blue-300 font-semibold')
|
||||
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
||||
}
|
||||
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1
|
||||
${themeClasses.containerBg === 'bg-gray-800' ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'}
|
||||
`}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<EyeOutlined className="w-3.5 h-3.5" />
|
||||
<span>Fields</span>
|
||||
{visibleCount > 0 && (
|
||||
<span className="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full">
|
||||
{visibleCount}
|
||||
</span>
|
||||
)}
|
||||
<DownOutlined
|
||||
className={`w-3.5 h-3.5 transition-transform duration-200 ${open ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Panel - matching FilterDropdown style */}
|
||||
{open && (
|
||||
<div className={`absolute top-full left-0 z-50 mt-1 w-64 ${themeClasses.dropdownBg} rounded-md shadow-lg border ${themeClasses.dropdownBorder}`}>
|
||||
{/* Options List */}
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{sortedFields.length === 0 ? (
|
||||
<div className={`p-2 text-xs text-center ${themeClasses.secondaryText}`}>
|
||||
No fields available
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-0.5">
|
||||
{sortedFields.map((field) => {
|
||||
const isSelected = field.visible;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={field.key}
|
||||
onClick={() => dispatch(toggleField(field.key))}
|
||||
className={`
|
||||
w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded
|
||||
transition-colors duration-150 text-left
|
||||
${isSelected
|
||||
? (isDarkMode ? 'bg-blue-600 text-white' : 'bg-blue-50 text-blue-800 font-semibold')
|
||||
: `${themeClasses.optionText} ${themeClasses.optionHover}`
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Checkbox indicator - matching FilterDropdown style */}
|
||||
<div className={`
|
||||
flex items-center justify-center w-3.5 h-3.5 border rounded
|
||||
${isSelected
|
||||
? 'bg-blue-500 border-blue-500 text-white'
|
||||
: 'border-gray-300 dark:border-gray-600'
|
||||
}
|
||||
`}>
|
||||
{isSelected && <CheckOutlined className="w-2.5 h-2.5" />}
|
||||
</div>
|
||||
|
||||
{/* Label and Count */}
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<span className="truncate">{field.label}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main Component
|
||||
const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
||||
position,
|
||||
@@ -673,7 +781,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
||||
// TODO: Implement column visibility change
|
||||
}, [projectId]);
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className={`${themeClasses.containerBg} border ${themeClasses.containerBorder} rounded-md p-3 shadow-sm ${className}`}>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Left Section - Main Filters */}
|
||||
@@ -748,14 +856,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
||||
)}
|
||||
|
||||
{/* Show Fields Button (for list view) */}
|
||||
{position === 'list' && (
|
||||
<button className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 ${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText} ${
|
||||
isDarkMode ? 'focus:ring-offset-gray-800' : 'focus:ring-offset-white'
|
||||
}`}>
|
||||
<EyeOutlined className="w-3.5 h-3.5" />
|
||||
<span>Fields</span>
|
||||
</button>
|
||||
)}
|
||||
{position === 'list' && <FieldsDropdown themeClasses={themeClasses} isDarkMode={isDarkMode} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -779,7 +880,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
||||
|
||||
{filterSectionsData
|
||||
.filter(section => section.id !== 'groupBy') // <-- skip groupBy
|
||||
.map((section) =>
|
||||
.flatMap((section) =>
|
||||
section.selectedValues.map((value) => {
|
||||
const option = section.options.find(opt => opt.value === value);
|
||||
if (!option) return null;
|
||||
@@ -809,7 +910,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}).filter(Boolean)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { taskManagementSelectors } from '@/features/task-management/task-managem
|
||||
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';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -39,22 +40,7 @@ const GROUP_COLORS = {
|
||||
default: '#d9d9d9',
|
||||
} as const;
|
||||
|
||||
// Column configurations for consistent layout
|
||||
const FIXED_COLUMNS = [
|
||||
{ key: 'drag', label: '', width: 40, fixed: true },
|
||||
{ key: 'select', label: '', width: 40, fixed: true },
|
||||
{ key: 'key', label: 'Key', width: 80, fixed: true },
|
||||
{ key: 'task', label: 'Task', width: 475, fixed: true },
|
||||
];
|
||||
|
||||
const SCROLLABLE_COLUMNS = [
|
||||
{ key: 'progress', label: 'Progress', width: 90 },
|
||||
{ key: 'members', label: 'Members', width: 150 },
|
||||
{ key: 'labels', label: 'Labels', width: 200 },
|
||||
{ key: 'status', label: 'Status', width: 100 },
|
||||
{ key: 'priority', label: 'Priority', width: 100 },
|
||||
{ key: 'timeTracking', label: 'Time Tracking', width: 120 },
|
||||
];
|
||||
|
||||
const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
group,
|
||||
@@ -82,6 +68,54 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
// 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[];
|
||||
|
||||
// Define all possible columns
|
||||
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: 220, 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 visibleFixedColumns = 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);
|
||||
return field?.visible ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}, [taskListFields, allFixedColumns]);
|
||||
|
||||
const visibleScrollableColumns = 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]);
|
||||
|
||||
// Get tasks for this group using memoization for performance
|
||||
const groupTasks = useMemo(() => {
|
||||
return group.taskIds
|
||||
@@ -142,7 +176,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
style={{ ...containerStyle, overflowX: 'unset' }}
|
||||
>
|
||||
<div className="task-group-scroll-wrapper" style={{ overflowX: 'auto', width: '100%' }}>
|
||||
<div style={{ minWidth: FIXED_COLUMNS.reduce((sum, col) => sum + col.width, 0) + SCROLLABLE_COLUMNS.reduce((sum, col) => sum + col.width, 0) }}>
|
||||
<div style={{ minWidth: visibleFixedColumns.reduce((sum, col) => sum + col.width, 0) + visibleScrollableColumns.reduce((sum, col) => sum + col.width, 0) }}>
|
||||
{/* Group Header Row */}
|
||||
<div className="task-group-header">
|
||||
<div className="task-group-header-row">
|
||||
@@ -172,7 +206,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
>
|
||||
<div className="task-group-column-headers-row">
|
||||
<div className="task-table-fixed-columns">
|
||||
{FIXED_COLUMNS.map(col => (
|
||||
{visibleFixedColumns.map(col => (
|
||||
<div
|
||||
key={col.key}
|
||||
className="task-table-cell task-table-header-cell"
|
||||
@@ -183,7 +217,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
))}
|
||||
</div>
|
||||
<div className="task-table-scrollable-columns">
|
||||
{SCROLLABLE_COLUMNS.map(col => (
|
||||
{visibleScrollableColumns.map(col => (
|
||||
<div
|
||||
key={col.key}
|
||||
className="task-table-cell task-table-header-cell"
|
||||
@@ -236,8 +270,8 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
fixedColumns={FIXED_COLUMNS}
|
||||
scrollableColumns={SCROLLABLE_COLUMNS}
|
||||
fixedColumns={visibleFixedColumns}
|
||||
scrollableColumns={visibleScrollableColumns}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { RootState } from '@/app/store';
|
||||
import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, LabelsSelector, Progress, Tag, Tooltip } from '@/components';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import TaskStatusDropdown from './task-status-dropdown';
|
||||
|
||||
interface TaskRowProps {
|
||||
task: Task;
|
||||
@@ -324,7 +325,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
})}
|
||||
</div>
|
||||
{/* Scrollable Columns */}
|
||||
<div className="scrollable-columns-row" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
|
||||
<div className="scrollable-columns-row overflow-visible" style={{ display: 'flex', minWidth: scrollableColumns?.reduce((sum, col) => sum + col.width, 0) || 0 }}>
|
||||
{scrollableColumns?.map(col => {
|
||||
switch (col.key) {
|
||||
case 'progress':
|
||||
@@ -392,14 +393,12 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
||||
);
|
||||
case 'status':
|
||||
return (
|
||||
<div key={col.key} className="flex items-center px-2 border-r" style={{ width: col.width }}>
|
||||
<Tag
|
||||
backgroundColor={getStatusColor(task.status)}
|
||||
color="white"
|
||||
className="text-xs font-medium uppercase"
|
||||
>
|
||||
{task.status}
|
||||
</Tag>
|
||||
<div key={col.key} className="flex items-center px-2 border-r overflow-visible" style={{ width: col.width }}>
|
||||
<TaskStatusDropdown
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'priority':
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
|
||||
interface TaskStatusDropdownProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
isDarkMode?: boolean;
|
||||
}
|
||||
|
||||
const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
||||
task,
|
||||
projectId,
|
||||
isDarkMode = false
|
||||
}) => {
|
||||
const { socket, connected } = useSocket();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const statusList = useAppSelector(state => state.taskStatusReducer.status);
|
||||
|
||||
// Find current status details
|
||||
const currentStatus = useMemo(() => {
|
||||
return statusList.find(status =>
|
||||
status.name?.toLowerCase() === task.status?.toLowerCase() ||
|
||||
status.id === task.status
|
||||
);
|
||||
}, [statusList, task.status]);
|
||||
|
||||
// Handle status change
|
||||
const handleStatusChange = useCallback((statusId: string, statusName: string) => {
|
||||
if (!task.id || !statusId || !connected) return;
|
||||
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
status_id: statusId,
|
||||
parent_task: null, // Assuming top-level tasks for now
|
||||
team_id: projectId, // Using projectId as teamId
|
||||
})
|
||||
);
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||
setIsOpen(false);
|
||||
}, [task.id, connected, socket, projectId]);
|
||||
|
||||
// Calculate dropdown position and handle outside clicks
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (buttonRef.current && buttonRef.current.contains(event.target as Node)) {
|
||||
return; // Don't close if clicking the button
|
||||
}
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen && buttonRef.current) {
|
||||
// Calculate position
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setDropdownPosition({
|
||||
top: rect.bottom + window.scrollY + 4,
|
||||
left: rect.left + window.scrollX,
|
||||
});
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Get status color
|
||||
const getStatusColor = useCallback((status: any) => {
|
||||
if (isDarkMode) {
|
||||
return status?.color_code_dark || status?.color_code || '#6b7280';
|
||||
}
|
||||
return status?.color_code || '#6b7280';
|
||||
}, [isDarkMode]);
|
||||
|
||||
|
||||
|
||||
// Status display name
|
||||
const getStatusDisplayName = useCallback((status: string) => {
|
||||
return status.charAt(0).toUpperCase() + status.slice(1);
|
||||
}, []);
|
||||
|
||||
if (!task.status) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Status Button */}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('Status dropdown clicked, current isOpen:', isOpen);
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className={`
|
||||
inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium
|
||||
transition-all duration-200 hover:opacity-80 border-0 min-w-[70px] justify-center
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: currentStatus ? getStatusColor(currentStatus) : (isDarkMode ? '#6b7280' : '#9ca3af'),
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<span>{currentStatus?.name || getStatusDisplayName(task.status)}</span>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu - Rendered in Portal */}
|
||||
{isOpen && createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className={`
|
||||
fixed min-w-[120px] max-w-[180px]
|
||||
rounded-lg shadow-xl border z-[9999]
|
||||
${isDarkMode
|
||||
? 'bg-gray-800 border-gray-600'
|
||||
: 'bg-white border-gray-200'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
top: dropdownPosition.top,
|
||||
left: dropdownPosition.left,
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<div className="py-1">
|
||||
{statusList.map((status) => (
|
||||
<button
|
||||
key={status.id}
|
||||
onClick={() => handleStatusChange(status.id!, status.name!)}
|
||||
className={`
|
||||
w-full px-3 py-2 text-left text-xs font-medium flex items-center gap-2
|
||||
transition-colors duration-150 rounded-md mx-1
|
||||
${isDarkMode
|
||||
? 'hover:bg-gray-700 text-gray-200'
|
||||
: 'hover:bg-gray-50 text-gray-900'
|
||||
}
|
||||
${(status.name?.toLowerCase() === task.status?.toLowerCase() || status.id === task.status)
|
||||
? (isDarkMode ? 'bg-gray-700' : 'bg-gray-50')
|
||||
: ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Status Pill Preview */}
|
||||
<div
|
||||
className="px-2 py-0.5 rounded-full text-white text-xs min-w-[50px] text-center"
|
||||
style={{ backgroundColor: getStatusColor(status) }}
|
||||
>
|
||||
{status.name}
|
||||
</div>
|
||||
|
||||
{/* Current Status Indicator */}
|
||||
{(status.name?.toLowerCase() === task.status?.toLowerCase() || status.id === task.status) && (
|
||||
<div className="ml-auto">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskStatusDropdown;
|
||||
@@ -6,6 +6,8 @@ import { taskManagementSelectors } from '@/features/task-management/task-managem
|
||||
import { Task } from '@/types/task-management.types';
|
||||
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';
|
||||
|
||||
interface VirtualizedTaskListProps {
|
||||
group: any;
|
||||
@@ -30,6 +32,18 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
}) => {
|
||||
const allTasks = useSelector(taskManagementSelectors.selectAll);
|
||||
|
||||
// Get field visibility from taskListFields slice
|
||||
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
console.log('VirtualizedTaskList Debug:', {
|
||||
taskListFields,
|
||||
fieldsLength: taskListFields?.length,
|
||||
fieldsState: taskListFields?.map(f => ({ key: f.key, visible: f.visible }))
|
||||
});
|
||||
}, [taskListFields]);
|
||||
|
||||
// Get tasks for this group using memoization for performance
|
||||
const groupTasks = useMemo(() => {
|
||||
return group.taskIds
|
||||
@@ -85,21 +99,43 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Define columns array for alignment
|
||||
const columns = [
|
||||
{ key: 'drag', label: '', width: 40, fixed: true },
|
||||
{ key: 'select', label: '', width: 40, fixed: true },
|
||||
{ key: 'key', label: 'KEY', width: 80, fixed: true },
|
||||
{ key: 'task', label: 'TASK', width: 475, fixed: true },
|
||||
{ key: 'progress', label: 'PROGRESS', width: 90 },
|
||||
{ key: 'members', label: 'MEMBERS', width: 150 },
|
||||
{ key: 'labels', label: 'LABELS', width: 200 },
|
||||
{ key: 'status', label: 'STATUS', width: 100 },
|
||||
{ key: 'priority', label: 'PRIORITY', width: 100 },
|
||||
{ key: 'timeTracking', label: 'TIME TRACKING', width: 120 },
|
||||
// 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 fixedColumns = columns.filter(col => col.fixed);
|
||||
const scrollableColumns = columns.filter(col => !col.fixed);
|
||||
|
||||
// Filter columns based on field visibility
|
||||
const visibleColumns = useMemo(() => {
|
||||
const filtered = allColumns.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 false;
|
||||
});
|
||||
|
||||
console.log('Visible columns after filtering:', filtered.map(c => ({ key: c.key, fieldKey: c.fieldKey })));
|
||||
return filtered;
|
||||
}, [taskListFields, allColumns]);
|
||||
|
||||
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;
|
||||
@@ -130,7 +166,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks]);
|
||||
}, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks, fixedColumns, scrollableColumns]);
|
||||
|
||||
return (
|
||||
<div className="virtualized-task-list" style={{ height: height }}>
|
||||
|
||||
Reference in New Issue
Block a user