Merge branch 'release-v2.1.4' of https://github.com/Worklenz/worklenz into feature/team-utilization
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import React, { useMemo, useCallback, useState } from 'react';
|
import React, { useMemo, useCallback, useState } from 'react';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
|
||||||
// @ts-ignore: Heroicons module types
|
// @ts-ignore: Heroicons module types
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
@@ -382,24 +381,12 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({
|
|||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Make the group header droppable
|
|
||||||
const { isOver, setNodeRef } = useDroppable({
|
|
||||||
id: group.id,
|
|
||||||
data: {
|
|
||||||
type: 'group',
|
|
||||||
group,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
className="inline-flex w-max items-center px-1 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-t border-b border-gray-200 dark:border-gray-700 rounded-t-md pr-2"
|
||||||
className={`inline-flex w-max items-center px-1 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-t border-b border-gray-200 dark:border-gray-700 rounded-t-md pr-2 ${
|
|
||||||
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
|
|
||||||
}`}
|
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isOver ? `${headerBackgroundColor}dd` : headerBackgroundColor,
|
backgroundColor: headerBackgroundColor,
|
||||||
color: headerTextColor,
|
color: headerTextColor,
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
TouchSensor,
|
TouchSensor,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
useDroppable,
|
Modifier,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
|
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||||
import {
|
import {
|
||||||
SortableContext,
|
SortableContext,
|
||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
@@ -67,109 +68,132 @@ import TaskListSkeleton from './components/TaskListSkeleton';
|
|||||||
import ConvertToSubtaskDrawer from '@/components/task-list-common/convert-to-subtask-drawer/convert-to-subtask-drawer';
|
import ConvertToSubtaskDrawer from '@/components/task-list-common/convert-to-subtask-drawer/convert-to-subtask-drawer';
|
||||||
import EmptyListPlaceholder from '@/components/EmptyListPlaceholder';
|
import EmptyListPlaceholder from '@/components/EmptyListPlaceholder';
|
||||||
|
|
||||||
// Empty Group Drop Zone Component
|
// Drop Spacer Component - creates space between tasks when dragging
|
||||||
const EmptyGroupDropZone: React.FC<{
|
const DropSpacer: React.FC<{ isVisible: boolean; visibleColumns: any[]; isDarkMode?: boolean }> = ({
|
||||||
groupId: string;
|
isVisible,
|
||||||
visibleColumns: any[];
|
visibleColumns,
|
||||||
t: (key: string) => string;
|
isDarkMode = false
|
||||||
}> = ({ groupId, visibleColumns, t }) => {
|
}) => {
|
||||||
const { setNodeRef, isOver, active } = useDroppable({
|
|
||||||
id: `empty-group-${groupId}`,
|
|
||||||
data: {
|
|
||||||
type: 'group',
|
|
||||||
groupId: groupId,
|
|
||||||
isEmpty: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
className={`relative w-full transition-colors duration-200 ${
|
|
||||||
isOver && active ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex items-center min-w-max px-1 border-t border-b border-gray-200 dark:border-gray-700"
|
|
||||||
style={{ height: '40px' }}
|
|
||||||
>
|
|
||||||
{visibleColumns.map((column, index) => {
|
|
||||||
const emptyColumnStyle = {
|
|
||||||
width: column.width,
|
|
||||||
flexShrink: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show text in the title column
|
|
||||||
if (column.id === 'title') {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`empty-${column.id}`}
|
|
||||||
className="flex items-center pl-1"
|
|
||||||
style={emptyColumnStyle}
|
|
||||||
>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
No tasks in this group
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`empty-${column.id}`}
|
|
||||||
className="border-r border-gray-200 dark:border-gray-700"
|
|
||||||
style={emptyColumnStyle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{isOver && active && (
|
|
||||||
<div className="absolute inset-0 border-2 border-dashed border-blue-400 dark:border-blue-500 rounded-md pointer-events-none" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Placeholder Drop Indicator Component
|
|
||||||
const PlaceholderDropIndicator: React.FC<{
|
|
||||||
isVisible: boolean;
|
|
||||||
visibleColumns: any[];
|
|
||||||
}> = ({ isVisible, visibleColumns }) => {
|
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center min-w-max px-1 border-2 border-dashed border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/20 rounded-md mx-1 my-1 transition-all duration-200 ease-in-out"
|
className="flex items-center min-w-max px-1 border-2 border-dashed border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/20 transition-all duration-200 ease-in-out"
|
||||||
style={{ minWidth: 'max-content', height: '40px' }}
|
style={{
|
||||||
|
height: isVisible ? '40px' : '0px',
|
||||||
|
opacity: isVisible ? 1 : 0,
|
||||||
|
marginTop: isVisible ? '2px' : '0px',
|
||||||
|
marginBottom: isVisible ? '2px' : '0px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column, index) => {
|
{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 = {
|
const columnStyle = {
|
||||||
width: column.width,
|
width: column.width,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
...(column.isSticky && {
|
||||||
|
position: 'sticky' as const,
|
||||||
|
left: leftPosition,
|
||||||
|
zIndex: 10,
|
||||||
|
backgroundColor: 'inherit', // Inherit from parent spacer
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (column.id === 'title') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`spacer-${column.id}`}
|
||||||
|
className="flex items-center pl-1 border-r border-blue-300 dark:border-blue-600"
|
||||||
|
style={columnStyle}
|
||||||
|
>
|
||||||
|
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium">
|
||||||
|
Drop here
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`placeholder-${column.id}`}
|
key={`spacer-${column.id}`}
|
||||||
className="flex items-center justify-center h-full"
|
className="border-r border-blue-300 dark:border-blue-600"
|
||||||
style={columnStyle}
|
style={columnStyle}
|
||||||
>
|
/>
|
||||||
{/* Show "Drop task here" message in the title column */}
|
|
||||||
{column.id === 'title' && (
|
|
||||||
<div className="text-xs text-blue-600 dark:text-blue-400 font-medium opacity-75">
|
|
||||||
Drop task here
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Show subtle placeholder content in other columns */}
|
|
||||||
{column.id !== 'title' && column.id !== 'dragHandle' && (
|
|
||||||
<div className="w-full h-4 mx-1 bg-white dark:bg-gray-700 rounded opacity-50" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Empty Group Message Component
|
||||||
|
const EmptyGroupMessage: React.FC<{ visibleColumns: any[]; isDarkMode?: boolean }> = ({
|
||||||
|
visibleColumns,
|
||||||
|
isDarkMode = false
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center min-w-max px-1 border-b border-gray-200 dark:border-gray-700" style={{ height: '40px' }}>
|
||||||
|
{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: 10,
|
||||||
|
backgroundColor: 'inherit', // Inherit from parent container
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show text in the title column
|
||||||
|
if (column.id === 'title') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`empty-${column.id}`}
|
||||||
|
className="flex items-center pl-1 border-r border-gray-200 dark:border-gray-700"
|
||||||
|
style={emptyColumnStyle}
|
||||||
|
>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||||
|
No tasks in this group
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`empty-${column.id}`}
|
||||||
|
className="border-r border-gray-200 dark:border-gray-700"
|
||||||
|
style={emptyColumnStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Hooks and utilities
|
// Hooks and utilities
|
||||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
@@ -211,7 +235,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
|
|
||||||
// State hooks
|
// State hooks
|
||||||
const [initializedFromDatabase, setInitializedFromDatabase] = useState(false);
|
const [initializedFromDatabase, setInitializedFromDatabase] = useState(false);
|
||||||
const [addTaskRows, setAddTaskRows] = useState<{ [groupId: string]: string[] }>({});
|
const [addTaskRows, setAddTaskRows] = useState<{[groupId: string]: string[]}>({});
|
||||||
|
|
||||||
// Configure sensors for drag and drop
|
// Configure sensors for drag and drop
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
@@ -232,7 +256,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Custom hooks
|
// Custom hooks
|
||||||
const { activeId, overId, handleDragStart, handleDragOver, handleDragEnd } = useDragAndDrop(
|
const { activeId, overId, dropPosition, handleDragStart, handleDragOver, handleDragEnd } = useDragAndDrop(
|
||||||
allTasks,
|
allTasks,
|
||||||
groups
|
groups
|
||||||
);
|
);
|
||||||
@@ -453,17 +477,17 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
const handleTaskAdded = useCallback((rowId: string) => {
|
const handleTaskAdded = useCallback((rowId: string) => {
|
||||||
// Task is now added in real-time via socket, no need to refetch
|
// Task is now added in real-time via socket, no need to refetch
|
||||||
// The global socket handler will handle the real-time update
|
// The global socket handler will handle the real-time update
|
||||||
|
|
||||||
// Find the group this row belongs to
|
// Find the group this row belongs to
|
||||||
const groupId = rowId.split('-')[2]; // Extract from rowId format: add-task-{groupId}-{index}
|
const groupId = rowId.split('-')[2]; // Extract from rowId format: add-task-{groupId}-{index}
|
||||||
|
|
||||||
// Add a new add task row to this group
|
// Add a new add task row to this group
|
||||||
setAddTaskRows(prev => {
|
setAddTaskRows(prev => {
|
||||||
const currentRows = prev[groupId] || [];
|
const currentRows = prev[groupId] || [];
|
||||||
const newRowId = `add-task-${groupId}-${currentRows.length + 1}`;
|
const newRowId = `add-task-${groupId}-${currentRows.length + 1}`;
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[groupId]: [...currentRows, newRowId],
|
[groupId]: [...currentRows, newRowId]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -493,7 +517,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
|
|
||||||
// Get add task rows for this group
|
// Get add task rows for this group
|
||||||
const groupAddRows = addTaskRows[group.id] || [];
|
const groupAddRows = addTaskRows[group.id] || [];
|
||||||
const addTaskItems = !isCurrentGroupCollapsed
|
const addTaskItems = !isCurrentGroupCollapsed
|
||||||
? [
|
? [
|
||||||
// Default add task row
|
// Default add task row
|
||||||
{
|
{
|
||||||
@@ -516,7 +540,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
projectId: urlProjectId,
|
projectId: urlProjectId,
|
||||||
rowId: rowId,
|
rowId: rowId,
|
||||||
autoFocus: index === groupAddRows.length - 1, // Auto-focus the latest row
|
autoFocus: index === groupAddRows.length - 1, // Auto-focus the latest row
|
||||||
})),
|
}))
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -545,6 +569,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
return virtuosoGroups.flatMap(group => group.tasks);
|
return virtuosoGroups.flatMap(group => group.tasks);
|
||||||
}, [virtuosoGroups]);
|
}, [virtuosoGroups]);
|
||||||
|
|
||||||
|
|
||||||
// Render functions
|
// Render functions
|
||||||
const renderGroup = useCallback(
|
const renderGroup = useCallback(
|
||||||
(groupIndex: number) => {
|
(groupIndex: number) => {
|
||||||
@@ -566,12 +591,12 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
projectId={urlProjectId || ''}
|
projectId={urlProjectId || ''}
|
||||||
/>
|
/>
|
||||||
{isGroupEmpty && !isGroupCollapsed && (
|
{isGroupEmpty && !isGroupCollapsed && (
|
||||||
<EmptyGroupDropZone groupId={group.id} visibleColumns={visibleColumns} t={t} />
|
<EmptyGroupMessage visibleColumns={visibleColumns} isDarkMode={isDarkMode} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[virtuosoGroups, collapsedGroups, handleGroupCollapse, visibleColumns, t]
|
[virtuosoGroups, collapsedGroups, handleGroupCollapse, visibleColumns, t, isDarkMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderTask = useCallback(
|
const renderTask = useCallback(
|
||||||
@@ -612,19 +637,40 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
const renderColumnHeaders = useCallback(
|
const renderColumnHeaders = useCallback(
|
||||||
() => (
|
() => (
|
||||||
<div
|
<div
|
||||||
className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"
|
className="border-b border-gray-200 dark:border-gray-700"
|
||||||
style={{ width: '100%', minWidth: 'max-content' }}
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
minWidth: 'max-content',
|
||||||
|
backgroundColor: isDarkMode ? '#141414' : '#f9fafb'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex items-center px-1 py-3 w-full"
|
className="flex items-center px-1 py-3 w-full"
|
||||||
style={{ minWidth: 'max-content', height: '44px' }}
|
style={{ minWidth: 'max-content', height: '44px' }}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column, index) => {
|
{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 = {
|
const columnStyle: ColumnStyle = {
|
||||||
width: column.width,
|
width: column.width,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
...((column as any).minWidth && { minWidth: (column as any).minWidth }),
|
...((column as any).minWidth && { minWidth: (column as any).minWidth }),
|
||||||
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }),
|
...((column as any).maxWidth && { maxWidth: (column as any).maxWidth }),
|
||||||
|
...(column.isSticky && {
|
||||||
|
position: 'sticky' as const,
|
||||||
|
left: leftPosition,
|
||||||
|
zIndex: 15,
|
||||||
|
backgroundColor: isDarkMode ? '#141414' : '#f9fafb', // custom dark header : bg-gray-50
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -701,9 +747,9 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
color: '#fbc84c69',
|
color: '#fbc84c69',
|
||||||
actualCount: 0,
|
actualCount: 0,
|
||||||
count: 1, // For the add task row
|
count: 1, // For the add task row
|
||||||
startIndex: 0,
|
startIndex: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
@@ -738,7 +784,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{renderColumnHeaders()}
|
{renderColumnHeaders()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ minWidth: 'max-content' }}>
|
<div style={{ minWidth: 'max-content' }}>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<TaskGroupHeader
|
<TaskGroupHeader
|
||||||
@@ -770,7 +816,7 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other groupings, show the empty state message
|
// For other groupings, show the empty state message
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-white dark:bg-gray-900 h-full">
|
<div className="flex flex-col bg-white dark:bg-gray-900 h-full">
|
||||||
@@ -789,13 +835,25 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<>
|
||||||
sensors={sensors}
|
{/* CSS for sticky column hover effects */}
|
||||||
collisionDetection={closestCenter}
|
<style>
|
||||||
onDragStart={handleDragStart}
|
{`
|
||||||
onDragOver={handleDragOver}
|
.hover\\:bg-gray-50:hover .sticky-column-hover,
|
||||||
onDragEnd={handleDragEnd}
|
.dark .hover\\:bg-gray-800:hover .sticky-column-hover {
|
||||||
>
|
background-color: var(--hover-bg) !important;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
>
|
||||||
<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
|
<div
|
||||||
@@ -839,63 +897,31 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
{renderGroup(groupIndex)}
|
{renderGroup(groupIndex)}
|
||||||
|
|
||||||
{/* Group Tasks */}
|
{/* Group Tasks */}
|
||||||
{!collapsedGroups.has(group.id) &&
|
{!collapsedGroups.has(group.id) && (
|
||||||
(group.tasks.length > 0
|
group.tasks.length > 0 ? (
|
||||||
? group.tasks.map((task, taskIndex) => {
|
group.tasks.map((task, taskIndex) => {
|
||||||
const globalTaskIndex =
|
const globalTaskIndex =
|
||||||
virtuosoGroups
|
virtuosoGroups.slice(0, groupIndex).reduce((sum, g) => sum + g.count, 0) +
|
||||||
.slice(0, groupIndex)
|
taskIndex;
|
||||||
.reduce((sum, g) => sum + g.count, 0) + taskIndex;
|
|
||||||
|
|
||||||
// Check if this is the first actual task in the group (not AddTaskRow)
|
// Check if this is the first actual task in the group (not AddTaskRow)
|
||||||
const isFirstTaskInGroup = taskIndex === 0 && !('isAddTaskRow' in task);
|
const isFirstTaskInGroup = taskIndex === 0 && !('isAddTaskRow' in task);
|
||||||
|
|
||||||
|
// Check if we should show drop spacer
|
||||||
|
const isOverThisTask = activeId && overId === task.id && !('isAddTaskRow' in task);
|
||||||
|
const showDropSpacerBefore = isOverThisTask && dropPosition === 'before';
|
||||||
|
const showDropSpacerAfter = isOverThisTask && dropPosition === 'after';
|
||||||
|
|
||||||
// Check if we should show drop indicators
|
return (
|
||||||
const isTaskBeingDraggedOver = overId === task.id;
|
<div key={task.id || `add-task-${group.id}-${taskIndex}`}>
|
||||||
const isGroupBeingDraggedOver = overId === group.id;
|
{showDropSpacerBefore && <DropSpacer isVisible={true} visibleColumns={visibleColumns} isDarkMode={isDarkMode} />}
|
||||||
const isFirstTaskInGroupBeingDraggedOver =
|
{renderTask(globalTaskIndex, isFirstTaskInGroup)}
|
||||||
isFirstTaskInGroup && isTaskBeingDraggedOver;
|
{showDropSpacerAfter && <DropSpacer isVisible={true} visibleColumns={visibleColumns} isDarkMode={isDarkMode} />}
|
||||||
|
</div>
|
||||||
return (
|
);
|
||||||
<div key={task.id || `add-task-${group.id}-${taskIndex}`}>
|
})
|
||||||
{/* Placeholder drop indicator before first task in group */}
|
) : null
|
||||||
{isFirstTaskInGroupBeingDraggedOver && (
|
)}
|
||||||
<PlaceholderDropIndicator
|
|
||||||
isVisible={true}
|
|
||||||
visibleColumns={visibleColumns}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Placeholder drop indicator between tasks */}
|
|
||||||
{isTaskBeingDraggedOver && !isFirstTaskInGroup && (
|
|
||||||
<PlaceholderDropIndicator
|
|
||||||
isVisible={true}
|
|
||||||
visibleColumns={visibleColumns}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{renderTask(globalTaskIndex, isFirstTaskInGroup)}
|
|
||||||
|
|
||||||
{/* Placeholder drop indicator at end of group when dragging over group */}
|
|
||||||
{isGroupBeingDraggedOver &&
|
|
||||||
taskIndex === group.tasks.length - 1 && (
|
|
||||||
<PlaceholderDropIndicator
|
|
||||||
isVisible={true}
|
|
||||||
visibleColumns={visibleColumns}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: // Handle empty groups with placeholder drop indicator
|
|
||||||
overId === group.id && (
|
|
||||||
<div style={{ minWidth: 'max-content' }}>
|
|
||||||
<PlaceholderDropIndicator
|
|
||||||
isVisible={true}
|
|
||||||
visibleColumns={visibleColumns}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -904,15 +930,15 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Drag Overlay */}
|
{/* Drag Overlay */}
|
||||||
<DragOverlay dropAnimation={{ duration: 200, easing: 'ease-in-out' }}>
|
<DragOverlay dropAnimation={{ duration: 200, easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)' }}>
|
||||||
{activeId ? (
|
{activeId ? (
|
||||||
<div
|
<div
|
||||||
className="bg-white dark:bg-gray-800 shadow-lg rounded-md border border-blue-400 dark:border-blue-500 opacity-90"
|
className="bg-white dark:bg-gray-800 shadow-2xl rounded-lg border-2 border-blue-500 dark:border-blue-400 opacity-95"
|
||||||
style={{ width: visibleColumns.find(col => col.id === 'title')?.width || '300px' }}
|
style={{ width: visibleColumns.find(col => col.id === 'title')?.width || '300px' }}
|
||||||
>
|
>
|
||||||
<div className="px-3 py-2">
|
<div className="px-4 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<HolderOutlined className="text-gray-400 dark:text-gray-500 text-xs" />
|
<HolderOutlined className="text-blue-500 dark:text-blue-400 text-sm" />
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-white truncate flex-1">
|
<div className="text-sm font-medium text-gray-900 dark:text-white truncate flex-1">
|
||||||
{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 ||
|
||||||
@@ -959,12 +985,13 @@ const TaskListV2Section: React.FC = () => {
|
|||||||
|
|
||||||
{/* Custom Column Modal */}
|
{/* Custom Column Modal */}
|
||||||
{createPortal(<CustomColumnModal />, document.body, 'custom-column-modal')}
|
{createPortal(<CustomColumnModal />, document.body, 'custom-column-modal')}
|
||||||
|
|
||||||
{/* Convert To Subtask Drawer */}
|
{/* Convert To Subtask Drawer */}
|
||||||
{createPortal(<ConvertToSubtaskDrawer />, document.body, 'convert-to-subtask-drawer')}
|
{createPortal(<ConvertToSubtaskDrawer />, document.body, 'convert-to-subtask-drawer')}
|
||||||
</div>
|
</div>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TaskListV2Section;
|
export default TaskListV2Section;
|
||||||
@@ -27,116 +27,134 @@ interface TaskRowProps {
|
|||||||
depth?: number;
|
depth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskRow: React.FC<TaskRowProps> = memo(
|
const TaskRow: React.FC<TaskRowProps> = memo(({
|
||||||
({
|
taskId,
|
||||||
taskId,
|
projectId,
|
||||||
projectId,
|
visibleColumns,
|
||||||
visibleColumns,
|
isSubtask = false,
|
||||||
isSubtask = false,
|
isFirstInGroup = false,
|
||||||
isFirstInGroup = false,
|
updateTaskCustomColumnValue,
|
||||||
updateTaskCustomColumnValue,
|
depth = 0
|
||||||
depth = 0,
|
}) => {
|
||||||
}) => {
|
// Get task data and selection state from Redux
|
||||||
// Get task data and selection state from Redux
|
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
|
||||||
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const isDarkMode = themeMode === 'dark';
|
||||||
const isDarkMode = themeMode === 'dark';
|
|
||||||
|
|
||||||
// Early return if task is not found
|
// Early return if task is not found
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
// Use extracted hooks for state management
|
|
||||||
const {
|
|
||||||
activeDatePicker,
|
|
||||||
setActiveDatePicker,
|
|
||||||
editTaskName,
|
|
||||||
setEditTaskName,
|
|
||||||
taskName,
|
|
||||||
setTaskName,
|
|
||||||
taskDisplayName,
|
|
||||||
convertedTask,
|
|
||||||
formattedDates,
|
|
||||||
dateValues,
|
|
||||||
labelsAdapter,
|
|
||||||
} = useTaskRowState(task);
|
|
||||||
|
|
||||||
const { handleCheckboxChange, handleTaskNameSave, handleTaskNameEdit } = useTaskRowActions({
|
|
||||||
task,
|
|
||||||
taskId,
|
|
||||||
taskName,
|
|
||||||
editTaskName,
|
|
||||||
setEditTaskName,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drag and drop functionality - only enable for parent tasks
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
||||||
id: task.id,
|
|
||||||
data: {
|
|
||||||
type: 'task',
|
|
||||||
task,
|
|
||||||
},
|
|
||||||
disabled: isSubtask, // Disable drag and drop for subtasks
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use extracted column renderer hook
|
|
||||||
const { renderColumn } = useTaskRowColumns({
|
|
||||||
task,
|
|
||||||
projectId,
|
|
||||||
isSubtask,
|
|
||||||
isSelected,
|
|
||||||
isDarkMode,
|
|
||||||
visibleColumns,
|
|
||||||
updateTaskCustomColumnValue,
|
|
||||||
taskDisplayName,
|
|
||||||
convertedTask,
|
|
||||||
formattedDates,
|
|
||||||
dateValues,
|
|
||||||
labelsAdapter,
|
|
||||||
activeDatePicker,
|
|
||||||
setActiveDatePicker,
|
|
||||||
editTaskName,
|
|
||||||
taskName,
|
|
||||||
setEditTaskName,
|
|
||||||
setTaskName,
|
|
||||||
handleCheckboxChange,
|
|
||||||
handleTaskNameSave,
|
|
||||||
handleTaskNameEdit,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
depth,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Memoize style object to prevent unnecessary re-renders
|
|
||||||
const style = useMemo(
|
|
||||||
() => ({
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
opacity: isDragging ? 0 : 1, // Completely hide the original task while dragging
|
|
||||||
}),
|
|
||||||
[transform, transition, isDragging]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={{ ...style, height: '40px' }}
|
|
||||||
className={`flex items-center min-w-max px-1 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 ${
|
|
||||||
isFirstInGroup ? 'border-t border-gray-200 dark:border-gray-700' : ''
|
|
||||||
} ${isDragging ? 'shadow-lg border border-blue-300' : ''}`}
|
|
||||||
>
|
|
||||||
{visibleColumns.map((column, index) => (
|
|
||||||
<React.Fragment key={column.id}>
|
|
||||||
{renderColumn(column.id, column.width, column.isSticky, index)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
// Use extracted hooks for state management
|
||||||
|
const {
|
||||||
|
activeDatePicker,
|
||||||
|
setActiveDatePicker,
|
||||||
|
editTaskName,
|
||||||
|
setEditTaskName,
|
||||||
|
taskName,
|
||||||
|
setTaskName,
|
||||||
|
taskDisplayName,
|
||||||
|
convertedTask,
|
||||||
|
formattedDates,
|
||||||
|
dateValues,
|
||||||
|
labelsAdapter,
|
||||||
|
} = useTaskRowState(task);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleCheckboxChange,
|
||||||
|
handleTaskNameSave,
|
||||||
|
handleTaskNameEdit,
|
||||||
|
} = useTaskRowActions({
|
||||||
|
task,
|
||||||
|
taskId,
|
||||||
|
taskName,
|
||||||
|
editTaskName,
|
||||||
|
setEditTaskName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag and drop functionality - only enable for parent tasks
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = useSortable({
|
||||||
|
id: task.id,
|
||||||
|
data: {
|
||||||
|
type: 'task',
|
||||||
|
task,
|
||||||
|
},
|
||||||
|
disabled: isSubtask, // Disable drag and drop for subtasks
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use extracted column renderer hook
|
||||||
|
const { renderColumn } = useTaskRowColumns({
|
||||||
|
task,
|
||||||
|
projectId,
|
||||||
|
isSubtask,
|
||||||
|
isSelected,
|
||||||
|
isDarkMode,
|
||||||
|
visibleColumns,
|
||||||
|
updateTaskCustomColumnValue,
|
||||||
|
taskDisplayName,
|
||||||
|
convertedTask,
|
||||||
|
formattedDates,
|
||||||
|
dateValues,
|
||||||
|
labelsAdapter,
|
||||||
|
activeDatePicker,
|
||||||
|
setActiveDatePicker,
|
||||||
|
editTaskName,
|
||||||
|
taskName,
|
||||||
|
setEditTaskName,
|
||||||
|
setTaskName,
|
||||||
|
handleCheckboxChange,
|
||||||
|
handleTaskNameSave,
|
||||||
|
handleTaskNameEdit,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
depth,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Memoize style object to prevent unnecessary re-renders
|
||||||
|
const style = useMemo(() => ({
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.3 : 1, // Make original task slightly transparent while dragging
|
||||||
|
}), [transform, transition, isDragging]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={{ ...style, height: '40px' }}
|
||||||
|
className={`flex items-center min-w-max px-1 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${
|
||||||
|
isFirstInGroup ? 'border-t border-gray-200 dark:border-gray-700' : ''
|
||||||
|
} ${
|
||||||
|
isDragging ? 'opacity-50' : ''
|
||||||
|
} ${
|
||||||
|
isOver && !isDragging ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<React.Fragment key={column.id}>
|
||||||
|
{renderColumn(column.id, column.width, column.isSticky, index, currentBg, rowBackgrounds)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
TaskRow.displayName = 'TaskRow';
|
TaskRow.displayName = 'TaskRow';
|
||||||
|
|
||||||
export default TaskRow;
|
export default TaskRow;
|
||||||
@@ -19,10 +19,11 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
const currentSession = useAuthService().getCurrentSession();
|
const currentSession = useAuthService().getCurrentSession();
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
const [overId, setOverId] = useState<string | null>(null);
|
const [overId, setOverId] = useState<string | null>(null);
|
||||||
|
const [dropPosition, setDropPosition] = useState<'before' | 'after' | null>(null);
|
||||||
|
|
||||||
// Helper function to emit socket event for persistence
|
// Helper function to emit socket event for persistence (within-group only)
|
||||||
const emitTaskSortChange = useCallback(
|
const emitTaskSortChange = useCallback(
|
||||||
(taskId: string, sourceGroup: TaskGroup, targetGroup: TaskGroup, insertIndex: number) => {
|
(taskId: string, group: TaskGroup, insertIndex: number) => {
|
||||||
if (!socket || !connected || !projectId) {
|
if (!socket || !connected || !projectId) {
|
||||||
logger.warning('Socket not connected or missing project ID');
|
logger.warning('Socket not connected or missing project ID');
|
||||||
return;
|
return;
|
||||||
@@ -39,55 +40,31 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
|
|
||||||
// Use new bulk update approach - recalculate ALL task orders to prevent duplicates
|
// Use new bulk update approach - recalculate ALL task orders to prevent duplicates
|
||||||
const taskUpdates: any[] = [];
|
const taskUpdates: any[] = [];
|
||||||
|
|
||||||
// Create a copy of all groups and perform the move operation
|
// Create a copy of all groups
|
||||||
const updatedGroups = groups.map(group => ({
|
const updatedGroups = groups.map(g => ({
|
||||||
...group,
|
...g,
|
||||||
taskIds: [...group.taskIds],
|
taskIds: [...g.taskIds]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Find the source and target groups in our copy
|
// Find the group in our copy
|
||||||
const sourceGroupCopy = updatedGroups.find(g => g.id === sourceGroup.id)!;
|
const groupCopy = updatedGroups.find(g => g.id === group.id)!;
|
||||||
const targetGroupCopy = updatedGroups.find(g => g.id === targetGroup.id)!;
|
|
||||||
|
// Reorder within the group
|
||||||
if (sourceGroup.id === targetGroup.id) {
|
const sourceIndex = groupCopy.taskIds.indexOf(taskId);
|
||||||
// Same group - reorder within the group
|
// Remove task from old position
|
||||||
const sourceIndex = sourceGroupCopy.taskIds.indexOf(taskId);
|
groupCopy.taskIds.splice(sourceIndex, 1);
|
||||||
// Remove task from old position
|
// Insert at new position
|
||||||
sourceGroupCopy.taskIds.splice(sourceIndex, 1);
|
groupCopy.taskIds.splice(insertIndex, 0, taskId);
|
||||||
// Insert at new position
|
|
||||||
sourceGroupCopy.taskIds.splice(insertIndex, 0, taskId);
|
|
||||||
} else {
|
|
||||||
// Different groups - move task between groups
|
|
||||||
// Remove from source group
|
|
||||||
const sourceIndex = sourceGroupCopy.taskIds.indexOf(taskId);
|
|
||||||
sourceGroupCopy.taskIds.splice(sourceIndex, 1);
|
|
||||||
|
|
||||||
// Add to target group
|
|
||||||
targetGroupCopy.taskIds.splice(insertIndex, 0, taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now assign sequential sort orders to ALL tasks across ALL groups
|
// Now assign sequential sort orders to ALL tasks across ALL groups
|
||||||
let currentSortOrder = 0;
|
let currentSortOrder = 0;
|
||||||
updatedGroups.forEach(group => {
|
updatedGroups.forEach(grp => {
|
||||||
group.taskIds.forEach(id => {
|
grp.taskIds.forEach(id => {
|
||||||
const update: any = {
|
taskUpdates.push({
|
||||||
task_id: id,
|
task_id: id,
|
||||||
sort_order: currentSortOrder,
|
sort_order: currentSortOrder
|
||||||
};
|
});
|
||||||
|
|
||||||
// Add group-specific fields for the moved task if it changed groups
|
|
||||||
if (id === taskId && sourceGroup.id !== targetGroup.id) {
|
|
||||||
if (currentGrouping === 'status') {
|
|
||||||
update.status_id = targetGroup.id;
|
|
||||||
} else if (currentGrouping === 'priority') {
|
|
||||||
update.priority_id = targetGroup.id;
|
|
||||||
} else if (currentGrouping === 'phase') {
|
|
||||||
update.phase_id = targetGroup.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
taskUpdates.push(update);
|
|
||||||
currentSortOrder++;
|
currentSortOrder++;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -96,8 +73,8 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
group_by: currentGrouping || 'status',
|
group_by: currentGrouping || 'status',
|
||||||
task_updates: taskUpdates,
|
task_updates: taskUpdates,
|
||||||
from_group: sourceGroup.id,
|
from_group: group.id,
|
||||||
to_group: targetGroup.id,
|
to_group: group.id,
|
||||||
task: {
|
task: {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
@@ -108,38 +85,6 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), socketData);
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), socketData);
|
||||||
|
|
||||||
// Also emit the specific grouping field change event for the moved task
|
|
||||||
if (sourceGroup.id !== targetGroup.id) {
|
|
||||||
if (currentGrouping === 'phase') {
|
|
||||||
// Emit phase change event
|
|
||||||
socket.emit(SocketEvents.TASK_PHASE_CHANGE.toString(), {
|
|
||||||
task_id: taskId,
|
|
||||||
phase_id: targetGroup.id,
|
|
||||||
parent_task: task.parent_task_id || null,
|
|
||||||
});
|
|
||||||
} else if (currentGrouping === 'priority') {
|
|
||||||
// Emit priority change event
|
|
||||||
socket.emit(
|
|
||||||
SocketEvents.TASK_PRIORITY_CHANGE.toString(),
|
|
||||||
JSON.stringify({
|
|
||||||
task_id: taskId,
|
|
||||||
priority_id: targetGroup.id,
|
|
||||||
team_id: teamId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (currentGrouping === 'status') {
|
|
||||||
// Emit status change event
|
|
||||||
socket.emit(
|
|
||||||
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
|
||||||
JSON.stringify({
|
|
||||||
task_id: taskId,
|
|
||||||
status_id: targetGroup.id,
|
|
||||||
team_id: teamId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[socket, connected, projectId, allTasks, groups, currentGrouping, currentSession]
|
[socket, connected, projectId, allTasks, groups, currentGrouping, currentSession]
|
||||||
);
|
);
|
||||||
@@ -154,32 +99,38 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
|
|
||||||
if (!over) {
|
if (!over) {
|
||||||
setOverId(null);
|
setOverId(null);
|
||||||
|
setDropPosition(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeId = active.id;
|
const activeTask = allTasks.find(task => task.id === active.id);
|
||||||
const overId = over.id;
|
const overTask = allTasks.find(task => task.id === over.id);
|
||||||
|
|
||||||
// Set the overId for drop indicators
|
if (activeTask && overTask) {
|
||||||
setOverId(overId as string);
|
const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id));
|
||||||
|
const overGroup = groups.find(group => group.taskIds.includes(overTask.id));
|
||||||
// Find the active task and the item being dragged over
|
|
||||||
const activeTask = allTasks.find(task => task.id === activeId);
|
// Only set overId if both tasks are in the same group
|
||||||
if (!activeTask) return;
|
if (activeGroup && overGroup && activeGroup.id === overGroup.id) {
|
||||||
|
setOverId(over.id as string);
|
||||||
// Check if we're dragging over a task or a group
|
|
||||||
const overTask = allTasks.find(task => task.id === overId);
|
// Calculate drop position based on task indices
|
||||||
const overGroup = groups.find(group => group.id === overId);
|
const activeIndex = activeGroup.taskIds.indexOf(activeTask.id);
|
||||||
|
const overIndex = activeGroup.taskIds.indexOf(overTask.id);
|
||||||
// Find the groups
|
|
||||||
const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id));
|
if (activeIndex < overIndex) {
|
||||||
let targetGroup = overGroup;
|
setDropPosition('after');
|
||||||
|
} else {
|
||||||
if (overTask) {
|
setDropPosition('before');
|
||||||
targetGroup = groups.find(group => group.taskIds.includes(overTask.id));
|
}
|
||||||
|
} else {
|
||||||
|
setOverId(null);
|
||||||
|
setDropPosition(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setOverId(null);
|
||||||
|
setDropPosition(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activeGroup || !targetGroup) return;
|
|
||||||
},
|
},
|
||||||
[allTasks, groups]
|
[allTasks, groups]
|
||||||
);
|
);
|
||||||
@@ -189,6 +140,7 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
setOverId(null);
|
setOverId(null);
|
||||||
|
setDropPosition(null);
|
||||||
|
|
||||||
if (!over || active.id === over.id) {
|
if (!over || active.id === over.id) {
|
||||||
return;
|
return;
|
||||||
@@ -204,86 +156,50 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the groups
|
// Find the active task's group
|
||||||
const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id));
|
const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id));
|
||||||
if (!activeGroup) {
|
if (!activeGroup) {
|
||||||
logger.error('Could not find active group for task:', activeId);
|
logger.error('Could not find active group for task:', activeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're dropping on a task, group, or empty group
|
// Only allow dropping on tasks in the same group
|
||||||
const overTask = allTasks.find(task => task.id === overId);
|
const overTask = allTasks.find(task => task.id === overId);
|
||||||
const overGroup = groups.find(group => group.id === overId);
|
if (!overTask) {
|
||||||
|
return;
|
||||||
// Check if dropping on empty group drop zone
|
}
|
||||||
const isEmptyGroupDrop = typeof overId === 'string' && overId.startsWith('empty-group-');
|
|
||||||
const emptyGroupId = isEmptyGroupDrop ? overId.replace('empty-group-', '') : null;
|
const overGroup = groups.find(group => group.taskIds.includes(overTask.id));
|
||||||
const emptyGroup = emptyGroupId ? groups.find(group => group.id === emptyGroupId) : null;
|
if (!overGroup || overGroup.id !== activeGroup.id) {
|
||||||
|
|
||||||
let targetGroup = overGroup || emptyGroup;
|
|
||||||
let insertIndex = 0;
|
|
||||||
|
|
||||||
if (overTask) {
|
|
||||||
// Dropping on a task
|
|
||||||
targetGroup = groups.find(group => group.taskIds.includes(overTask.id));
|
|
||||||
if (targetGroup) {
|
|
||||||
insertIndex = targetGroup.taskIds.indexOf(overTask.id);
|
|
||||||
}
|
|
||||||
} else if (overGroup) {
|
|
||||||
// Dropping on a group (at the end)
|
|
||||||
targetGroup = overGroup;
|
|
||||||
insertIndex = targetGroup.taskIds.length;
|
|
||||||
} else if (emptyGroup) {
|
|
||||||
// Dropping on an empty group
|
|
||||||
targetGroup = emptyGroup;
|
|
||||||
insertIndex = 0; // First position in empty group
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetGroup) {
|
|
||||||
logger.error('Could not find target group');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCrossGroup = activeGroup.id !== targetGroup.id;
|
|
||||||
const activeIndex = activeGroup.taskIds.indexOf(activeTask.id);
|
const activeIndex = activeGroup.taskIds.indexOf(activeTask.id);
|
||||||
|
const overIndex = activeGroup.taskIds.indexOf(overTask.id);
|
||||||
|
|
||||||
if (isCrossGroup) {
|
if (activeIndex !== overIndex) {
|
||||||
// Moving task between groups
|
// Reorder task within same group
|
||||||
console.log('Moving task between groups:', {
|
|
||||||
task: activeTask.name || activeTask.title,
|
|
||||||
from: activeGroup.title,
|
|
||||||
to: targetGroup.title,
|
|
||||||
newPosition: insertIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
// reorderTasksInGroup handles both same-group and cross-group moves
|
|
||||||
// No need for separate moveTaskBetweenGroups call
|
|
||||||
dispatch(
|
dispatch(
|
||||||
reorderTasksInGroup({
|
reorderTasksInGroup({
|
||||||
sourceTaskId: activeId as string,
|
sourceTaskId: activeId as string,
|
||||||
destinationTaskId: over.id as string,
|
destinationTaskId: overId as string,
|
||||||
sourceGroupId: activeGroup.id,
|
sourceGroupId: activeGroup.id,
|
||||||
destinationGroupId: targetGroup.id,
|
destinationGroupId: activeGroup.id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Emit socket event for persistence
|
// Calculate the final index after reordering for socket emission
|
||||||
emitTaskSortChange(activeId as string, activeGroup, targetGroup, insertIndex);
|
let finalIndex = overIndex;
|
||||||
} else {
|
if (activeIndex < overIndex) {
|
||||||
if (activeIndex !== insertIndex) {
|
// When dragging down, the task ends up just after the destination
|
||||||
// Reorder task within same group at drop position
|
finalIndex = overIndex;
|
||||||
dispatch(
|
} else {
|
||||||
reorderTasksInGroup({
|
// When dragging up, the task ends up at the destination position
|
||||||
sourceTaskId: activeId as string,
|
finalIndex = overIndex;
|
||||||
destinationTaskId: over.id as string,
|
|
||||||
sourceGroupId: activeGroup.id,
|
|
||||||
destinationGroupId: activeGroup.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Emit socket event for persistence
|
|
||||||
emitTaskSortChange(activeId as string, activeGroup, targetGroup, insertIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit socket event for persistence
|
||||||
|
emitTaskSortChange(activeId as string, activeGroup, finalIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[allTasks, groups, dispatch, emitTaskSortChange]
|
[allTasks, groups, dispatch, emitTaskSortChange]
|
||||||
@@ -292,6 +208,7 @@ export const useDragAndDrop = (allTasks: Task[], groups: TaskGroup[]) => {
|
|||||||
return {
|
return {
|
||||||
activeId,
|
activeId,
|
||||||
overId,
|
overId,
|
||||||
|
dropPosition,
|
||||||
handleDragStart,
|
handleDragStart,
|
||||||
handleDragOver,
|
handleDragOver,
|
||||||
handleDragEnd,
|
handleDragEnd,
|
||||||
|
|||||||
@@ -89,8 +89,30 @@ export const useTaskRowColumns = ({
|
|||||||
listeners,
|
listeners,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
}: UseTaskRowColumnsProps) => {
|
}: UseTaskRowColumnsProps) => {
|
||||||
const renderColumn = useCallback(
|
|
||||||
(columnId: string, width: string, isSticky?: boolean, index?: number) => {
|
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) {
|
switch (columnId) {
|
||||||
case 'dragHandle':
|
case 'dragHandle':
|
||||||
return (
|
return (
|
||||||
@@ -102,172 +124,245 @@ export const useTaskRowColumns = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return (
|
return (
|
||||||
<CheckboxColumn
|
<CheckboxColumn
|
||||||
width={width}
|
width={width}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onCheckboxChange={handleCheckboxChange}
|
onCheckboxChange={handleCheckboxChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'taskKey':
|
case 'taskKey':
|
||||||
return <TaskKeyColumn width={width} taskKey={task.task_key || ''} />;
|
return (
|
||||||
|
<TaskKeyColumn
|
||||||
|
width={width}
|
||||||
|
taskKey={task.task_key || ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case 'title':
|
case 'title':
|
||||||
|
return (
|
||||||
|
<TitleColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
isSubtask={isSubtask}
|
||||||
|
taskDisplayName={taskDisplayName}
|
||||||
|
editTaskName={editTaskName}
|
||||||
|
taskName={taskName}
|
||||||
|
onEditTaskName={setEditTaskName}
|
||||||
|
onTaskNameChange={setTaskName}
|
||||||
|
onTaskNameSave={handleTaskNameSave}
|
||||||
|
depth={depth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'description':
|
||||||
|
return (
|
||||||
|
<DescriptionColumn
|
||||||
|
width={width}
|
||||||
|
description={task.description || ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
return (
|
||||||
|
<StatusColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'assignees':
|
||||||
|
return (
|
||||||
|
<AssigneesColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
convertedTask={convertedTask}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'priority':
|
||||||
|
return (
|
||||||
|
<PriorityColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'dueDate':
|
||||||
|
return (
|
||||||
|
<DatePickerColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
field="dueDate"
|
||||||
|
formattedDate={formattedDates.due}
|
||||||
|
dateValue={dateValues.due}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
activeDatePicker={activeDatePicker}
|
||||||
|
onActiveDatePickerChange={setActiveDatePicker}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'startDate':
|
||||||
|
return (
|
||||||
|
<DatePickerColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
field="startDate"
|
||||||
|
formattedDate={formattedDates.start}
|
||||||
|
dateValue={dateValues.start}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
activeDatePicker={activeDatePicker}
|
||||||
|
onActiveDatePickerChange={setActiveDatePicker}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'progress':
|
||||||
|
return (
|
||||||
|
<ProgressColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'labels':
|
||||||
|
return (
|
||||||
|
<LabelsColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
labelsAdapter={labelsAdapter}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
visibleColumns={visibleColumns}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'phase':
|
||||||
|
return (
|
||||||
|
<PhaseColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'timeTracking':
|
||||||
|
return (
|
||||||
|
<TimeTrackingColumn
|
||||||
|
width={width}
|
||||||
|
taskId={task.id || ''}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'estimation':
|
||||||
|
return (
|
||||||
|
<EstimationColumn
|
||||||
|
width={width}
|
||||||
|
task={task}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'completedDate':
|
||||||
|
return (
|
||||||
|
<DateColumn
|
||||||
|
width={width}
|
||||||
|
formattedDate={formattedDates.completed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'createdDate':
|
||||||
|
return (
|
||||||
|
<DateColumn
|
||||||
|
width={width}
|
||||||
|
formattedDate={formattedDates.created}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'lastUpdated':
|
||||||
|
return (
|
||||||
|
<DateColumn
|
||||||
|
width={width}
|
||||||
|
formattedDate={formattedDates.updated}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'reporter':
|
||||||
|
return (
|
||||||
|
<ReporterColumn
|
||||||
|
width={width}
|
||||||
|
reporter={task.reporter || ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Handle custom columns
|
||||||
|
const column = visibleColumns.find(col => col.id === columnId);
|
||||||
|
if (column && (column.custom_column || column.isCustom) && updateTaskCustomColumnValue) {
|
||||||
return (
|
return (
|
||||||
<TitleColumn
|
<DragHandleColumn
|
||||||
width={width}
|
width={width}
|
||||||
task={task}
|
|
||||||
projectId={projectId}
|
|
||||||
isSubtask={isSubtask}
|
isSubtask={isSubtask}
|
||||||
taskDisplayName={taskDisplayName}
|
attributes={attributes}
|
||||||
editTaskName={editTaskName}
|
listeners={listeners}
|
||||||
taskName={taskName}
|
|
||||||
onEditTaskName={setEditTaskName}
|
|
||||||
onTaskNameChange={setTaskName}
|
|
||||||
onTaskNameSave={handleTaskNameSave}
|
|
||||||
depth={depth}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case 'description':
|
return null;
|
||||||
return <DescriptionColumn width={width} description={task.description || ''} />;
|
|
||||||
|
|
||||||
case 'status':
|
|
||||||
return (
|
|
||||||
<StatusColumn width={width} task={task} projectId={projectId} isDarkMode={isDarkMode} />
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'assignees':
|
|
||||||
return (
|
|
||||||
<AssigneesColumn
|
|
||||||
width={width}
|
|
||||||
task={task}
|
|
||||||
convertedTask={convertedTask}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'priority':
|
|
||||||
return (
|
|
||||||
<PriorityColumn
|
|
||||||
width={width}
|
|
||||||
task={task}
|
|
||||||
projectId={projectId}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'dueDate':
|
|
||||||
return (
|
|
||||||
<DatePickerColumn
|
|
||||||
width={width}
|
|
||||||
task={task}
|
|
||||||
field="dueDate"
|
|
||||||
formattedDate={formattedDates.due}
|
|
||||||
dateValue={dateValues.due}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
activeDatePicker={activeDatePicker}
|
|
||||||
onActiveDatePickerChange={setActiveDatePicker}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'startDate':
|
|
||||||
return (
|
|
||||||
<DatePickerColumn
|
|
||||||
width={width}
|
|
||||||
task={task}
|
|
||||||
field="startDate"
|
|
||||||
formattedDate={formattedDates.start}
|
|
||||||
dateValue={dateValues.start}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
activeDatePicker={activeDatePicker}
|
|
||||||
onActiveDatePickerChange={setActiveDatePicker}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'progress':
|
|
||||||
return <ProgressColumn width={width} task={task} />;
|
|
||||||
|
|
||||||
case 'labels':
|
|
||||||
return (
|
|
||||||
<LabelsColumn
|
|
||||||
width={width}
|
|
||||||
task={task}
|
|
||||||
labelsAdapter={labelsAdapter}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
visibleColumns={visibleColumns}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'phase':
|
|
||||||
return (
|
|
||||||
<PhaseColumn width={width} task={task} projectId={projectId} isDarkMode={isDarkMode} />
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'timeTracking':
|
|
||||||
return (
|
|
||||||
<TimeTrackingColumn width={width} taskId={task.id || ''} isDarkMode={isDarkMode} />
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'estimation':
|
|
||||||
return <EstimationColumn width={width} task={task} />;
|
|
||||||
|
|
||||||
case 'completedDate':
|
|
||||||
return <DateColumn width={width} formattedDate={formattedDates.completed} />;
|
|
||||||
|
|
||||||
case 'createdDate':
|
|
||||||
return <DateColumn width={width} formattedDate={formattedDates.created} />;
|
|
||||||
|
|
||||||
case 'lastUpdated':
|
|
||||||
return <DateColumn width={width} formattedDate={formattedDates.updated} />;
|
|
||||||
|
|
||||||
case 'reporter':
|
|
||||||
return <ReporterColumn width={width} reporter={task.reporter || ''} />;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Handle custom columns
|
|
||||||
const column = visibleColumns.find(col => col.id === columnId);
|
|
||||||
if (column && (column.custom_column || column.isCustom) && updateTaskCustomColumnValue) {
|
|
||||||
return (
|
|
||||||
<CustomColumn
|
|
||||||
width={width}
|
|
||||||
column={column}
|
|
||||||
task={task}
|
|
||||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[
|
|
||||||
task,
|
// Wrap content with sticky positioning if needed
|
||||||
projectId,
|
const content = renderColumnContent();
|
||||||
isSubtask,
|
if (isSticky) {
|
||||||
isSelected,
|
const hoverBg = rowBackgrounds?.hover || (isDarkMode ? '#2a2a2a' : '#f9fafb');
|
||||||
isDarkMode,
|
return (
|
||||||
visibleColumns,
|
<div
|
||||||
updateTaskCustomColumnValue,
|
style={{
|
||||||
taskDisplayName,
|
...wrapperStyle,
|
||||||
convertedTask,
|
'--hover-bg': hoverBg,
|
||||||
formattedDates,
|
} as React.CSSProperties}
|
||||||
dateValues,
|
className="border-r border-gray-200 dark:border-gray-700 overflow-hidden sticky-column-hover hover:bg-[var(--hover-bg)]"
|
||||||
labelsAdapter,
|
>
|
||||||
activeDatePicker,
|
{content}
|
||||||
setActiveDatePicker,
|
</div>
|
||||||
editTaskName,
|
);
|
||||||
taskName,
|
}
|
||||||
setEditTaskName,
|
|
||||||
setTaskName,
|
return content;
|
||||||
handleCheckboxChange,
|
}, [
|
||||||
handleTaskNameSave,
|
task,
|
||||||
handleTaskNameEdit,
|
projectId,
|
||||||
attributes,
|
isSubtask,
|
||||||
listeners,
|
isSelected,
|
||||||
]
|
isDarkMode,
|
||||||
);
|
visibleColumns,
|
||||||
|
updateTaskCustomColumnValue,
|
||||||
|
taskDisplayName,
|
||||||
|
convertedTask,
|
||||||
|
formattedDates,
|
||||||
|
dateValues,
|
||||||
|
labelsAdapter,
|
||||||
|
activeDatePicker,
|
||||||
|
setActiveDatePicker,
|
||||||
|
editTaskName,
|
||||||
|
taskName,
|
||||||
|
setEditTaskName,
|
||||||
|
setTaskName,
|
||||||
|
handleCheckboxChange,
|
||||||
|
handleTaskNameSave,
|
||||||
|
handleTaskNameEdit,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
depth,
|
||||||
|
]);
|
||||||
|
|
||||||
return { renderColumn };
|
return { renderColumn };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Task } from '@/types/task-management.types';
|
|||||||
import {
|
import {
|
||||||
updateTask,
|
updateTask,
|
||||||
selectCurrentGroupingV3,
|
selectCurrentGroupingV3,
|
||||||
|
selectGroups,
|
||||||
|
moveTaskBetweenGroups,
|
||||||
} from '@/features/task-management/task-management.slice';
|
} from '@/features/task-management/task-management.slice';
|
||||||
|
|
||||||
interface TaskStatusDropdownProps {
|
interface TaskStatusDropdownProps {
|
||||||
@@ -30,6 +32,7 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
|||||||
|
|
||||||
const statusList = useAppSelector(state => state.taskStatusReducer.status);
|
const statusList = useAppSelector(state => state.taskStatusReducer.status);
|
||||||
const currentGroupingV3 = useAppSelector(selectCurrentGroupingV3);
|
const currentGroupingV3 = useAppSelector(selectCurrentGroupingV3);
|
||||||
|
const groups = useAppSelector(selectGroups);
|
||||||
|
|
||||||
// Find current status details
|
// Find current status details
|
||||||
const currentStatus = useMemo(() => {
|
const currentStatus = useMemo(() => {
|
||||||
@@ -44,21 +47,53 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
|
|||||||
(statusId: string, statusName: string) => {
|
(statusId: string, statusName: string) => {
|
||||||
if (!task.id || !statusId || !connected) return;
|
if (!task.id || !statusId || !connected) return;
|
||||||
|
|
||||||
console.log('🎯 Status change initiated:', { taskId: task.id, statusId, statusName });
|
// Optimistic update: immediately update the task status in Redux for instant feedback
|
||||||
|
const updatedTask = {
|
||||||
|
...task,
|
||||||
|
status: statusId,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
dispatch(updateTask(updatedTask));
|
||||||
|
|
||||||
|
// Handle group movement if grouping by status
|
||||||
|
if (currentGroupingV3 === 'status' && groups && groups.length > 0) {
|
||||||
|
// Find current group containing the task
|
||||||
|
const currentGroup = groups.find(group => group.taskIds.includes(task.id));
|
||||||
|
|
||||||
|
// Find target group based on the new status ID
|
||||||
|
let targetGroup = groups.find(group => group.id === statusId);
|
||||||
|
|
||||||
|
// If not found by status ID, try matching with group value
|
||||||
|
if (!targetGroup) {
|
||||||
|
targetGroup = groups.find(group => group.groupValue === statusId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
||||||
|
// Move task between groups immediately for instant feedback
|
||||||
|
dispatch(
|
||||||
|
moveTaskBetweenGroups({
|
||||||
|
taskId: task.id,
|
||||||
|
sourceGroupId: currentGroup.id,
|
||||||
|
targetGroupId: targetGroup.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit socket event for server-side update and real-time sync
|
||||||
socket?.emit(
|
socket?.emit(
|
||||||
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
task_id: task.id,
|
task_id: task.id,
|
||||||
status_id: statusId,
|
status_id: statusId,
|
||||||
parent_task: null, // Assuming top-level tasks for now
|
parent_task: task.parent_task_id || null,
|
||||||
team_id: projectId, // Using projectId as teamId
|
team_id: projectId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
},
|
},
|
||||||
[task.id, connected, socket, projectId]
|
[task, connected, socket, projectId, dispatch, currentGroupingV3, groups]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate dropdown position and handle outside clicks
|
// Calculate dropdown position and handle outside clicks
|
||||||
|
|||||||
@@ -706,8 +706,23 @@ const taskManagementSlice = createSlice({
|
|||||||
const group = state.groups.find(g => g.id === sourceGroupId);
|
const group = state.groups.find(g => g.id === sourceGroupId);
|
||||||
if (group) {
|
if (group) {
|
||||||
const newTasks = Array.from(group.taskIds);
|
const newTasks = Array.from(group.taskIds);
|
||||||
const [removed] = newTasks.splice(newTasks.indexOf(sourceTaskId), 1);
|
const sourceIndex = newTasks.indexOf(sourceTaskId);
|
||||||
newTasks.splice(newTasks.indexOf(destinationTaskId), 0, removed);
|
const destinationIndex = newTasks.indexOf(destinationTaskId);
|
||||||
|
|
||||||
|
// Remove the task from its current position
|
||||||
|
const [removed] = newTasks.splice(sourceIndex, 1);
|
||||||
|
|
||||||
|
// Calculate the insertion index
|
||||||
|
let insertIndex = destinationIndex;
|
||||||
|
if (sourceIndex < destinationIndex) {
|
||||||
|
// When dragging down, we need to insert after the destination
|
||||||
|
insertIndex = destinationIndex;
|
||||||
|
} else {
|
||||||
|
// When dragging up, we insert before the destination
|
||||||
|
insertIndex = destinationIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
newTasks.splice(insertIndex, 0, removed);
|
||||||
group.taskIds = newTasks;
|
group.taskIds = newTasks;
|
||||||
|
|
||||||
// Update order for affected tasks using the appropriate sort field
|
// Update order for affected tasks using the appropriate sort field
|
||||||
|
|||||||
@@ -244,47 +244,18 @@ export const useTaskSocketHandlers = () => {
|
|||||||
// Find current group containing the task
|
// Find current group containing the task
|
||||||
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
|
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
|
||||||
|
|
||||||
// Find target group based on new status value with multiple matching strategies
|
// Find target group based on the actual status ID from response
|
||||||
let targetGroup = groups.find(group => group.groupValue === newStatusValue);
|
let targetGroup = groups.find(group => group.id === response.status_id);
|
||||||
|
|
||||||
// If not found, try case-insensitive matching
|
// If not found by status ID, try matching with group value
|
||||||
if (!targetGroup) {
|
if (!targetGroup) {
|
||||||
targetGroup = groups.find(
|
targetGroup = groups.find(group => group.groupValue === response.status_id);
|
||||||
group => group.groupValue?.toLowerCase() === newStatusValue.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If still not found, try matching with title
|
// If still not found, try matching by status name (fallback)
|
||||||
if (!targetGroup) {
|
if (!targetGroup && response.status) {
|
||||||
targetGroup = groups.find(
|
targetGroup = groups.find(group =>
|
||||||
group => group.title?.toLowerCase() === newStatusValue.toLowerCase()
|
group.title?.toLowerCase() === response.status.toLowerCase()
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still not found, try matching common status patterns
|
|
||||||
if (!targetGroup && newStatusValue === 'todo') {
|
|
||||||
targetGroup = groups.find(
|
|
||||||
group =>
|
|
||||||
group.title?.toLowerCase().includes('todo') ||
|
|
||||||
group.title?.toLowerCase().includes('to do') ||
|
|
||||||
group.title?.toLowerCase().includes('pending') ||
|
|
||||||
group.groupValue?.toLowerCase().includes('todo')
|
|
||||||
);
|
|
||||||
} else if (!targetGroup && newStatusValue === 'doing') {
|
|
||||||
targetGroup = groups.find(
|
|
||||||
group =>
|
|
||||||
group.title?.toLowerCase().includes('doing') ||
|
|
||||||
group.title?.toLowerCase().includes('progress') ||
|
|
||||||
group.title?.toLowerCase().includes('active') ||
|
|
||||||
group.groupValue?.toLowerCase().includes('doing')
|
|
||||||
);
|
|
||||||
} else if (!targetGroup && newStatusValue === 'done') {
|
|
||||||
targetGroup = groups.find(
|
|
||||||
group =>
|
|
||||||
group.title?.toLowerCase().includes('done') ||
|
|
||||||
group.title?.toLowerCase().includes('complete') ||
|
|
||||||
group.title?.toLowerCase().includes('finish') ||
|
|
||||||
group.groupValue?.toLowerCase().includes('done')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,14 +269,11 @@ export const useTaskSocketHandlers = () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (!targetGroup) {
|
} else if (!targetGroup) {
|
||||||
console.log('❌ Target group not found for status:', newStatusValue);
|
// Fallback: refetch tasks to ensure consistency
|
||||||
} else if (!currentGroup) {
|
if (projectId) {
|
||||||
console.log('❌ Current group not found for task:', response.id);
|
dispatch(fetchTasksV3(projectId));
|
||||||
} else {
|
}
|
||||||
console.log('🔧 No group movement needed - task already in correct group');
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log('🔧 Not grouped by status, skipping group movement');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,60 +1,18 @@
|
|||||||
import { Col, ConfigProvider, Layout } from '@/shared/antd-imports';
|
import { ConfigProvider, Layout } from '@/shared/antd-imports';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import { memo, useMemo, useEffect, useRef } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useMediaQuery } from 'react-responsive';
|
|
||||||
|
|
||||||
import Navbar from '../features/navbar/navbar';
|
import Navbar from '../features/navbar/navbar';
|
||||||
import { useAppSelector } from '../hooks/useAppSelector';
|
import { useAppSelector } from '../hooks/useAppSelector';
|
||||||
import { colors } from '../styles/colors';
|
import { colors } from '../styles/colors';
|
||||||
|
|
||||||
import { useRenderPerformance } from '@/utils/performance';
|
|
||||||
import { DynamicCSSLoader, LayoutStabilizer } from '@/utils/css-optimizations';
|
|
||||||
|
|
||||||
const MainLayout = memo(() => {
|
const MainLayout = memo(() => {
|
||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
const isDesktop = useMediaQuery({ query: '(min-width: 1024px)' });
|
const location = useLocation();
|
||||||
const layoutRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
const isProjectView = location.pathname.includes('/projects/') &&
|
||||||
|
!location.pathname.endsWith('/projects');
|
||||||
|
|
||||||
// Performance monitoring in development
|
|
||||||
useRenderPerformance('MainLayout');
|
|
||||||
|
|
||||||
// Apply layout optimizations
|
|
||||||
useEffect(() => {
|
|
||||||
if (layoutRef.current) {
|
|
||||||
// Prevent layout shifts in main content area
|
|
||||||
LayoutStabilizer.applyContainment(layoutRef.current, 'layout');
|
|
||||||
|
|
||||||
// Load non-critical CSS dynamically
|
|
||||||
DynamicCSSLoader.loadCSS('/styles/non-critical.css', {
|
|
||||||
priority: 'low',
|
|
||||||
media: 'all',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Memoize styles to prevent object recreation on every render
|
|
||||||
const headerStyles = useMemo(
|
|
||||||
() => ({
|
|
||||||
zIndex: 999,
|
|
||||||
position: 'fixed' as const,
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 0,
|
|
||||||
borderBottom: themeMode === 'dark' ? '1px solid #303030' : 'none',
|
|
||||||
}),
|
|
||||||
[themeMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
const contentStyles = useMemo(
|
|
||||||
() => ({
|
|
||||||
paddingInline: isDesktop ? 64 : 24,
|
|
||||||
overflowX: 'hidden' as const,
|
|
||||||
}),
|
|
||||||
[isDesktop]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize theme configuration
|
|
||||||
const themeConfig = useMemo(
|
const themeConfig = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
components: {
|
components: {
|
||||||
@@ -67,27 +25,19 @@ const MainLayout = memo(() => {
|
|||||||
[themeMode]
|
[themeMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize header className
|
|
||||||
const headerClassName = useMemo(
|
|
||||||
() => `shadow-md ${themeMode === 'dark' ? '' : 'shadow-[#18181811]'}`,
|
|
||||||
[themeMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={themeConfig}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<Layout ref={layoutRef} style={{ minHeight: '100vh' }} className="prevent-layout-shift">
|
<Layout className="min-h-screen">
|
||||||
<Layout.Header className={`${headerClassName} gpu-accelerated`} style={headerStyles}>
|
<Layout.Header
|
||||||
|
className={`sticky top-0 z-[999] flex items-center p-0 shadow-md ${
|
||||||
|
themeMode === 'dark' ? 'border-b border-[#303030]' : 'shadow-[#18181811]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
|
|
||||||
<Layout.Content className="layout-contained">
|
<Layout.Content className={`px-4 sm:px-8 lg:px-12 xl:px-16 ${!isProjectView ? 'overflow-x-hidden max-w-[1400px]' : ''} mx-auto w-full`}>
|
||||||
<Col
|
<Outlet />
|
||||||
xxl={{ span: 18, offset: 3, flex: '100%' }}
|
|
||||||
style={contentStyles}
|
|
||||||
className="task-content-container"
|
|
||||||
>
|
|
||||||
<Outlet />
|
|
||||||
</Col>
|
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ const HomePage = memo(() => {
|
|||||||
}, [isDesktop, isOwnerOrAdmin]);
|
}, [isDesktop, isOwnerOrAdmin]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-24 min-h-[90vh]">
|
<div className="my-8 min-h-[90vh]">
|
||||||
<Col className="flex flex-col gap-6">
|
<Col className="flex flex-col gap-6">
|
||||||
<GreetingWithTime />
|
<GreetingWithTime />
|
||||||
{CreateProjectButtonComponent}
|
{CreateProjectButtonComponent}
|
||||||
@@ -113,13 +113,13 @@ const HomePage = memo(() => {
|
|||||||
<Col xs={24} lg={16}>
|
<Col xs={24} lg={16}>
|
||||||
<Flex vertical gap={24}>
|
<Flex vertical gap={24}>
|
||||||
<TasksList />
|
<TasksList />
|
||||||
|
|
||||||
<TodoList />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} lg={8}>
|
<Col xs={24} lg={8}>
|
||||||
<Flex vertical gap={24}>
|
<Flex vertical gap={24}>
|
||||||
|
<TodoList />
|
||||||
|
|
||||||
<UserActivityFeed />
|
<UserActivityFeed />
|
||||||
|
|
||||||
<RecentAndFavouriteProjectList />
|
<RecentAndFavouriteProjectList />
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CheckCircleOutlined, SyncOutlined } from '@/shared/antd-imports';
|
import { CheckCircleOutlined, SyncOutlined, DownOutlined, RightOutlined } from '@/shared/antd-imports';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import Form from 'antd/es/form';
|
import Form from 'antd/es/form';
|
||||||
import Input, { InputRef } from 'antd/es/input';
|
import Input, { InputRef } from 'antd/es/input';
|
||||||
import Flex from 'antd/es/flex';
|
import Flex from 'antd/es/flex';
|
||||||
import Card from 'antd/es/card';
|
import Card from 'antd/es/card';
|
||||||
|
import Collapse from 'antd/es/collapse';
|
||||||
import ConfigProvider from 'antd/es/config-provider';
|
import ConfigProvider from 'antd/es/config-provider';
|
||||||
import Table, { TableProps } from 'antd/es/table';
|
import Table, { TableProps } from 'antd/es/table';
|
||||||
import Tooltip from 'antd/es/tooltip';
|
import Tooltip from 'antd/es/tooltip';
|
||||||
@@ -23,6 +24,7 @@ import { useCreatePersonalTaskMutation } from '@/api/home-page/home-page.api.ser
|
|||||||
|
|
||||||
const TodoList = () => {
|
const TodoList = () => {
|
||||||
const [isAlertShowing, setIsAlertShowing] = useState(false);
|
const [isAlertShowing, setIsAlertShowing] = useState(false);
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { t } = useTranslation('home');
|
const { t } = useTranslation('home');
|
||||||
|
|
||||||
@@ -97,73 +99,109 @@ const TodoList = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card style={{ width: '100%' }} bodyStyle={{ padding: 0 }}>
|
||||||
title={
|
<style>{`
|
||||||
<Typography.Title level={5} style={{ marginBlockEnd: 0 }}>
|
.todo-collapse .ant-collapse-header {
|
||||||
{t('home:todoList.title')} ({data?.body.length})
|
display: flex !important;
|
||||||
</Typography.Title>
|
align-items: center !important;
|
||||||
}
|
padding: 12px 16px !important;
|
||||||
extra={
|
}
|
||||||
<Tooltip title={t('home:todoList.refreshTasks')}>
|
.todo-collapse .ant-collapse-expand-icon {
|
||||||
<Button shape="circle" icon={<SyncOutlined spin={isFetching} />} onClick={refetch} />
|
margin-right: 8px !important;
|
||||||
</Tooltip>
|
display: flex !important;
|
||||||
}
|
align-items: center !important;
|
||||||
style={{ width: '100%' }}
|
}
|
||||||
>
|
`}</style>
|
||||||
<div>
|
<Collapse
|
||||||
<Form form={form} onFinish={handleTodoSubmit}>
|
defaultActiveKey={[]}
|
||||||
<Form.Item name="name">
|
ghost
|
||||||
<Flex vertical>
|
size="small"
|
||||||
<Input
|
className="todo-collapse"
|
||||||
ref={todoInputRef}
|
expandIcon={({ isActive }) =>
|
||||||
placeholder={t('home:todoList.addTask')}
|
isActive ? <DownOutlined /> : <RightOutlined />
|
||||||
onChange={e => {
|
}
|
||||||
const inputValue = e.currentTarget.value;
|
onChange={(keys) => {
|
||||||
|
setIsCollapsed(keys.length === 0);
|
||||||
|
}}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
label: (
|
||||||
|
<Flex style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||||
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||||
|
{t('home:todoList.title')} ({data?.body.length})
|
||||||
|
</Typography.Title>
|
||||||
|
<Tooltip title={t('home:todoList.refreshTasks')}>
|
||||||
|
<Button
|
||||||
|
shape="circle"
|
||||||
|
icon={<SyncOutlined spin={isFetching} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<div style={{ padding: '0 16px 16px 16px' }}>
|
||||||
|
<Form form={form} onFinish={handleTodoSubmit}>
|
||||||
|
<Form.Item name="name">
|
||||||
|
<Flex vertical>
|
||||||
|
<Input
|
||||||
|
ref={todoInputRef}
|
||||||
|
placeholder={t('home:todoList.addTask')}
|
||||||
|
onChange={e => {
|
||||||
|
const inputValue = e.currentTarget.value;
|
||||||
|
|
||||||
if (inputValue.length >= 1) setIsAlertShowing(true);
|
if (inputValue.length >= 1) setIsAlertShowing(true);
|
||||||
else if (inputValue === '') setIsAlertShowing(false);
|
else if (inputValue === '') setIsAlertShowing(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isAlertShowing && (
|
{isAlertShowing && (
|
||||||
<Alert
|
<Alert
|
||||||
message={
|
message={
|
||||||
<Typography.Text style={{ fontSize: 11 }}>
|
<Typography.Text style={{ fontSize: 11 }}>
|
||||||
{t('home:todoList.pressEnter')} <strong>Enter</strong>{' '}
|
{t('home:todoList.pressEnter')} <strong>Enter</strong>{' '}
|
||||||
{t('home:todoList.toCreate')}
|
{t('home:todoList.toCreate')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
}
|
}
|
||||||
type="info"
|
type="info"
|
||||||
style={{
|
style={{
|
||||||
width: 'fit-content',
|
width: 'fit-content',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
padding: '0 6px',
|
padding: '0 6px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div style={{ maxHeight: 300, overflow: 'auto' }}>
|
<div style={{ maxHeight: 300, overflow: 'auto' }}>
|
||||||
{data?.body.length === 0 ? (
|
{data?.body.length === 0 ? (
|
||||||
<EmptyListPlaceholder
|
<EmptyListPlaceholder
|
||||||
imageSrc="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/empty-box.webp"
|
imageSrc="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/empty-box.webp"
|
||||||
text={t('home:todoList.noTasks')}
|
text={t('home:todoList.noTasks')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Table
|
<Table
|
||||||
className="custom-two-colors-row-table"
|
className="custom-two-colors-row-table"
|
||||||
rowKey={record => record.id || ''}
|
rowKey={record => record.id || ''}
|
||||||
dataSource={data?.body}
|
dataSource={data?.body}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
size="small"
|
size="small"
|
||||||
loading={isFetching}
|
loading={isFetching}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -820,7 +820,7 @@ const ProjectList: React.FC = () => {
|
|||||||
}, [loadingProjects, isFetchingProjects, viewMode, groupedProjects.loading, isLoading]);
|
}, [loadingProjects, isFetchingProjects, viewMode, groupedProjects.loading, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBlock: 65, minHeight: '90vh' }}>
|
<div style={{ minHeight: '90vh' }}>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
className="site-page-header"
|
className="site-page-header"
|
||||||
title={`${projectCount} ${t('projects')}`}
|
title={`${projectCount} ${t('projects')}`}
|
||||||
|
|||||||
@@ -369,14 +369,14 @@ const ProjectView = React.memo(() => {
|
|||||||
// Show loading state while project is being fetched or translations are loading
|
// Show loading state while project is being fetched or translations are loading
|
||||||
if (projectLoading || !isInitialized || !translationsReady) {
|
if (projectLoading || !isInitialized || !translationsReady) {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBlockStart: 70, marginBlockEnd: 12, minHeight: '80vh' }}>
|
<div style={{ marginBlockEnd: 12, minHeight: '80vh' }}>
|
||||||
<SuspenseFallback />
|
<SuspenseFallback />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBlockStart: 70, marginBlockEnd: 12, minHeight: '80vh' }}>
|
<div style={{ marginBlockEnd: 12, minHeight: '80vh' }}>
|
||||||
<ProjectViewHeader />
|
<ProjectViewHeader />
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const Schedule: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBlockStart: 65, minHeight: '90vh' }}>
|
<div style={{ minHeight: '90vh' }}>
|
||||||
<Flex align="center" justify="space-between">
|
<Flex align="center" justify="space-between">
|
||||||
<Flex
|
<Flex
|
||||||
gap={16}
|
gap={16}
|
||||||
|
|||||||
Reference in New Issue
Block a user