From a25fcf209af3f216dd8ca111d5bce67ff939af5a Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Wed, 25 Jun 2025 07:57:53 +0530 Subject: [PATCH] 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. --- worklenz-frontend/src/app/store.ts | 2 + .../column-configuration-modal.tsx | 179 ++++++++++++++++ .../show-fields-filter-dropdown.tsx | 194 +++++++++++++++--- .../task-management/improved-task-filters.tsx | 125 +++++++++-- .../components/task-management/task-group.tsx | 74 +++++-- .../components/task-management/task-row.tsx | 17 +- .../task-management/task-status-dropdown.tsx | 187 +++++++++++++++++ .../task-management/virtualized-task-list.tsx | 66 ++++-- .../task-management/taskListFields.slice.ts | 100 +++++++++ 9 files changed, 857 insertions(+), 87 deletions(-) create mode 100644 worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx create mode 100644 worklenz-frontend/src/components/task-management/task-status-dropdown.tsx create mode 100644 worklenz-frontend/src/features/task-management/taskListFields.slice.ts diff --git a/worklenz-frontend/src/app/store.ts b/worklenz-frontend/src/app/store.ts index 8f9b594f..573333d7 100644 --- a/worklenz-frontend/src/app/store.ts +++ b/worklenz-frontend/src/app/store.ts @@ -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, }, }); diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx new file mode 100644 index 00000000..d5ed3660 --- /dev/null +++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx @@ -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 = ({ + open, + onClose, + projectId, + onSave, + currentConfig, +}) => { + const { t } = useTranslation('task-list-filters'); + const [config, setConfig] = useState(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); + + const categoryLabels: Record = { + basic: 'Basic Information', + time: 'Time & Estimation', + dates: 'Dates', + other: 'Other', + }; + + return ( + + + Configure Show Fields Dropdown + + } + open={open} + onCancel={onClose} + width={600} + footer={[ + , + , + , + ]} + > +
+ + Configure which columns appear in the "Show Fields" dropdown and their order. + Use the up/down arrows to reorder columns. + +
+ + {Object.entries(groupedColumns).map(([category, columns]) => ( +
+ + {categoryLabels[category] || category} + + + {columns.map((column, index) => ( +
+ handleToggleColumn(column.key)} + style={{ flex: 1 }} + > + {column.label} + + + + Order: {column.order} + + + +
+ ))} +
+ ))} +
+ ); +}; + +export default ColumnConfigurationModal; \ No newline at end of file diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown.tsx index 4bc338a5..8ad5a6bc 100644 --- a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown.tsx +++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown.tsx @@ -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(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: ( - - handleColumnVisibilityChange(col)}> - {col.key === 'PHASE' ? project?.phase_label : ''} - {col.key !== 'PHASE' && - (col.custom_column - ? col.name - : t(`${col.key?.replace('_', '').toLowerCase() + 'Text'}`))} - - - ), - })); + const handleConfigSave = (newConfig: ColumnConfig[]) => { + setColumnConfig(newConfig); + if (projectId) saveColumnConfig(projectId, newConfig); + }; + + const menuItems = [ + ...sortedColumns.map(col => ({ + key: col.key || '', + type: 'item' as const, + label: ( + + handleColumnVisibilityChange(col)}> + {col.key === 'PHASE' ? project?.phase_label : ''} + {col.key !== 'PHASE' && + (col.custom_column + ? col.name + : t(`${col.key?.replace('_', '').toLowerCase() + 'Text'}`))} + + + ), + })), + { + type: 'divider' as const, + }, + { + key: 'configure', + type: 'item' as const, + label: ( + + ), + }, + ]; + return ( - - - + <> + + + + setConfigModalOpen(false)} + projectId={projectId || undefined} + onSave={handleConfigSave} + currentConfig={columnConfig} + /> + ); }; diff --git a/worklenz-frontend/src/components/task-management/improved-task-filters.tsx b/worklenz-frontend/src/components/task-management/improved-task-filters.tsx index 8df3a558..cd533b86 100644 --- a/worklenz-frontend/src/components/task-management/improved-task-filters.tsx +++ b/worklenz-frontend/src/components/task-management/improved-task-filters.tsx @@ -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(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 ( +
+ {/* Trigger Button - matching FilterDropdown style */} + + + {/* Dropdown Panel - matching FilterDropdown style */} + {open && ( +
+ {/* Options List */} +
+ {sortedFields.length === 0 ? ( +
+ No fields available +
+ ) : ( +
+ {sortedFields.map((field) => { + const isSelected = field.visible; + + return ( + + ); + })} +
+ )} +
+
+ )} +
+ ); +}; + // Main Component const ImprovedTaskFilters: React.FC = ({ position, @@ -673,7 +781,7 @@ const ImprovedTaskFilters: React.FC = ({ // TODO: Implement column visibility change }, [projectId]); - return ( + return (
{/* Left Section - Main Filters */} @@ -748,14 +856,7 @@ const ImprovedTaskFilters: React.FC = ({ )} {/* Show Fields Button (for list view) */} - {position === 'list' && ( - - )} + {position === 'list' && }
@@ -779,7 +880,7 @@ const ImprovedTaskFilters: React.FC = ({ {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 = ({ ); - }) + }).filter(Boolean) )} )} diff --git a/worklenz-frontend/src/components/task-management/task-group.tsx b/worklenz-frontend/src/components/task-management/task-group.tsx index 5793389a..2d5953c9 100644 --- a/worklenz-frontend/src/components/task-management/task-group.tsx +++ b/worklenz-frontend/src/components/task-management/task-group.tsx @@ -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 = React.memo(({ group, @@ -82,6 +68,54 @@ const TaskGroup: React.FC = 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 = React.memo(({ style={{ ...containerStyle, overflowX: 'unset' }} >
-
sum + col.width, 0) + SCROLLABLE_COLUMNS.reduce((sum, col) => sum + col.width, 0) }}> +
sum + col.width, 0) + visibleScrollableColumns.reduce((sum, col) => sum + col.width, 0) }}> {/* Group Header Row */}
@@ -172,7 +206,7 @@ const TaskGroup: React.FC = React.memo(({ >
- {FIXED_COLUMNS.map(col => ( + {visibleFixedColumns.map(col => (
= React.memo(({ ))}
- {SCROLLABLE_COLUMNS.map(col => ( + {visibleScrollableColumns.map(col => (
= React.memo(({ index={index} onSelect={onSelectTask} onToggleSubtasks={onToggleSubtasks} - fixedColumns={FIXED_COLUMNS} - scrollableColumns={SCROLLABLE_COLUMNS} + fixedColumns={visibleFixedColumns} + scrollableColumns={visibleScrollableColumns} /> ))}
diff --git a/worklenz-frontend/src/components/task-management/task-row.tsx b/worklenz-frontend/src/components/task-management/task-row.tsx index 2f278957..668fe248 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -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 = React.memo(({ })}
{/* Scrollable Columns */} -
sum + col.width, 0) || 0 }}> +
sum + col.width, 0) || 0 }}> {scrollableColumns?.map(col => { switch (col.key) { case 'progress': @@ -392,14 +393,12 @@ const TaskRow: React.FC = React.memo(({ ); case 'status': return ( -
- - {task.status} - +
+
); case 'priority': diff --git a/worklenz-frontend/src/components/task-management/task-status-dropdown.tsx b/worklenz-frontend/src/components/task-management/task-status-dropdown.tsx new file mode 100644 index 00000000..65bb55b8 --- /dev/null +++ b/worklenz-frontend/src/components/task-management/task-status-dropdown.tsx @@ -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 = ({ + task, + projectId, + isDarkMode = false +}) => { + const { socket, connected } = useSocket(); + const [isOpen, setIsOpen] = useState(false); + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); + const buttonRef = useRef(null); + const dropdownRef = useRef(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 */} + + + {/* Dropdown Menu - Rendered in Portal */} + {isOpen && createPortal( +
+
+ {statusList.map((status) => ( + + ))} +
+
, + document.body + )} + + ); +}; + +export default TaskStatusDropdown; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx b/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx index 47579249..3b18ab1c 100644 --- a/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx +++ b/worklenz-frontend/src/components/task-management/virtualized-task-list.tsx @@ -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 = 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 = 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 = React.memo(({ />
); - }, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks]); + }, [group, groupTasks, projectId, currentGrouping, selectedTaskIds, onSelectTask, onToggleSubtasks, fixedColumns, scrollableColumns]); return (
diff --git a/worklenz-frontend/src/features/task-management/taskListFields.slice.ts b/worklenz-frontend/src/features/task-management/taskListFields.slice.ts new file mode 100644 index 00000000..873dd037 --- /dev/null +++ b/worklenz-frontend/src/features/task-management/taskListFields.slice.ts @@ -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) { + const field = state.find(f => f.key === action.payload); + if (field) { + field.visible = !field.visible; + saveFields(state); + } + }, + setFields(state, action: PayloadAction) { + 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; \ No newline at end of file