- {visibleColumns.map((column) => {
+ {visibleColumns.map((column, index) => {
+ // Calculate left position for sticky columns
+ let leftPosition = 0;
+ if (column.isSticky) {
+ for (let i = 0; i < index; i++) {
+ const prevColumn = visibleColumns[i];
+ if (prevColumn.isSticky) {
+ leftPosition += parseInt(prevColumn.width.replace('px', ''));
+ }
+ }
+ }
+
const emptyColumnStyle = {
width: column.width,
flexShrink: 0,
+ ...(column.isSticky && {
+ position: 'sticky' as const,
+ left: leftPosition,
+ zIndex: 5,
+ backgroundColor: 'inherit', // Inherit from parent container
+ }),
};
// Show text in the title column
@@ -130,7 +171,7 @@ const EmptyGroupMessage: React.FC<{ visibleColumns: any[] }> = ({ visibleColumns
return (
@@ -550,7 +591,7 @@ const TaskListV2Section: React.FC = () => {
projectId={urlProjectId || ''}
/>
{isGroupEmpty && !isGroupCollapsed && (
-
+
)}
);
@@ -596,19 +637,40 @@ const TaskListV2Section: React.FC = () => {
const renderColumnHeaders = useCallback(
() => (
{visibleColumns.map((column, index) => {
+ // Calculate left position for sticky columns
+ let leftPosition = 0;
+ if (column.isSticky) {
+ for (let i = 0; i < index; i++) {
+ const prevColumn = visibleColumns[i];
+ if (prevColumn.isSticky) {
+ leftPosition += parseInt(prevColumn.width.replace('px', ''));
+ }
+ }
+ }
+
const columnStyle: ColumnStyle = {
width: column.width,
flexShrink: 0,
...((column as any).minWidth && { minWidth: (column as any).minWidth }),
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }),
+ ...(column.isSticky && {
+ position: 'sticky' as const,
+ left: leftPosition,
+ zIndex: 10,
+ backgroundColor: isDarkMode ? '#141414' : '#f9fafb', // custom dark header : bg-gray-50
+ }),
};
return (
@@ -773,14 +835,25 @@ const TaskListV2Section: React.FC = () => {
}
return (
-
+ <>
+ {/* CSS for sticky column hover effects */}
+
+
+
{/* Table Container */}
{
return (
- {showDropSpacerBefore && }
+ {showDropSpacerBefore && }
{renderTask(globalTaskIndex, isFirstTaskInGroup)}
- {showDropSpacerAfter && }
+ {showDropSpacerAfter && }
);
})
@@ -917,6 +990,7 @@ const TaskListV2Section: React.FC = () => {
{createPortal(
, document.body, 'convert-to-subtask-drawer')}
+ >
);
};
diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx
index c2cbdd96..be2cec2c 100644
--- a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx
+++ b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx
@@ -131,11 +131,26 @@ const TaskRow: React.FC
= memo(({
isOver && !isDragging ? 'bg-blue-50 dark:bg-blue-900/20' : ''
}`}
>
- {visibleColumns.map((column, index) => (
-
- {renderColumn(column.id, column.width, column.isSticky, index)}
-
- ))}
+ {visibleColumns.map((column, index) => {
+ // Calculate background state for sticky columns - custom dark mode colors
+ const rowBackgrounds = {
+ normal: isDarkMode ? '#1e1e1e' : '#ffffff', // custom dark : bg-white
+ hover: isDarkMode ? '#1f2937' : '#f9fafb', // slightly lighter dark : bg-gray-50
+ dragOver: isDarkMode ? '#1e3a8a33' : '#dbeafe', // bg-blue-900/20 : bg-blue-50
+ };
+
+ let currentBg = rowBackgrounds.normal;
+ if (isOver && !isDragging) {
+ currentBg = rowBackgrounds.dragOver;
+ }
+ // Note: hover state is handled by CSS, so we'll use a CSS custom property
+
+ return (
+
+ {renderColumn(column.id, column.width, column.isSticky, index, currentBg, rowBackgrounds)}
+
+ );
+ })}
);
});
diff --git a/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowColumns.tsx b/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowColumns.tsx
index 6359deb3..38e61c19 100644
--- a/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowColumns.tsx
+++ b/worklenz-frontend/src/components/task-list-v2/hooks/useTaskRowColumns.tsx
@@ -90,17 +90,39 @@ export const useTaskRowColumns = ({
depth = 0,
}: UseTaskRowColumnsProps) => {
- const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
- switch (columnId) {
- case 'dragHandle':
- return (
-
- );
+ const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number, currentBg?: string, rowBackgrounds?: any) => {
+ // Calculate left position for sticky columns
+ let leftPosition = 0;
+ if (isSticky && typeof index === 'number') {
+ for (let i = 0; i < index; i++) {
+ const prevColumn = visibleColumns[i];
+ if (prevColumn.isSticky) {
+ leftPosition += parseInt(prevColumn.width.replace('px', ''));
+ }
+ }
+ }
+
+ // Create wrapper style for sticky positioning
+ const wrapperStyle = isSticky ? {
+ position: 'sticky' as const,
+ left: leftPosition,
+ zIndex: 5, // Lower than header but above regular content
+ backgroundColor: currentBg || (isDarkMode ? '#1e1e1e' : '#ffffff'), // Use dynamic background or fallback
+ overflow: 'hidden', // Prevent content from spilling over
+ width: width, // Ensure the wrapper respects column width
+ } : {};
+
+ const renderColumnContent = () => {
+ switch (columnId) {
+ case 'dragHandle':
+ return (
+
+ );
case 'checkbox':
return (
@@ -294,7 +316,27 @@ export const useTaskRowColumns = ({
);
}
return null;
+ }
+ };
+
+ // Wrap content with sticky positioning if needed
+ const content = renderColumnContent();
+ if (isSticky) {
+ const hoverBg = rowBackgrounds?.hover || (isDarkMode ? '#2a2a2a' : '#f9fafb');
+ return (
+
+ {content}
+
+ );
}
+
+ return content;
}, [
task,
projectId,
@@ -319,6 +361,7 @@ export const useTaskRowColumns = ({
handleTaskNameEdit,
attributes,
listeners,
+ depth,
]);
return { renderColumn };