diff --git a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx index b1b4c4a9..5a95db6e 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskListV2.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useEffect, useState } from 'react'; +import React, { useCallback, useMemo, useEffect, useState, useRef } from 'react'; import { GroupedVirtuoso } from 'react-virtuoso'; import { DndContext, @@ -100,6 +100,13 @@ const TaskListV2: React.FC = () => { const customColumns = useAppSelector(selectCustomColumns); const loadingColumns = useAppSelector(selectLoadingColumns); + // Refs for scroll synchronization + const headerScrollRef = useRef(null); + const contentScrollRef = useRef(null); + + // State hooks + const [initializedFromDatabase, setInitializedFromDatabase] = useState(false); + // Configure sensors for drag and drop const sensors = useSensors( useSensor(PointerSensor, { @@ -161,13 +168,32 @@ const TaskListV2: React.FC = () => { defaultWidth = 170; // Extra width for people with avatars } + // Map the configuration data structure to the expected format + const customColumnObj = column.custom_column_obj || (column as any).configuration; + + // Transform configuration format to custom_column_obj format if needed + let transformedColumnObj = customColumnObj; + if (customColumnObj && !customColumnObj.fieldType && customColumnObj.field_type) { + transformedColumnObj = { + ...customColumnObj, + fieldType: customColumnObj.field_type, + numberType: customColumnObj.number_type, + labelPosition: customColumnObj.label_position, + previewValue: customColumnObj.preview_value, + firstNumericColumn: customColumnObj.first_numeric_column_key, + secondNumericColumn: customColumnObj.second_numeric_column_key, + selectionsList: customColumnObj.selections_list || customColumnObj.selectionsList || [], + labelsList: customColumnObj.labels_list || customColumnObj.labelsList || [], + }; + } + return { id: column.key || column.id || 'unknown', label: column.name || t('customColumns.customColumnHeader'), width: `${(column as any).width || defaultWidth}px`, key: column.key || column.id || 'unknown', custom_column: true, - custom_column_obj: column.custom_column_obj || (column as any).configuration, + custom_column_obj: transformedColumnObj, isCustom: true, name: column.name, uuid: column.id, @@ -186,7 +212,6 @@ const TaskListV2: React.FC = () => { }, [dispatch, urlProjectId]); // Initialize field visibility from database when columns are loaded (only once) - const [initializedFromDatabase, setInitializedFromDatabase] = useState(false); useEffect(() => { if (columns.length > 0 && fields.length > 0 && !initializedFromDatabase) { // Update local fields to match database state only on initial load @@ -274,9 +299,13 @@ const TaskListV2: React.FC = () => { const columnData = visibleColumns.find(col => col.key === columnKey || col.id === columnKey); + // Use the UUID for API calls, not the key (nanoid) + // For custom columns, prioritize the uuid field over id field + const columnId = (columnData as any)?.uuid || columnData?.id || columnKey; + dispatch(setCustomColumnModalAttributes({ modalType: 'edit', - columnId: columnKey, + columnId: columnId, columnData: columnData })); dispatch(toggleCustomColumnModalOpen(true)); @@ -288,6 +317,13 @@ const TaskListV2: React.FC = () => { // The global socket handler will handle the real-time update }, []); + // Handle scroll synchronization + const handleContentScroll = useCallback(() => { + if (headerScrollRef.current && contentScrollRef.current) { + headerScrollRef.current.scrollLeft = contentScrollRef.current.scrollLeft; + } + }, []); + // Memoized values for GroupedVirtuoso const virtuosoGroups = useMemo(() => { let currentTaskIndex = 0; @@ -346,6 +382,8 @@ const TaskListV2: React.FC = () => { const isGroupCollapsed = collapsedGroups.has(group.id); const isGroupEmpty = group.actualCount === 0; + + return (
0 ? 'mt-2' : ''}> { const renderTask = useCallback( (taskIndex: number) => { const item = virtuosoItems[taskIndex]; + + if (!item || !urlProjectId) return null; if ('isAddTaskRow' in item && item.isAddTaskRow) { @@ -458,9 +498,32 @@ const TaskListV2: React.FC = () => {
), [visibleColumns, t, handleCustomColumnSettings]); + + // Loading and error states if (loading || loadingColumns) return ; if (error) return
Error: {error}
; + + // Show message when no data + if (groups.length === 0 && !loading) { + return ( +
+
+ +
+
+
+
+ No task groups found +
+
+ Tasks will appear here when they are created or when filters are applied. +
+
+
+
+ ); + } return ( { onDragOver={handleDragOver} onDragEnd={handleDragEnd} > -
+
{/* Task Filters */}
@@ -481,44 +544,64 @@ const TaskListV2: React.FC = () => { {/* Table Container */}
-
- {/* Column Headers */} - {renderColumnHeaders()} - - {/* Task List Content */} -
- !('isAddTaskRow' in item) && !item.parent_task_id) - .map(item => item.id) - .filter((id): id is string => id !== undefined)} - strategy={verticalListSortingStrategy} - > - (({ style, children }, ref) => ( -
- {children} -
- )), - }} - /> -
+ {/* Column Headers */} +
+
+ {renderColumnHeaders()}
+ + {/* Task List Content */} +
+ !('isAddTaskRow' in item) && !item.parent_task_id) + .map(item => item.id) + .filter((id): id is string => id !== undefined)} + strategy={verticalListSortingStrategy} + > +
+ {/* Render groups manually for debugging */} + {virtuosoGroups.map((group, groupIndex) => ( +
+ {/* Group Header */} + {renderGroup(groupIndex)} + + {/* Group Tasks */} + {!collapsedGroups.has(group.id) && group.tasks.map((task, taskIndex) => { + const globalTaskIndex = virtuosoGroups + .slice(0, groupIndex) + .reduce((sum, g) => sum + g.count, 0) + taskIndex; + + return ( +
+ {renderTask(globalTaskIndex)} +
+ ); + })} +
+ ))} +
+
+
{/* Drag Overlay */} diff --git a/worklenz-frontend/src/components/task-list-v2/components/CustomColumnComponents.tsx b/worklenz-frontend/src/components/task-list-v2/components/CustomColumnComponents.tsx index 25293e75..12ff6f73 100644 --- a/worklenz-frontend/src/components/task-list-v2/components/CustomColumnComponents.tsx +++ b/worklenz-frontend/src/components/task-list-v2/components/CustomColumnComponents.tsx @@ -60,6 +60,7 @@ export const CustomColumnHeader: React.FC<{ onSettingsClick: (columnId: string) => void; }> = ({ column, onSettingsClick }) => { const { t } = useTranslation('task-list-table'); + const [isHovered, setIsHovered] = useState(false); const displayName = column.name || column.label || @@ -68,15 +69,20 @@ export const CustomColumnHeader: React.FC<{ t('customColumns.customColumnHeader'); return ( - - {displayName} + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={() => onSettingsClick(column.key || column.id)} + > + {displayName} { - e.stopPropagation(); - onSettingsClick(column.key || column.id); - }} + className={`hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 flex-shrink-0 ${ + isHovered ? 'opacity-100 scale-100' : 'opacity-0 scale-95' + }`} /> diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal.tsx index 6c666fe6..d70f74f6 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal.tsx @@ -9,7 +9,7 @@ import { Select, Typography, Popconfirm, -} from 'antd'; +} from '@/shared/antd-imports'; import { useTranslation } from 'react-i18next'; import SelectionTypeColumn from './selection-type-column/selection-type-column'; import NumberTypeColumn from './number-type-column/number-type-column'; @@ -118,11 +118,13 @@ const CustomColumnModal = () => { fullColumnData: openedColumn }); - // Try to get UUID from different possible locations in the column data - const columnUUID = openedColumn?.id || + // The customColumnId should now be the UUID passed from TaskListV2 + // But also check the column data as a fallback, prioritizing uuid over id + const columnUUID = customColumnId || openedColumn?.uuid || - openedColumn?.custom_column_obj?.id || - openedColumn?.custom_column_obj?.uuid; + openedColumn?.id || + openedColumn?.custom_column_obj?.uuid || + openedColumn?.custom_column_obj?.id; console.log('Extracted UUID candidates:', { 'openedColumn?.id': openedColumn?.id, @@ -328,7 +330,14 @@ const CustomColumnModal = () => { } : null; - if (updatedColumn && openedColumn?.id) { + // Get the correct UUID for the update operation, prioritizing uuid over id + const updateColumnUUID = customColumnId || + openedColumn?.uuid || + openedColumn?.id || + openedColumn?.custom_column_obj?.uuid || + openedColumn?.custom_column_obj?.id; + + if (updatedColumn && updateColumnUUID) { try { // Prepare the configuration object const configuration = { @@ -363,7 +372,7 @@ const CustomColumnModal = () => { }; // Make API request to update custom column using the service - await tasksCustomColumnsService.updateCustomColumn(openedColumn.id, { + await tasksCustomColumnsService.updateCustomColumn(updateColumnUUID, { name: value.fieldTitle, field_type: value.fieldType, width: 150, diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/selection-type-column/selection-type-column.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/selection-type-column/selection-type-column.tsx index 3f17469d..9dbe601d 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/selection-type-column/selection-type-column.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/selection-type-column/selection-type-column.tsx @@ -28,13 +28,12 @@ const SelectionTypeColumn = () => { const { customColumnModalType, customColumnId, + currentColumnData, selectionsList: storeSelectionsList, } = useAppSelector(state => state.taskListCustomColumnsReducer); - // Get the opened column data if in edit mode - const openedColumn = useAppSelector(state => - state.taskReducer.customColumns.find(col => col.key === customColumnId) - ); + // Use the current column data passed from TaskListV2 + const openedColumn = currentColumnData; console.log('SelectionTypeColumn render:', { customColumnModalType,