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:
@@ -83,6 +83,7 @@ import homePageApiService from '@/api/home-page/home-page.api.service';
|
||||
import { projectsApi } from '@/api/projects/projects.v1.api.service';
|
||||
|
||||
import projectViewReducer from '@features/project/project-view-slice';
|
||||
import taskManagementFields from '@features/task-management/taskListFields.slice';
|
||||
|
||||
export const store = configureStore({
|
||||
middleware: getDefaultMiddleware =>
|
||||
@@ -171,6 +172,7 @@ export const store = configureStore({
|
||||
taskManagement: taskManagementReducer,
|
||||
grouping: groupingReducer,
|
||||
taskManagementSelection: selectionReducer,
|
||||
taskManagementFields,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Checkbox, Button, Flex, Typography, Space, Divider, message } from 'antd';
|
||||
import { SettingOutlined, UpOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Configuration interface for column visibility
|
||||
interface ColumnConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
showInDropdown: boolean;
|
||||
order: number;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
interface ColumnConfigurationModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
projectId?: string;
|
||||
onSave: (config: ColumnConfig[]) => void;
|
||||
currentConfig: ColumnConfig[];
|
||||
}
|
||||
|
||||
const ColumnConfigurationModal: React.FC<ColumnConfigurationModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
projectId,
|
||||
onSave,
|
||||
currentConfig,
|
||||
}) => {
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
const [config, setConfig] = useState<ColumnConfig[]>(currentConfig);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setConfig(currentConfig);
|
||||
setHasChanges(false);
|
||||
}, [currentConfig, open]);
|
||||
|
||||
const handleToggleColumn = (key: string) => {
|
||||
const newConfig = config.map(col =>
|
||||
col.key === key ? { ...col, showInDropdown: !col.showInDropdown } : col
|
||||
);
|
||||
setConfig(newConfig);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const moveColumn = (key: string, direction: 'up' | 'down') => {
|
||||
const currentIndex = config.findIndex(col => col.key === key);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
||||
if (newIndex < 0 || newIndex >= config.length) return;
|
||||
|
||||
const newConfig = [...config];
|
||||
[newConfig[currentIndex], newConfig[newIndex]] = [newConfig[newIndex], newConfig[currentIndex]];
|
||||
|
||||
// Update order numbers
|
||||
const updatedConfig = newConfig.map((item, index) => ({
|
||||
...item,
|
||||
order: index + 1,
|
||||
}));
|
||||
|
||||
setConfig(updatedConfig);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(config);
|
||||
setHasChanges(false);
|
||||
message.success('Column configuration saved successfully');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setConfig(currentConfig);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const groupedColumns = config.reduce((groups, column) => {
|
||||
const category = column.category || 'other';
|
||||
if (!groups[category]) {
|
||||
groups[category] = [];
|
||||
}
|
||||
groups[category].push(column);
|
||||
return groups;
|
||||
}, {} as Record<string, ColumnConfig[]>);
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
basic: 'Basic Information',
|
||||
time: 'Time & Estimation',
|
||||
dates: 'Dates',
|
||||
other: 'Other',
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Flex align="center" gap={8}>
|
||||
<SettingOutlined />
|
||||
<span>Configure Show Fields Dropdown</span>
|
||||
</Flex>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={600}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button key="reset" onClick={handleReset} disabled={!hasChanges}>
|
||||
Reset
|
||||
</Button>,
|
||||
<Button key="save" type="primary" onClick={handleSave} disabled={!hasChanges}>
|
||||
Save Configuration
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary">
|
||||
Configure which columns appear in the "Show Fields" dropdown and their order.
|
||||
Use the up/down arrows to reorder columns.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{Object.entries(groupedColumns).map(([category, columns]) => (
|
||||
<div key={category}>
|
||||
<Divider orientation="left">
|
||||
<Typography.Text strong>{categoryLabels[category] || category}</Typography.Text>
|
||||
</Divider>
|
||||
|
||||
{columns.map((column, index) => (
|
||||
<div
|
||||
key={column.key}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
margin: '4px 0',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: '#fafafa',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={column.showInDropdown}
|
||||
onChange={() => handleToggleColumn(column.key)}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Typography.Text>{column.label}</Typography.Text>
|
||||
</Checkbox>
|
||||
|
||||
<Typography.Text type="secondary" style={{ fontSize: '12px', minWidth: '60px' }}>
|
||||
Order: {column.order}
|
||||
</Typography.Text>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UpOutlined />}
|
||||
onClick={() => moveColumn(column.key, 'up')}
|
||||
disabled={index === 0}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownOutlined />}
|
||||
onClick={() => moveColumn(column.key, 'down')}
|
||||
disabled={index === columns.length - 1}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnConfigurationModal;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { MoreOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from 'antd/es/button';
|
||||
import Checkbox from 'antd/es/checkbox';
|
||||
import Dropdown from 'antd/es/dropdown';
|
||||
import Space from 'antd/es/space';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
@@ -14,20 +15,113 @@ import {
|
||||
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import ColumnConfigurationModal from './column-configuration-modal';
|
||||
|
||||
// Configuration interface for column visibility
|
||||
interface ColumnConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
showInDropdown: boolean;
|
||||
order: number;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
// Default column configuration - this can be customized per project or globally
|
||||
const DEFAULT_COLUMN_CONFIG: ColumnConfig[] = [
|
||||
{ key: 'KEY', label: 'Key', showInDropdown: true, order: 1, category: 'basic' },
|
||||
{ key: 'TASK', label: 'Task', showInDropdown: false, order: 2, category: 'basic' }, // Always visible, not in dropdown
|
||||
{ key: 'DESCRIPTION', label: 'Description', showInDropdown: true, order: 3, category: 'basic' },
|
||||
{ key: 'PROGRESS', label: 'Progress', showInDropdown: true, order: 4, category: 'basic' },
|
||||
{ key: 'ASSIGNEES', label: 'Assignees', showInDropdown: true, order: 5, category: 'basic' },
|
||||
{ key: 'LABELS', label: 'Labels', showInDropdown: true, order: 6, category: 'basic' },
|
||||
{ key: 'PHASE', label: 'Phase', showInDropdown: true, order: 7, category: 'basic' },
|
||||
{ key: 'STATUS', label: 'Status', showInDropdown: true, order: 8, category: 'basic' },
|
||||
{ key: 'PRIORITY', label: 'Priority', showInDropdown: true, order: 9, category: 'basic' },
|
||||
{ key: 'TIME_TRACKING', label: 'Time Tracking', showInDropdown: true, order: 10, category: 'time' },
|
||||
{ key: 'ESTIMATION', label: 'Estimation', showInDropdown: true, order: 11, category: 'time' },
|
||||
{ key: 'START_DATE', label: 'Start Date', showInDropdown: true, order: 12, category: 'dates' },
|
||||
{ key: 'DUE_DATE', label: 'Due Date', showInDropdown: true, order: 13, category: 'dates' },
|
||||
{ key: 'DUE_TIME', label: 'Due Time', showInDropdown: true, order: 14, category: 'dates' },
|
||||
{ key: 'COMPLETED_DATE', label: 'Completed Date', showInDropdown: true, order: 15, category: 'dates' },
|
||||
{ key: 'CREATED_DATE', label: 'Created Date', showInDropdown: true, order: 16, category: 'dates' },
|
||||
{ key: 'LAST_UPDATED', label: 'Last Updated', showInDropdown: true, order: 17, category: 'dates' },
|
||||
{ key: 'REPORTER', label: 'Reporter', showInDropdown: true, order: 18, category: 'basic' },
|
||||
];
|
||||
|
||||
// Hook to get column configuration - can be extended to fetch from API or localStorage
|
||||
const useColumnConfig = (projectId?: string): ColumnConfig[] => {
|
||||
// In the future, this could fetch from:
|
||||
// 1. Project-specific settings from API
|
||||
// 2. User preferences from localStorage
|
||||
// 3. Global settings from configuration
|
||||
// 4. Team-level settings
|
||||
|
||||
// For now, return default configuration
|
||||
// You can extend this to load from localStorage or API
|
||||
const storedConfig = localStorage.getItem(`worklenz.column-config.${projectId}`);
|
||||
|
||||
if (storedConfig) {
|
||||
try {
|
||||
return JSON.parse(storedConfig);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse stored column config, using default');
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_COLUMN_CONFIG;
|
||||
};
|
||||
|
||||
// Hook to save column configuration
|
||||
const useSaveColumnConfig = () => {
|
||||
return (projectId: string, config: ColumnConfig[]) => {
|
||||
localStorage.setItem(`worklenz.column-config.${projectId}`, JSON.stringify(config));
|
||||
};
|
||||
};
|
||||
|
||||
const ShowFieldsFilterDropdown = () => {
|
||||
const { socket, connected } = useSocket();
|
||||
// localization
|
||||
const { socket } = useSocket();
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const columnList = useAppSelector(state => state.taskReducer.columns);
|
||||
const { projectId, project } = useAppSelector(state => state.projectReducer);
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>(useColumnConfig(projectId || undefined));
|
||||
const saveColumnConfig = useSaveColumnConfig();
|
||||
|
||||
const visibilityChangableColumnList = columnList.filter(
|
||||
column => column.key !== 'selector' && column.key !== 'TASK' && column.key !== 'customColumn'
|
||||
);
|
||||
// Update config if projectId changes
|
||||
React.useEffect(() => {
|
||||
setColumnConfig(useColumnConfig(projectId || undefined));
|
||||
}, [projectId, configModalOpen]);
|
||||
|
||||
// Filter columns based on configuration
|
||||
const visibilityChangableColumnList = columnList.filter(column => {
|
||||
// Always exclude selector and TASK columns from dropdown
|
||||
if (column.key === 'selector' || column.key === 'TASK') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find configuration for this column
|
||||
const config = columnConfig.find(c => c.key === column.key);
|
||||
|
||||
// If no config found, show custom columns by default
|
||||
if (!config) {
|
||||
return column.custom_column;
|
||||
}
|
||||
|
||||
// Return based on configuration
|
||||
return config.showInDropdown;
|
||||
});
|
||||
|
||||
// Sort columns based on configuration order
|
||||
const sortedColumns = visibilityChangableColumnList.sort((a, b) => {
|
||||
const configA = columnConfig.find(c => c.key === a.key);
|
||||
const configB = columnConfig.find(c => c.key === b.key);
|
||||
|
||||
const orderA = configA?.order ?? 999;
|
||||
const orderB = configB?.order ?? 999;
|
||||
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
|
||||
@@ -51,30 +145,68 @@ const ShowFieldsFilterDropdown = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = visibilityChangableColumnList.map(col => ({
|
||||
key: col.key,
|
||||
label: (
|
||||
<Space>
|
||||
<Checkbox checked={col.pinned} onChange={e => handleColumnVisibilityChange(col)}>
|
||||
{col.key === 'PHASE' ? project?.phase_label : ''}
|
||||
{col.key !== 'PHASE' &&
|
||||
(col.custom_column
|
||||
? col.name
|
||||
: t(`${col.key?.replace('_', '').toLowerCase() + 'Text'}`))}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
),
|
||||
}));
|
||||
const handleConfigSave = (newConfig: ColumnConfig[]) => {
|
||||
setColumnConfig(newConfig);
|
||||
if (projectId) saveColumnConfig(projectId, newConfig);
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
...sortedColumns.map(col => ({
|
||||
key: col.key || '',
|
||||
type: 'item' as const,
|
||||
label: (
|
||||
<Space>
|
||||
<Checkbox checked={col.pinned} onChange={e => handleColumnVisibilityChange(col)}>
|
||||
{col.key === 'PHASE' ? project?.phase_label : ''}
|
||||
{col.key !== 'PHASE' &&
|
||||
(col.custom_column
|
||||
? col.name
|
||||
: t(`${col.key?.replace('_', '').toLowerCase() + 'Text'}`))}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
),
|
||||
})),
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
key: 'configure',
|
||||
type: 'item' as const,
|
||||
label: (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setConfigModalOpen(true);
|
||||
}}
|
||||
style={{ width: '100%', textAlign: 'left' }}
|
||||
>
|
||||
Configure Fields
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems,
|
||||
style: { maxHeight: '400px', overflowY: 'auto' },
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button icon={<MoreOutlined />}>{t('showFieldsText')}</Button>
|
||||
</Dropdown>
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems,
|
||||
style: { maxHeight: '400px', overflowY: 'auto' },
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button icon={<MoreOutlined />}>{t('showFieldsText')}</Button>
|
||||
</Dropdown>
|
||||
<ColumnConfigurationModal
|
||||
open={configModalOpen}
|
||||
onClose={() => setConfigModalOpen(false)}
|
||||
projectId={projectId || undefined}
|
||||
onSave={handleConfigSave}
|
||||
currentConfig={columnConfig}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export interface TaskListField {
|
||||
key: string;
|
||||
label: string;
|
||||
visible: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
const DEFAULT_FIELDS: TaskListField[] = [
|
||||
{ key: 'KEY', label: 'Key', visible: false, order: 1 },
|
||||
{ key: 'DESCRIPTION', label: 'Description', visible: false, order: 2 },
|
||||
{ key: 'PROGRESS', label: 'Progress', visible: true, order: 3 },
|
||||
{ key: 'ASSIGNEES', label: 'Assignees', visible: true, order: 4 },
|
||||
{ key: 'LABELS', label: 'Labels', visible: true, order: 5 },
|
||||
{ key: 'PHASE', label: 'Phase', visible: true, order: 6 },
|
||||
{ key: 'STATUS', label: 'Status', visible: true, order: 7 },
|
||||
{ key: 'PRIORITY', label: 'Priority', visible: true, order: 8 },
|
||||
{ key: 'TIME_TRACKING', label: 'Time Tracking', visible: true, order: 9 },
|
||||
{ key: 'ESTIMATION', label: 'Estimation', visible: false, order: 10 },
|
||||
{ key: 'START_DATE', label: 'Start Date', visible: false, order: 11 },
|
||||
{ key: 'DUE_DATE', label: 'Due Date', visible: true, order: 12 },
|
||||
{ key: 'DUE_TIME', label: 'Due Time', visible: false, order: 13 },
|
||||
{ key: 'COMPLETED_DATE', label: 'Completed Date', visible: false, order: 14 },
|
||||
{ key: 'CREATED_DATE', label: 'Created Date', visible: false, order: 15 },
|
||||
{ key: 'LAST_UPDATED', label: 'Last Updated', visible: false, order: 16 },
|
||||
{ key: 'REPORTER', label: 'Reporter', visible: false, order: 17 },
|
||||
];
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'worklenz.taskManagement.fields';
|
||||
|
||||
function loadFields(): TaskListField[] {
|
||||
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
console.log('Loading fields from localStorage:', stored);
|
||||
|
||||
// Temporarily force defaults to debug
|
||||
console.log('FORCING DEFAULT FIELDS FOR DEBUGGING');
|
||||
return DEFAULT_FIELDS;
|
||||
|
||||
/* Commented out for debugging
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
console.log('Parsed fields from localStorage:', parsed);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse stored fields, using defaults:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Using default fields:', DEFAULT_FIELDS);
|
||||
return DEFAULT_FIELDS;
|
||||
*/
|
||||
}
|
||||
|
||||
function saveFields(fields: TaskListField[]) {
|
||||
console.log('Saving fields to localStorage:', fields);
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fields));
|
||||
}
|
||||
|
||||
const initialState: TaskListField[] = loadFields();
|
||||
console.log('TaskListFields slice initial state:', initialState);
|
||||
|
||||
const taskListFieldsSlice = createSlice({
|
||||
name: 'taskManagementFields',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleField(state, action: PayloadAction<string>) {
|
||||
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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { toggleField, setFields, resetFields } = taskListFieldsSlice.actions;
|
||||
|
||||
// Utility function to force reset fields (can be called from browser console)
|
||||
export const forceResetFields = () => {
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
||||
console.log('Cleared localStorage and reset fields to defaults');
|
||||
return DEFAULT_FIELDS;
|
||||
};
|
||||
|
||||
// Make it available globally for debugging
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).forceResetTaskFields = forceResetFields;
|
||||
}
|
||||
|
||||
export default taskListFieldsSlice.reducer;
|
||||
Reference in New Issue
Block a user