refactor(task-list): enhance styling and structure in TaskListV2 and TaskRow components

- Consolidated import statements for better readability.
- Improved layout and styling consistency by adding border styles to various elements in TaskRow and AddTaskRow components.
- Updated TaskListV2Table to enhance the rendering logic and maintainability.
- Adjusted custom column handling and task estimation display for improved user experience.
This commit is contained in:
chamikaJ
2025-07-09 14:58:54 +05:30
parent 9cc19460bd
commit 399a01904a
5 changed files with 396 additions and 300 deletions

View File

@@ -62,10 +62,7 @@ import ImprovedTaskFilters from '@/components/task-management/improved-task-filt
import OptimizedBulkActionBar from '@/components/task-management/optimized-bulk-action-bar'; import OptimizedBulkActionBar from '@/components/task-management/optimized-bulk-action-bar';
import CustomColumnModal from '@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal'; import CustomColumnModal from '@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal';
import AddTaskRow from './components/AddTaskRow'; import AddTaskRow from './components/AddTaskRow';
import { import { AddCustomColumnButton, CustomColumnHeader } from './components/CustomColumnComponents';
AddCustomColumnButton,
CustomColumnHeader,
} from './components/CustomColumnComponents';
// Hooks and utilities // Hooks and utilities
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers'; import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
@@ -126,7 +123,10 @@ const TaskListV2Section: React.FC = () => {
); );
// Custom hooks // Custom hooks
const { activeId, handleDragStart, handleDragOver, handleDragEnd } = useDragAndDrop(allTasks, groups); const { activeId, handleDragStart, handleDragOver, handleDragEnd } = useDragAndDrop(
allTasks,
groups
);
const bulkActions = useBulkActions(); const bulkActions = useBulkActions();
// Enable real-time updates via socket handlers // Enable real-time updates via socket handlers
@@ -156,49 +156,51 @@ const TaskListV2Section: React.FC = () => {
}); });
// Add visible custom columns // Add visible custom columns
const visibleCustomColumns = customColumns const visibleCustomColumns =
?.filter(column => column.pinned) customColumns
?.map(column => { ?.filter(column => column.pinned)
// Give selection columns more width for dropdown content ?.map(column => {
const fieldType = column.custom_column_obj?.fieldType; // Give selection columns more width for dropdown content
let defaultWidth = 160; const fieldType = column.custom_column_obj?.fieldType;
if (fieldType === 'selection') { let defaultWidth = 160;
defaultWidth = 150; // Reduced width for selection dropdowns if (fieldType === 'selection') {
} else if (fieldType === 'people') { defaultWidth = 150; // Reduced width for selection dropdowns
defaultWidth = 170; // Extra width for people with avatars } else if (fieldType === 'people') {
} defaultWidth = 170; // Extra width for people with avatars
}
// Map the configuration data structure to the expected format // Map the configuration data structure to the expected format
const customColumnObj = column.custom_column_obj || (column as any).configuration; const customColumnObj = column.custom_column_obj || (column as any).configuration;
// Transform configuration format to custom_column_obj format if needed // Transform configuration format to custom_column_obj format if needed
let transformedColumnObj = customColumnObj; let transformedColumnObj = customColumnObj;
if (customColumnObj && !customColumnObj.fieldType && customColumnObj.field_type) { if (customColumnObj && !customColumnObj.fieldType && customColumnObj.field_type) {
transformedColumnObj = { transformedColumnObj = {
...customColumnObj, ...customColumnObj,
fieldType: customColumnObj.field_type, fieldType: customColumnObj.field_type,
numberType: customColumnObj.number_type, numberType: customColumnObj.number_type,
labelPosition: customColumnObj.label_position, labelPosition: customColumnObj.label_position,
previewValue: customColumnObj.preview_value, previewValue: customColumnObj.preview_value,
firstNumericColumn: customColumnObj.first_numeric_column_key, firstNumericColumn: customColumnObj.first_numeric_column_key,
secondNumericColumn: customColumnObj.second_numeric_column_key, secondNumericColumn: customColumnObj.second_numeric_column_key,
selectionsList: customColumnObj.selections_list || customColumnObj.selectionsList || [], selectionsList:
labelsList: customColumnObj.labels_list || customColumnObj.labelsList || [], 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: transformedColumnObj,
isCustom: true,
name: column.name,
uuid: column.id,
}; };
} }) || [];
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: transformedColumnObj,
isCustom: true,
name: column.name,
uuid: column.id,
};
}) || [];
return [...baseVisibleColumns, ...visibleCustomColumns]; return [...baseVisibleColumns, ...visibleCustomColumns];
}, [fields, columns, customColumns, t]); }, [fields, columns, customColumns, t]);
@@ -222,15 +224,15 @@ const TaskListV2Section: React.FC = () => {
if (backendColumn) { if (backendColumn) {
return { return {
...field, ...field,
visible: backendColumn.pinned ?? field.visible visible: backendColumn.pinned ?? field.visible,
}; };
} }
return field; return field;
}); });
// Only update if there are actual changes // Only update if there are actual changes
const hasChanges = updatedFields.some((field, index) => const hasChanges = updatedFields.some(
field.visible !== fields[index].visible (field, index) => field.visible !== fields[index].visible
); );
if (hasChanges) { if (hasChanges) {
@@ -269,65 +271,73 @@ const TaskListV2Section: React.FC = () => {
); );
// Function to update custom column values // Function to update custom column values
const updateTaskCustomColumnValue = useCallback((taskId: string, columnKey: string, value: string) => { const updateTaskCustomColumnValue = useCallback(
try { (taskId: string, columnKey: string, value: string) => {
if (!urlProjectId) { try {
console.error('Project ID is missing'); if (!urlProjectId) {
return; console.error('Project ID is missing');
} return;
}
const body = { const body = {
task_id: taskId, task_id: taskId,
column_key: columnKey, column_key: columnKey,
value: value, value: value,
project_id: urlProjectId, project_id: urlProjectId,
};
// Update the Redux store immediately for optimistic updates
const currentTask = allTasks.find(task => task.id === taskId);
if (currentTask) {
const updatedTask = {
...currentTask,
custom_column_values: {
...currentTask.custom_column_values,
[columnKey]: value,
},
updated_at: new Date().toISOString(),
}; };
// Import and dispatch the updateTask action // Update the Redux store immediately for optimistic updates
import('@/features/task-management/task-management.slice').then(({ updateTask }) => { const currentTask = allTasks.find(task => task.id === taskId);
dispatch(updateTask(updatedTask)); if (currentTask) {
}); const updatedTask = {
} ...currentTask,
custom_column_values: {
...currentTask.custom_column_values,
[columnKey]: value,
},
updated_at: new Date().toISOString(),
};
if (socket && connected) { // Import and dispatch the updateTask action
socket.emit(SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(), JSON.stringify(body)); import('@/features/task-management/task-management.slice').then(({ updateTask }) => {
} else { dispatch(updateTask(updatedTask));
console.warn('Socket not connected, unable to emit TASK_CUSTOM_COLUMN_UPDATE event'); });
}
if (socket && connected) {
socket.emit(SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(), JSON.stringify(body));
} else {
console.warn('Socket not connected, unable to emit TASK_CUSTOM_COLUMN_UPDATE event');
}
} catch (error) {
console.error('Error updating custom column value:', error);
} }
} catch (error) { },
console.error('Error updating custom column value:', error); [urlProjectId, socket, connected, allTasks, dispatch]
} );
}, [urlProjectId, socket, connected, allTasks, dispatch]);
// Custom column settings handler // Custom column settings handler
const handleCustomColumnSettings = useCallback((columnKey: string) => { const handleCustomColumnSettings = useCallback(
if (!columnKey) return; (columnKey: string) => {
if (!columnKey) return;
const columnData = visibleColumns.find(col => col.key === columnKey || col.id === columnKey); const columnData = visibleColumns.find(col => col.key === columnKey || col.id === columnKey);
// Use the UUID for API calls, not the key (nanoid) // Use the UUID for API calls, not the key (nanoid)
// For custom columns, prioritize the uuid field over id field // For custom columns, prioritize the uuid field over id field
const columnId = (columnData as any)?.uuid || columnData?.id || columnKey; const columnId = (columnData as any)?.uuid || columnData?.id || columnKey;
dispatch(setCustomColumnModalAttributes({ dispatch(
modalType: 'edit', setCustomColumnModalAttributes({
columnId: columnId, modalType: 'edit',
columnData: columnData columnId: columnId,
})); columnData: columnData,
dispatch(toggleCustomColumnModalOpen(true)); })
}, [dispatch, visibleColumns]); );
dispatch(toggleCustomColumnModalOpen(true));
},
[dispatch, visibleColumns]
);
// Add callback for task added // Add callback for task added
const handleTaskAdded = useCallback(() => { const handleTaskAdded = useCallback(() => {
@@ -350,25 +360,27 @@ const TaskListV2Section: React.FC = () => {
const visibleTasksInGroup = isCurrentGroupCollapsed const visibleTasksInGroup = isCurrentGroupCollapsed
? [] ? []
: group.taskIds : group.taskIds
.map(taskId => allTasks.find(task => task.id === taskId)) .map(taskId => allTasks.find(task => task.id === taskId))
.filter((task): task is Task => task !== undefined); .filter((task): task is Task => task !== undefined);
const tasksForVirtuoso = visibleTasksInGroup.map(task => ({ const tasksForVirtuoso = visibleTasksInGroup.map(task => ({
...task, ...task,
originalIndex: allTasks.indexOf(task), originalIndex: allTasks.indexOf(task),
})); }));
const itemsWithAddTask = !isCurrentGroupCollapsed ? [ const itemsWithAddTask = !isCurrentGroupCollapsed
...tasksForVirtuoso, ? [
{ ...tasksForVirtuoso,
id: `add-task-${group.id}`, {
isAddTaskRow: true, id: `add-task-${group.id}`,
groupId: group.id, isAddTaskRow: true,
groupType: currentGrouping || 'status', groupId: group.id,
groupValue: group.id, // Use the actual database ID from backend groupType: currentGrouping || 'status',
projectId: urlProjectId, groupValue: group.id, // Use the actual database ID from backend
} projectId: urlProjectId,
] : tasksForVirtuoso; },
]
: tasksForVirtuoso;
const groupData = { const groupData = {
...group, ...group,
@@ -398,8 +410,6 @@ const TaskListV2Section: React.FC = () => {
const isGroupCollapsed = collapsedGroups.has(group.id); const isGroupCollapsed = collapsedGroups.has(group.id);
const isGroupEmpty = group.actualCount === 0; const isGroupEmpty = group.actualCount === 0;
return ( return (
<div className={groupIndex > 0 ? 'mt-2' : ''}> <div className={groupIndex > 0 ? 'mt-2' : ''}>
<TaskGroupHeader <TaskGroupHeader
@@ -416,12 +426,22 @@ const TaskListV2Section: React.FC = () => {
{isGroupEmpty && !isGroupCollapsed && ( {isGroupEmpty && !isGroupCollapsed && (
<div className="relative w-full"> <div className="relative w-full">
<div className="flex items-center min-w-max px-1 py-3"> <div className="flex items-center min-w-max px-1 py-3">
{visibleColumns.map((column, index) => ( {visibleColumns.map((column, index) => {
<div const emptyColumnStyle = {
key={`empty-${column.id}`} width: column.width,
style={{ width: column.width, flexShrink: 0 }} flexShrink: 0,
/> ...(column.id === 'labels' && column.width === 'auto'
))} ? { minWidth: '200px', flexGrow: 1 }
: {}),
};
return (
<div
key={`empty-${column.id}`}
className="border-r border-gray-200 dark:border-gray-700"
style={emptyColumnStyle}
/>
);
})}
</div> </div>
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<div className="text-sm italic text-gray-400 dark:text-gray-500 bg-white dark:bg-gray-900 px-4 py-1 rounded-md border border-gray-200 dark:border-gray-700"> <div className="text-sm italic text-gray-400 dark:text-gray-500 bg-white dark:bg-gray-900 px-4 py-1 rounded-md border border-gray-200 dark:border-gray-700">
@@ -440,7 +460,6 @@ const TaskListV2Section: React.FC = () => {
(taskIndex: number) => { (taskIndex: number) => {
const item = virtuosoItems[taskIndex]; const item = virtuosoItems[taskIndex];
if (!item || !urlProjectId) return null; if (!item || !urlProjectId) return null;
if ('isAddTaskRow' in item && item.isAddTaskRow) { if ('isAddTaskRow' in item && item.isAddTaskRow) {
@@ -469,70 +488,86 @@ const TaskListV2Section: React.FC = () => {
); );
// Render column headers // Render column headers
const renderColumnHeaders = useCallback(() => ( const renderColumnHeaders = useCallback(
<div className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700" style={{ width: '100%', minWidth: 'max-content' }}> () => (
<div className="flex items-center px-1 py-3 w-full" style={{ minWidth: 'max-content', height: '44px' }}> <div
{visibleColumns.map((column, index) => { className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"
const columnStyle: ColumnStyle = { style={{ width: '100%', minWidth: 'max-content' }}
width: column.width, >
flexShrink: 0, <div
...(column.id === 'labels' && column.width === 'auto' className="flex items-center px-1 py-3 w-full"
? { style={{ minWidth: 'max-content', height: '44px' }}
minWidth: '200px', >
flexGrow: 1, {visibleColumns.map((column, index) => {
} const columnStyle: ColumnStyle = {
: {}), width: column.width,
...((column as any).minWidth && { minWidth: (column as any).minWidth }), flexShrink: 0,
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }), ...(column.id === 'labels' && column.width === 'auto'
}; ? {
minWidth: '200px',
flexGrow: 1,
}
: {}),
...((column as any).minWidth && { minWidth: (column as any).minWidth }),
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }),
};
return ( return (
<div <div
key={column.id} key={column.id}
className={`text-sm font-semibold text-gray-600 dark:text-gray-300 ${column.id === 'dragHandle' className={`text-sm font-semibold text-gray-600 dark:text-gray-300 ${
? 'flex items-center justify-center' column.id === 'dragHandle'
: column.id === 'checkbox'
? 'flex items-center justify-center' ? 'flex items-center justify-center'
: column.id === 'taskKey' : column.id === 'checkbox'
? 'flex items-center pl-3' ? 'flex items-center justify-center'
: column.id === 'title' : column.id === 'taskKey'
? 'flex items-center justify-between' ? 'flex items-center pl-3'
: column.id === 'description' : column.id === 'title'
? 'flex items-center px-2' ? 'flex items-center justify-between'
: column.id === 'labels' : column.id === 'description'
? 'flex items-center gap-0.5 flex-wrap min-w-0 px-2' ? 'flex items-center px-2'
: column.id === 'assignees' : column.id === 'labels'
? 'flex items-center px-2' ? 'flex items-center gap-0.5 flex-wrap min-w-0 px-2'
: 'flex items-center justify-center px-2' : column.id === 'assignees'
? 'flex items-center px-2'
: 'flex items-center justify-center px-2'
}`} }`}
style={columnStyle} style={columnStyle}
> >
{column.id === 'dragHandle' || column.id === 'checkbox' ? ( {column.id === 'dragHandle' || column.id === 'checkbox' ? (
<span></span> <span></span>
) : (column as any).isCustom ? ( ) : (column as any).isCustom ? (
<CustomColumnHeader <CustomColumnHeader
column={column} column={column}
onSettingsClick={handleCustomColumnSettings} onSettingsClick={handleCustomColumnSettings}
/> />
) : ( ) : (
t(column.label || '') t(column.label || '')
)} )}
</div> </div>
); );
})} })}
{/* Add Custom Column Button - positioned at the end and scrolls with content */} {/* Add Custom Column Button - positioned at the end and scrolls with content */}
<div className="flex items-center justify-center px-2" style={{ width: '50px', flexShrink: 0 }}> <div
<AddCustomColumnButton /> className="flex items-center justify-center px-2"
style={{ width: '50px', flexShrink: 0 }}
>
<AddCustomColumnButton />
</div>
</div> </div>
</div> </div>
</div> ),
), [visibleColumns, t, handleCustomColumnSettings]); [visibleColumns, t, handleCustomColumnSettings]
);
// Loading and error states // Loading and error states
if (loading || loadingColumns) return <Skeleton style={{ marginTop: 8 }} active />; if (loading || loadingColumns) return <Skeleton style={{ marginTop: 8 }} active />;
if (error) return <div>{t('emptyStates.errorPrefix')} {error}</div>; if (error)
return (
<div>
{t('emptyStates.errorPrefix')} {error}
</div>
);
// Show message when no data // Show message when no data
if (groups.length === 0 && !loading) { if (groups.length === 0 && !loading) {
@@ -556,58 +591,61 @@ const TaskListV2Section: React.FC = () => {
} }
return ( return (
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<div className="flex flex-col bg-white dark:bg-gray-900 h-full overflow-hidden"> <div className="flex flex-col bg-white dark:bg-gray-900 h-full overflow-hidden">
{/* Table Container */}
{/* Table Container */} <div
className="border border-gray-200 dark:border-gray-700 rounded-lg"
style={{
height: 'calc(100vh - 240px)', // Slightly reduce height to ensure scrollbar visibility
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
{/* Task List Content with Sticky Header */}
<div <div
className="border border-gray-200 dark:border-gray-700 rounded-lg" ref={contentScrollRef}
className="flex-1 bg-white dark:bg-gray-900 relative"
style={{ style={{
height: 'calc(100vh - 240px)', // Slightly reduce height to ensure scrollbar visibility overflowX: 'auto',
display: 'flex', overflowY: 'auto',
flexDirection: 'column', minHeight: 0,
overflow: 'hidden'
}} }}
> >
{/* Task List Content with Sticky Header */} {/* Sticky Column Headers */}
<div <div
ref={contentScrollRef} className="sticky top-0 z-30 bg-gray-50 dark:bg-gray-800"
className="flex-1 bg-white dark:bg-gray-900 relative" style={{ width: '100%', minWidth: 'max-content' }}
style={{
overflowX: 'auto',
overflowY: 'auto',
minHeight: 0
}}
> >
{/* Sticky Column Headers */} {renderColumnHeaders()}
<div className="sticky top-0 z-30 bg-gray-50 dark:bg-gray-800" style={{ width: '100%', minWidth: 'max-content' }}> </div>
{renderColumnHeaders()} <SortableContext
</div> items={virtuosoItems
<SortableContext .filter(item => !('isAddTaskRow' in item) && !item.parent_task_id)
items={virtuosoItems .map(item => item.id)
.filter(item => !('isAddTaskRow' in item) && !item.parent_task_id) .filter((id): id is string => id !== undefined)}
.map(item => item.id) strategy={verticalListSortingStrategy}
.filter((id): id is string => id !== undefined)} >
strategy={verticalListSortingStrategy} <div style={{ minWidth: 'max-content' }}>
> {/* Render groups manually for debugging */}
<div style={{ minWidth: 'max-content' }}> {virtuosoGroups.map((group, groupIndex) => (
{/* Render groups manually for debugging */} <div key={group.id}>
{virtuosoGroups.map((group, groupIndex) => ( {/* Group Header */}
<div key={group.id}> {renderGroup(groupIndex)}
{/* Group Header */}
{renderGroup(groupIndex)}
{/* Group Tasks */} {/* Group Tasks */}
{!collapsedGroups.has(group.id) && group.tasks.map((task, taskIndex) => { {!collapsedGroups.has(group.id) &&
const globalTaskIndex = virtuosoGroups group.tasks.map((task, taskIndex) => {
.slice(0, groupIndex) const globalTaskIndex =
.reduce((sum, g) => sum + g.count, 0) + taskIndex; virtuosoGroups.slice(0, groupIndex).reduce((sum, g) => sum + g.count, 0) +
taskIndex;
return ( return (
<div key={task.id || `add-task-${group.id}-${taskIndex}`}> <div key={task.id || `add-task-${group.id}-${taskIndex}`}>
@@ -615,63 +653,73 @@ const TaskListV2Section: React.FC = () => {
</div> </div>
); );
})} })}
</div> </div>
))} ))}
</div> </div>
</SortableContext> </SortableContext>
</div>
</div> </div>
</div>
{/* Drag Overlay */} {/* Drag Overlay */}
<DragOverlay dropAnimation={null}> <DragOverlay dropAnimation={null}>
{activeId ? ( {activeId ? (
<div className="bg-white dark:bg-gray-800 shadow-xl rounded-md border-2 border-blue-400 opacity-95"> <div className="bg-white dark:bg-gray-800 shadow-xl rounded-md border-2 border-blue-400 opacity-95">
<div className="px-4 py-3"> <div className="px-4 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<HolderOutlined className="text-blue-500" /> <HolderOutlined className="text-blue-500" />
<div> <div>
<div className="text-sm font-medium text-gray-900 dark:text-white"> <div className="text-sm font-medium text-gray-900 dark:text-white">
{allTasks.find(task => task.id === activeId)?.name || {allTasks.find(task => task.id === activeId)?.name ||
allTasks.find(task => task.id === activeId)?.title || allTasks.find(task => task.id === activeId)?.title ||
t('emptyStates.dragTaskFallback')} t('emptyStates.dragTaskFallback')}
</div> </div>
<div className="text-xs text-gray-500 dark:text-gray-400"> <div className="text-xs text-gray-500 dark:text-gray-400">
{allTasks.find(task => task.id === activeId)?.task_key} {allTasks.find(task => task.id === activeId)?.task_key}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) : null}
</DragOverlay>
{/* Bulk Action Bar */}
{selectedTaskIds.length > 0 && urlProjectId && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-50">
<OptimizedBulkActionBar
selectedTaskIds={selectedTaskIds}
totalSelected={selectedTaskIds.length}
projectId={urlProjectId}
onClearSelection={bulkActions.handleClearSelection}
onBulkStatusChange={(statusId) => bulkActions.handleBulkStatusChange(statusId, selectedTaskIds)}
onBulkPriorityChange={(priorityId) => bulkActions.handleBulkPriorityChange(priorityId, selectedTaskIds)}
onBulkPhaseChange={(phaseId) => bulkActions.handleBulkPhaseChange(phaseId, selectedTaskIds)}
onBulkAssignToMe={() => bulkActions.handleBulkAssignToMe(selectedTaskIds)}
onBulkAssignMembers={(memberIds) => bulkActions.handleBulkAssignMembers(memberIds, selectedTaskIds)}
onBulkAddLabels={(labelIds) => bulkActions.handleBulkAddLabels(labelIds, selectedTaskIds)}
onBulkArchive={() => bulkActions.handleBulkArchive(selectedTaskIds)}
onBulkDelete={() => bulkActions.handleBulkDelete(selectedTaskIds)}
onBulkDuplicate={() => bulkActions.handleBulkDuplicate(selectedTaskIds)}
onBulkExport={() => bulkActions.handleBulkExport(selectedTaskIds)}
onBulkSetDueDate={(date) => bulkActions.handleBulkSetDueDate(date, selectedTaskIds)}
/>
</div> </div>
)} ) : null}
</DragOverlay>
{/* Custom Column Modal */} {/* Bulk Action Bar */}
{createPortal(<CustomColumnModal />, document.body, 'custom-column-modal')} {selectedTaskIds.length > 0 && urlProjectId && (
</div> <div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-50">
</DndContext> <OptimizedBulkActionBar
selectedTaskIds={selectedTaskIds}
totalSelected={selectedTaskIds.length}
projectId={urlProjectId}
onClearSelection={bulkActions.handleClearSelection}
onBulkStatusChange={statusId =>
bulkActions.handleBulkStatusChange(statusId, selectedTaskIds)
}
onBulkPriorityChange={priorityId =>
bulkActions.handleBulkPriorityChange(priorityId, selectedTaskIds)
}
onBulkPhaseChange={phaseId =>
bulkActions.handleBulkPhaseChange(phaseId, selectedTaskIds)
}
onBulkAssignToMe={() => bulkActions.handleBulkAssignToMe(selectedTaskIds)}
onBulkAssignMembers={memberIds =>
bulkActions.handleBulkAssignMembers(memberIds, selectedTaskIds)
}
onBulkAddLabels={labelIds =>
bulkActions.handleBulkAddLabels(labelIds, selectedTaskIds)
}
onBulkArchive={() => bulkActions.handleBulkArchive(selectedTaskIds)}
onBulkDelete={() => bulkActions.handleBulkDelete(selectedTaskIds)}
onBulkDuplicate={() => bulkActions.handleBulkDuplicate(selectedTaskIds)}
onBulkExport={() => bulkActions.handleBulkExport(selectedTaskIds)}
onBulkSetDueDate={date => bulkActions.handleBulkSetDueDate(date, selectedTaskIds)}
/>
</div>
)}
{/* Custom Column Modal */}
{createPortal(<CustomColumnModal />, document.body, 'custom-column-modal')}
</div>
</DndContext>
); );
}; };

View File

@@ -271,7 +271,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'checkbox': case 'checkbox':
return ( return (
<div className="flex items-center justify-center" style={baseStyle}> <div className="flex items-center justify-center dark:border-gray-700" style={baseStyle}>
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
onChange={handleCheckboxChange} onChange={handleCheckboxChange}
@@ -282,7 +282,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'taskKey': case 'taskKey':
return ( return (
<div className="flex items-center pl-3" style={baseStyle}> <div className="flex items-center pl-3 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<span className="text-xs font-medium px-2 py-1 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 whitespace-nowrap border border-gray-200 dark:border-gray-600"> <span className="text-xs font-medium px-2 py-1 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 whitespace-nowrap border border-gray-200 dark:border-gray-600">
{task.task_key || 'N/A'} {task.task_key || 'N/A'}
</span> </span>
@@ -291,7 +291,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'title': case 'title':
return ( return (
<div className="flex items-center justify-between group" style={baseStyle}> <div className="flex items-center justify-between group pl-1 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<div className="flex items-center flex-1 min-w-0"> <div className="flex items-center flex-1 min-w-0">
{/* Indentation for subtasks - tighter spacing */} {/* Indentation for subtasks - tighter spacing */}
{isSubtask && <div className="w-4 flex-shrink-0" />} {isSubtask && <div className="w-4 flex-shrink-0" />}
@@ -417,7 +417,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'description': case 'description':
return ( return (
<div className="flex items-center px-2" style={baseStyle}> <div className="flex items-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<div <div
className="text-sm text-gray-600 dark:text-gray-400 truncate w-full" className="text-sm text-gray-600 dark:text-gray-400 truncate w-full"
style={{ style={{
@@ -435,7 +435,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'status': case 'status':
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<TaskStatusDropdown <TaskStatusDropdown
task={task} task={task}
projectId={projectId} projectId={projectId}
@@ -446,7 +446,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'assignees': case 'assignees':
return ( return (
<div className="flex items-center gap-1 px-2" style={baseStyle}> <div className="flex items-center gap-1 px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<AvatarGroup <AvatarGroup
members={task.assignee_names || []} members={task.assignee_names || []}
maxCount={3} maxCount={3}
@@ -463,7 +463,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'priority': case 'priority':
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<TaskPriorityDropdown <TaskPriorityDropdown
task={task} task={task}
projectId={projectId} projectId={projectId}
@@ -474,7 +474,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'dueDate': case 'dueDate':
return ( return (
<div className="flex items-center justify-center px-2 relative group" style={baseStyle}> <div className="flex items-center justify-center px-2 relative group border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
{activeDatePicker === 'dueDate' ? ( {activeDatePicker === 'dueDate' ? (
<div className="w-full relative"> <div className="w-full relative">
<DatePicker <DatePicker
@@ -532,7 +532,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'progress': case 'progress':
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
{task.progress !== undefined && {task.progress !== undefined &&
task.progress >= 0 && task.progress >= 0 &&
(task.progress === 100 ? ( (task.progress === 100 ? (
@@ -555,8 +555,13 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
); );
case 'labels': case 'labels':
const labelsColumn = visibleColumns.find(col => col.id === 'labels');
const labelsStyle = {
...baseStyle,
...(labelsColumn?.width === 'auto' ? { minWidth: '200px', flexGrow: 1 } : {})
};
return ( return (
<div className="flex items-center gap-0.5 flex-wrap min-w-0 px-2" style={{ ...baseStyle, minWidth: '150px', width: 'auto', flexGrow: 1 }}> <div className="flex items-center gap-0.5 flex-wrap min-w-0 px-2 border-r border-gray-200 dark:border-gray-700" style={labelsStyle}>
<TaskLabelsCell labels={task.labels} isDarkMode={isDarkMode} /> <TaskLabelsCell labels={task.labels} isDarkMode={isDarkMode} />
<LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} /> <LabelsSelector task={labelsAdapter} isDarkMode={isDarkMode} />
</div> </div>
@@ -564,7 +569,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'phase': case 'phase':
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<TaskPhaseDropdown <TaskPhaseDropdown
task={task} task={task}
projectId={projectId} projectId={projectId}
@@ -575,21 +580,42 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'timeTracking': case 'timeTracking':
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<TaskTimeTracking taskId={task.id || ''} isDarkMode={isDarkMode} /> <TaskTimeTracking taskId={task.id || ''} isDarkMode={isDarkMode} />
</div> </div>
); );
case 'estimation': case 'estimation':
// Use timeTracking.estimated which is the converted value from backend's total_minutes
const estimationDisplay = (() => {
const estimatedHours = task.timeTracking?.estimated;
if (estimatedHours && estimatedHours > 0) {
// Convert decimal hours to hours and minutes for display
const hours = Math.floor(estimatedHours);
const minutes = Math.round((estimatedHours - hours) * 60);
if (hours > 0 && minutes > 0) {
return `${hours}h ${minutes}m`;
} else if (hours > 0) {
return `${hours}h`;
} else if (minutes > 0) {
return `${minutes}m`;
}
}
return null;
})();
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
{task.timeTracking?.estimated ? ( {estimationDisplay ? (
<span className="text-sm text-gray-500 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
{task.timeTracking.estimated}h {estimationDisplay}
</span> </span>
) : ( ) : (
<span className="text-sm text-gray-400 dark:text-gray-500"> <span className="text-sm text-gray-400 dark:text-gray-500">
0 -
</span> </span>
)} )}
</div> </div>
@@ -597,7 +623,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'startDate': case 'startDate':
return ( return (
<div className="flex items-center justify-center px-2 relative group" style={baseStyle}> <div className="flex items-center justify-center px-2 relative group border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
{activeDatePicker === 'startDate' ? ( {activeDatePicker === 'startDate' ? (
<div className="w-full relative"> <div className="w-full relative">
<DatePicker <DatePicker
@@ -655,7 +681,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'completedDate': case 'completedDate':
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
{formattedDates.completed ? ( {formattedDates.completed ? (
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap"> <span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{formattedDates.completed} {formattedDates.completed}
@@ -668,7 +694,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'createdDate': case 'createdDate':
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
{formattedDates.created ? ( {formattedDates.created ? (
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap"> <span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{formattedDates.created} {formattedDates.created}
@@ -680,9 +706,8 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
); );
case 'lastUpdated': case 'lastUpdated':
console.log('formattedDates.updated', formattedDates.updated);
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
{formattedDates.updated ? ( {formattedDates.updated ? (
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap"> <span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{formattedDates.updated} {formattedDates.updated}
@@ -695,7 +720,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
case 'reporter': case 'reporter':
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
{task.reporter ? ( {task.reporter ? (
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{task.reporter}</span> <span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{task.reporter}</span>
) : ( ) : (
@@ -709,7 +734,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
const column = visibleColumns.find(col => col.id === columnId); const column = visibleColumns.find(col => col.id === columnId);
if (column && (column.custom_column || column.isCustom) && updateTaskCustomColumnValue) { if (column && (column.custom_column || column.isCustom) && updateTaskCustomColumnValue) {
return ( return (
<div className="flex items-center justify-center px-2" style={baseStyle}> <div className="flex items-center justify-center px-2 border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<CustomColumnCell <CustomColumnCell
column={column} column={column}
task={task} task={task}

View File

@@ -93,10 +93,16 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
case 'checkbox': case 'checkbox':
case 'taskKey': case 'taskKey':
case 'description': case 'description':
return <div style={baseStyle} />; return <div className="border-r border-gray-200 dark:border-gray-700" style={baseStyle} />;
case 'labels':
const labelsStyle = {
...baseStyle,
...(width === 'auto' ? { minWidth: '200px', flexGrow: 1 } : {})
};
return <div className="border-r border-gray-200 dark:border-gray-700" style={labelsStyle} />;
case 'title': case 'title':
return ( return (
<div className="flex items-center h-full" style={baseStyle}> <div className="flex items-center h-full border-r border-gray-200 dark:border-gray-700" style={baseStyle}>
<div className="flex items-center w-full h-full"> <div className="flex items-center w-full h-full">
<div className="w-4 mr-1" /> <div className="w-4 mr-1" />
@@ -129,7 +135,7 @@ const AddTaskRow: React.FC<AddTaskRowProps> = memo(({
</div> </div>
); );
default: default:
return <div style={baseStyle} />; return <div className="border-r border-gray-200 dark:border-gray-700" style={baseStyle} />;
} }
}, [isAdding, taskName, handleAddTask, handleCancel, t]); }, [isAdding, taskName, handleAddTask, handleCancel, t]);

View File

@@ -281,8 +281,8 @@ export const fetchTasksV3 = createAsyncThunk(
dueDate: task.dueDate, dueDate: task.dueDate,
startDate: task.startDate, startDate: task.startDate,
timeTracking: { timeTracking: {
estimated: convertTimeValue(task.total_time), estimated: task.timeTracking?.estimated || 0,
logged: convertTimeValue(task.time_spent), logged: task.timeTracking?.logged || 0,
}, },
customFields: {}, customFields: {},
custom_column_values: task.custom_column_values || {}, custom_column_values: task.custom_column_values || {},

View File

@@ -670,15 +670,32 @@ export const useTaskSocketHandlers = () => {
const handleEstimationChange = useCallback( const handleEstimationChange = useCallback(
(task: { id: string; parent_task: string | null; estimation: number }) => { (data: { id: string; parent_task: string | null; total_hours: number; total_minutes: number }) => {
if (!task) return; if (!data) return;
// Update the old task slice (for backward compatibility)
const taskWithProgress = { const taskWithProgress = {
...task, ...data,
manual_progress: false, manual_progress: false,
} as IProjectTask; } as IProjectTask;
dispatch(updateTaskEstimation({ task: taskWithProgress })); dispatch(updateTaskEstimation({ task: taskWithProgress }));
// Update task-management slice for task-list-v2 components
const currentTask = store.getState().taskManagement.entities[data.id];
if (currentTask) {
const estimatedHours = (data.total_hours || 0) + (data.total_minutes || 0) / 60;
const updatedTask: Task = {
...currentTask,
timeTracking: {
...currentTask.timeTracking,
estimated: estimatedHours,
},
updatedAt: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
dispatch(updateTask(updatedTask));
}
}, },
[dispatch] [dispatch]
); );