feat(task-list): implement drag-and-drop functionality for task reordering
- Integrated drag-and-drop capabilities in the task list using `@dnd-kit` for improved user experience. - Created a `DraggableRow` component to handle individual task dragging and dropping. - Updated task list rendering to support dynamic reordering and socket integration for backend persistence. - Enhanced task selection and hover effects for better visual feedback during drag operations. - Refactored task list components to streamline rendering and improve performance.
This commit is contained in:
@@ -97,30 +97,28 @@ const InfoTabFooter = () => {
|
||||
// mentions options
|
||||
const mentionsOptions =
|
||||
members?.map(member => ({
|
||||
value: member.id,
|
||||
value: member.name,
|
||||
label: member.name,
|
||||
key: member.id,
|
||||
})) ?? [];
|
||||
|
||||
const memberSelectHandler = useCallback((member: IMentionMemberSelectOption) => {
|
||||
console.log('member', member);
|
||||
if (!member?.value || !member?.label) return;
|
||||
|
||||
// Find the member ID from the members list using the name
|
||||
const selectedMember = members.find(m => m.name === member.value);
|
||||
if (!selectedMember) return;
|
||||
|
||||
// Add to selected members if not already present
|
||||
setSelectedMembers(prev =>
|
||||
prev.some(mention => mention.team_member_id === member.value)
|
||||
prev.some(mention => mention.team_member_id === selectedMember.id)
|
||||
? prev
|
||||
: [...prev, { team_member_id: member.value, name: member.label }]
|
||||
: [...prev, { team_member_id: selectedMember.id!, name: selectedMember.name! }]
|
||||
);
|
||||
|
||||
setCommentValue(prev => {
|
||||
const parts = prev.split('@');
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const mentionText = member.label;
|
||||
// Keep only the part before the @ and add the new mention
|
||||
return prev.slice(0, prev.length - lastPart.length) + mentionText;
|
||||
});
|
||||
}, []);
|
||||
}, [members]);
|
||||
|
||||
const handleCommentChange = useCallback((value: string) => {
|
||||
// Only update the value without trying to replace mentions
|
||||
setCommentValue(value);
|
||||
setCharacterLength(value.trim().length);
|
||||
}, []);
|
||||
@@ -275,6 +273,12 @@ const InfoTabFooter = () => {
|
||||
maxLength={5000}
|
||||
onClick={() => setIsCommentBoxExpand(true)}
|
||||
onChange={e => setCharacterLength(e.length)}
|
||||
prefix="@"
|
||||
filterOption={(input, option) => {
|
||||
if (!input) return true;
|
||||
const optionLabel = (option as any)?.label || '';
|
||||
return optionLabel.toLowerCase().includes(input.toLowerCase());
|
||||
}}
|
||||
style={{
|
||||
minHeight: 60,
|
||||
resize: 'none',
|
||||
@@ -371,7 +375,11 @@ const InfoTabFooter = () => {
|
||||
onSelect={option => memberSelectHandler(option as IMentionMemberSelectOption)}
|
||||
onChange={handleCommentChange}
|
||||
prefix="@"
|
||||
split=""
|
||||
filterOption={(input, option) => {
|
||||
if (!input) return true;
|
||||
const optionLabel = (option as any)?.label || '';
|
||||
return optionLabel.toLowerCase().includes(input.toLowerCase());
|
||||
}}
|
||||
style={{
|
||||
minHeight: 100,
|
||||
maxHeight: 200,
|
||||
|
||||
@@ -57,20 +57,31 @@ const AddTaskInlineForm = ({ t, calendarView }: AddTaskInlineFormProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const calculateEndDate = (dueDate: string): Date | undefined => {
|
||||
const calculateEndDate = (dueDate: string): string | undefined => {
|
||||
const today = new Date();
|
||||
let targetDate: Date;
|
||||
|
||||
switch (dueDate) {
|
||||
case 'Today':
|
||||
return today;
|
||||
targetDate = new Date(today);
|
||||
break;
|
||||
case 'Tomorrow':
|
||||
return new Date(today.setDate(today.getDate() + 1));
|
||||
targetDate = new Date(today);
|
||||
targetDate.setDate(today.getDate() + 1);
|
||||
break;
|
||||
case 'Next Week':
|
||||
return new Date(today.setDate(today.getDate() + 7));
|
||||
targetDate = new Date(today);
|
||||
targetDate.setDate(today.getDate() + 7);
|
||||
break;
|
||||
case 'Next Month':
|
||||
return new Date(today.setMonth(today.getMonth() + 1));
|
||||
targetDate = new Date(today);
|
||||
targetDate.setMonth(today.getMonth() + 1);
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return targetDate.toISOString().split('T')[0]; // Returns YYYY-MM-DD format
|
||||
};
|
||||
|
||||
const projectOptions = [
|
||||
@@ -82,12 +93,16 @@ const AddTaskInlineForm = ({ t, calendarView }: AddTaskInlineFormProps) => {
|
||||
];
|
||||
|
||||
const handleTaskSubmit = (values: { name: string; project: string; dueDate: string }) => {
|
||||
const newTask: IHomeTaskCreateRequest = {
|
||||
const endDate = calendarView
|
||||
? homeTasksConfig.selected_date?.format('YYYY-MM-DD')
|
||||
: calculateEndDate(values.dueDate);
|
||||
|
||||
const newTask = {
|
||||
name: values.name,
|
||||
project_id: values.project,
|
||||
reporter_id: currentSession?.id,
|
||||
team_id: currentSession?.team_id,
|
||||
end_date: (calendarView ? homeTasksConfig.selected_date?.format('YYYY-MM-DD') : calculateEndDate(values.dueDate)),
|
||||
end_date: endDate || new Date().toISOString().split('T')[0], // Fallback to today if undefined
|
||||
};
|
||||
|
||||
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(newTask));
|
||||
|
||||
@@ -1,12 +1,110 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Checkbox, Flex, Tag, Tooltip } from 'antd';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { HolderOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragStartEvent,
|
||||
DragOverlay,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
KeyboardSensor,
|
||||
TouchSensor,
|
||||
UniqueIdentifier,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
useSortable,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { reorderTasks } from '@/features/tasks/tasks.slice';
|
||||
import { evt_project_task_list_drag_and_move } from '@/shared/worklenz-analytics-events';
|
||||
|
||||
// Draggable Row Component
|
||||
interface DraggableRowProps {
|
||||
task: IProjectTask;
|
||||
visibleColumns: Array<{ key: string; width: number }>;
|
||||
renderCell: (columnKey: string | number, task: IProjectTask, isSubtask?: boolean) => React.ReactNode;
|
||||
hoverRow: string | null;
|
||||
onRowHover: (taskId: string | null) => void;
|
||||
isSubtask?: boolean;
|
||||
}
|
||||
|
||||
const DraggableRow = ({
|
||||
task,
|
||||
visibleColumns,
|
||||
renderCell,
|
||||
hoverRow,
|
||||
onRowHover,
|
||||
isSubtask = false
|
||||
}: DraggableRowProps) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: task.id as UniqueIdentifier,
|
||||
data: {
|
||||
type: 'task',
|
||||
task,
|
||||
},
|
||||
disabled: isSubtask, // Disable drag for subtasks
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
zIndex: isDragging ? 1000 : 'auto',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="flex w-full border-b hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
onMouseEnter={() => onRowHover(task.id)}
|
||||
onMouseLeave={() => onRowHover(null)}
|
||||
>
|
||||
<div className="sticky left-0 z-10 w-8 flex items-center justify-center">
|
||||
{!isSubtask && (
|
||||
<div {...attributes} {...listeners} className="cursor-grab active:cursor-grabbing">
|
||||
<HolderOutlined />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{visibleColumns.map(column => (
|
||||
<div
|
||||
key={column.key}
|
||||
className={`flex items-center px-3 border-r ${
|
||||
hoverRow === task.id ? 'bg-gray-50 dark:bg-gray-800' : ''
|
||||
}`}
|
||||
style={{ width: column.width }}
|
||||
>
|
||||
{renderCell(column.key, task, isSubtask)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TaskListTable = ({
|
||||
taskListGroup,
|
||||
tableId,
|
||||
visibleColumns,
|
||||
onTaskSelect,
|
||||
onTaskExpand,
|
||||
@@ -18,11 +116,38 @@ const TaskListTable = ({
|
||||
onTaskExpand?: (taskId: string) => void;
|
||||
}) => {
|
||||
const [hoverRow, setHoverRow] = useState<string | null>(null);
|
||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
||||
const tableRef = useRef<HTMLDivElement | null>(null);
|
||||
const parentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||
const groupBy = useAppSelector(state => state.taskReducer.groupBy);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { socket } = useSocket();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||
|
||||
// Memoize all tasks including subtasks for virtualization
|
||||
// Configure sensors for drag and drop
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 250,
|
||||
tolerance: 5,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Memoize all tasks including subtasks
|
||||
const flattenedTasks = useMemo(() => {
|
||||
return taskListGroup.tasks.reduce((acc: IProjectTask[], task: IProjectTask) => {
|
||||
acc.push(task);
|
||||
@@ -33,13 +158,10 @@ const TaskListTable = ({
|
||||
}, []);
|
||||
}, [taskListGroup.tasks]);
|
||||
|
||||
// Virtual row renderer
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: flattenedTasks.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 42, // row height
|
||||
overscan: 5,
|
||||
});
|
||||
// Get only main tasks for sortable context (exclude subtasks)
|
||||
const mainTasks = useMemo(() => {
|
||||
return taskListGroup.tasks.filter(task => !task.isSubtask);
|
||||
}, [taskListGroup.tasks]);
|
||||
|
||||
// Memoize cell render functions
|
||||
const renderCell = useCallback(
|
||||
@@ -54,7 +176,7 @@ const TaskListTable = ({
|
||||
);
|
||||
},
|
||||
task: () => (
|
||||
<Flex align="center" className="pl-2">
|
||||
<Flex align="center" className={isSubtask ? "pl-6" : "pl-2"}>
|
||||
{task.name}
|
||||
</Flex>
|
||||
),
|
||||
@@ -66,6 +188,77 @@ const TaskListTable = ({
|
||||
[]
|
||||
);
|
||||
|
||||
// Handle drag start
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
setActiveId(event.active.id);
|
||||
document.body.style.cursor = 'grabbing';
|
||||
}, []);
|
||||
|
||||
// Handle drag end with socket integration
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
setActiveId(null);
|
||||
document.body.style.cursor = '';
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeIndex = mainTasks.findIndex(task => task.id === active.id);
|
||||
const overIndex = mainTasks.findIndex(task => task.id === over.id);
|
||||
|
||||
if (activeIndex !== -1 && overIndex !== -1) {
|
||||
const activeTask = mainTasks[activeIndex];
|
||||
const overTask = mainTasks[overIndex];
|
||||
|
||||
// Create updated task arrays
|
||||
const updatedTasks = [...mainTasks];
|
||||
updatedTasks.splice(activeIndex, 1);
|
||||
updatedTasks.splice(overIndex, 0, activeTask);
|
||||
|
||||
// Dispatch Redux action for optimistic update
|
||||
dispatch(reorderTasks({
|
||||
activeGroupId: tableId,
|
||||
overGroupId: tableId,
|
||||
fromIndex: activeIndex,
|
||||
toIndex: overIndex,
|
||||
task: activeTask,
|
||||
updatedSourceTasks: updatedTasks,
|
||||
updatedTargetTasks: updatedTasks,
|
||||
}));
|
||||
|
||||
// Emit socket event for backend persistence
|
||||
if (socket && projectId && currentSession?.team_id) {
|
||||
const toPos = overTask?.sort_order || mainTasks[mainTasks.length - 1]?.sort_order || -1;
|
||||
|
||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||
project_id: projectId,
|
||||
from_index: activeTask.sort_order,
|
||||
to_index: toPos,
|
||||
to_last_index: overIndex === mainTasks.length - 1,
|
||||
from_group: tableId,
|
||||
to_group: tableId,
|
||||
group_by: groupBy,
|
||||
task: activeTask,
|
||||
team_id: currentSession.team_id,
|
||||
});
|
||||
|
||||
// Track analytics event
|
||||
trackMixpanelEvent(evt_project_task_list_drag_and_move);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
mainTasks,
|
||||
tableId,
|
||||
dispatch,
|
||||
socket,
|
||||
projectId,
|
||||
currentSession?.team_id,
|
||||
groupBy,
|
||||
trackMixpanelEvent
|
||||
]);
|
||||
|
||||
// Memoize header rendering
|
||||
const TableHeader = useMemo(
|
||||
() => (
|
||||
@@ -94,48 +287,55 @@ const TaskListTable = ({
|
||||
target.classList.toggle('show-shadow', hasHorizontalShadow);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="h-[400px] overflow-auto" onScroll={handleScroll}>
|
||||
{TableHeader}
|
||||
// Find active task for drag overlay
|
||||
const activeTask = activeId ? flattenedTasks.find(task => task.id === activeId) : null;
|
||||
|
||||
<div
|
||||
ref={tableRef}
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div ref={parentRef} className="h-[400px] overflow-auto" onScroll={handleScroll}>
|
||||
{TableHeader}
|
||||
|
||||
<SortableContext items={mainTasks.map(task => task.id)} strategy={verticalListSortingStrategy}>
|
||||
<div ref={tableRef} style={{ width: '100%' }}>
|
||||
{flattenedTasks.map((task, index) => (
|
||||
<DraggableRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
visibleColumns={visibleColumns}
|
||||
renderCell={renderCell}
|
||||
hoverRow={hoverRow}
|
||||
onRowHover={setHoverRow}
|
||||
isSubtask={task.isSubtask}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</div>
|
||||
|
||||
<DragOverlay
|
||||
dropAnimation={{
|
||||
duration: 200,
|
||||
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map(virtualRow => {
|
||||
const task = flattenedTasks[virtualRow.index];
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className="absolute top-0 left-0 flex w-full border-b hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
style={{
|
||||
height: 42,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="sticky left-0 z-10 w-8 flex items-center justify-center">
|
||||
{/* <Checkbox checked={task.selected} /> */}
|
||||
</div>
|
||||
{visibleColumns.map(column => (
|
||||
<div
|
||||
key={column.key}
|
||||
className={`flex items-center px-3 border-r ${
|
||||
hoverRow === task.id ? 'bg-gray-50 dark:bg-gray-800' : ''
|
||||
}`}
|
||||
style={{ width: column.width }}
|
||||
>
|
||||
{renderCell(column.key, task, task.is_sub_task)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{activeTask && (
|
||||
<div className="bg-white dark:bg-gray-800 shadow-lg rounded border">
|
||||
<DraggableRow
|
||||
task={activeTask}
|
||||
visibleColumns={visibleColumns}
|
||||
renderCell={renderCell}
|
||||
hoverRow={null}
|
||||
onRowHover={() => {}}
|
||||
isSubtask={activeTask.isSubtask}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Row,
|
||||
Column,
|
||||
} from '@tanstack/react-table';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import React from 'react';
|
||||
@@ -78,19 +77,6 @@ const TaskListCustom: React.FC<TaskListCustomProps> = ({ tasks, color, groupId,
|
||||
|
||||
const { rows } = table.getRowModel();
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
getScrollElement: () => tableContainerRef.current,
|
||||
estimateSize: () => 50,
|
||||
overscan: 20,
|
||||
});
|
||||
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
const totalSize = rowVirtualizer.getTotalSize();
|
||||
const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0;
|
||||
const paddingBottom =
|
||||
virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0;
|
||||
|
||||
const columnToggleItems = columns.map(column => ({
|
||||
key: column.id as string,
|
||||
label: (
|
||||
@@ -125,6 +111,7 @@ const TaskListCustom: React.FC<TaskListCustomProps> = ({ tasks, color, groupId,
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
maxHeight: '100%',
|
||||
}}
|
||||
>
|
||||
@@ -161,80 +148,75 @@ const TaskListCustom: React.FC<TaskListCustomProps> = ({ tasks, color, groupId,
|
||||
))}
|
||||
</div>
|
||||
<div className="table-body">
|
||||
{paddingTop > 0 && <div style={{ height: `${paddingTop}px` }} />}
|
||||
{virtualRows.map(virtualRow => {
|
||||
const row = rows[virtualRow.index];
|
||||
return (
|
||||
<React.Fragment key={row.id}>
|
||||
<div
|
||||
className="table-row"
|
||||
style={{
|
||||
'&:hover div': {
|
||||
background: `${token.colorFillAlter} !important`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell, index) => (
|
||||
<div
|
||||
key={cell.id}
|
||||
className={`${cell.column.getIsPinned() === 'left' ? 'sticky left-0 z-10' : ''}`}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
position: index < 2 ? 'sticky' : 'relative',
|
||||
left: 'auto',
|
||||
background: token.colorBgContainer,
|
||||
color: token.colorText,
|
||||
height: '42px',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: '8px 0px 8px 8px',
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{expandedRows[row.id] &&
|
||||
row.original.sub_tasks?.map(subTask => (
|
||||
<div
|
||||
key={subTask.task_key}
|
||||
className="table-row"
|
||||
style={{
|
||||
'&:hover div': {
|
||||
background: `${token.colorFillAlter} !important`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{columns.map((col, index) => (
|
||||
<div
|
||||
key={`${subTask.task_key}-${col.id}`}
|
||||
style={{
|
||||
width: col.getSize(),
|
||||
position: index < 2 ? 'sticky' : 'relative',
|
||||
left: index < 2 ? `${index * col.getSize()}px` : 'auto',
|
||||
background: token.colorBgContainer,
|
||||
color: token.colorText,
|
||||
height: '42px',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
paddingLeft: index === 3 ? '32px' : '8px',
|
||||
paddingRight: '8px',
|
||||
}}
|
||||
>
|
||||
{flexRender(col.cell, {
|
||||
getValue: () => subTask[col.id as keyof typeof subTask] ?? null,
|
||||
row: { original: subTask } as Row<IProjectTask>,
|
||||
column: col as Column<IProjectTask>,
|
||||
table,
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{paddingBottom > 0 && <div style={{ height: `${paddingBottom}px` }} />}
|
||||
{rows.map(row => (
|
||||
<React.Fragment key={row.id}>
|
||||
<div
|
||||
className="table-row"
|
||||
style={{
|
||||
'&:hover div': {
|
||||
background: `${token.colorFillAlter} !important`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell, index) => (
|
||||
<div
|
||||
key={cell.id}
|
||||
className={`${cell.column.getIsPinned() === 'left' ? 'sticky left-0 z-10' : ''}`}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
position: index < 2 ? 'sticky' : 'relative',
|
||||
left: 'auto',
|
||||
background: token.colorBgContainer,
|
||||
color: token.colorText,
|
||||
height: '42px',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: '8px 0px 8px 8px',
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{expandedRows[row.id] &&
|
||||
row.original.sub_tasks?.map(subTask => (
|
||||
<div
|
||||
key={subTask.task_key}
|
||||
className="table-row"
|
||||
style={{
|
||||
'&:hover div': {
|
||||
background: `${token.colorFillAlter} !important`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{columns.map((col, index) => (
|
||||
<div
|
||||
key={`${subTask.task_key}-${col.id}`}
|
||||
style={{
|
||||
width: col.getSize(),
|
||||
position: index < 2 ? 'sticky' : 'relative',
|
||||
left: index < 2 ? `${index * col.getSize()}px` : 'auto',
|
||||
background: token.colorBgContainer,
|
||||
color: token.colorText,
|
||||
height: '42px',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
paddingLeft: index === 3 ? '32px' : '8px',
|
||||
paddingRight: '8px',
|
||||
}}
|
||||
>
|
||||
{flexRender(col.cell, {
|
||||
getValue: () => subTask[col.id as keyof typeof subTask] ?? null,
|
||||
row: { original: subTask } as Row<IProjectTask>,
|
||||
column: col as Column<IProjectTask>,
|
||||
table,
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { TaskType } from '@/types/task.types';
|
||||
import { EditOutlined, EllipsisOutlined, RetweetOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { colors } from '@/styles/colors';
|
||||
import './task-list-table-wrapper.css';
|
||||
import TaskListTable from '../task-list-table-old/task-list-table-old';
|
||||
import TaskListTable from '../table-v2';
|
||||
import { MenuProps } from 'antd/lib';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import TaskListCustom from '../task-list-custom';
|
||||
import { columnList as defaultColumnList } from '@/pages/projects/project-view-1/taskList/taskListTable/columns/columnList';
|
||||
|
||||
type TaskListTableWrapperProps = {
|
||||
taskList: ITaskListGroup;
|
||||
@@ -37,6 +37,22 @@ const TaskListTableWrapper = ({
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-table');
|
||||
|
||||
// Get column visibility from Redux
|
||||
const columnVisibilityList = useAppSelector(
|
||||
state => state.projectViewTaskListColumnsReducer.columnList
|
||||
);
|
||||
|
||||
// Filter visible columns and format them for table-v2
|
||||
const visibleColumns = defaultColumnList
|
||||
.filter(column => {
|
||||
const visibilityConfig = columnVisibilityList.find(col => col.key === column.key);
|
||||
return visibilityConfig?.isVisible ?? false;
|
||||
})
|
||||
.map(column => ({
|
||||
key: column.key,
|
||||
width: column.width,
|
||||
}));
|
||||
|
||||
// function to handle toggle expand
|
||||
const handlToggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
@@ -98,6 +114,14 @@ const TaskListTableWrapper = ({
|
||||
},
|
||||
];
|
||||
|
||||
const handleTaskSelect = (taskId: string) => {
|
||||
console.log('Task selected:', taskId);
|
||||
};
|
||||
|
||||
const handleTaskExpand = (taskId: string) => {
|
||||
console.log('Task expanded:', taskId);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
wave={{ disabled: true }}
|
||||
@@ -172,11 +196,13 @@ const TaskListTableWrapper = ({
|
||||
key: groupId || '1',
|
||||
className: `custom-collapse-content-box relative after:content after:absolute after:h-full after:w-1 ${color} after:z-10 after:top-0 after:left-0`,
|
||||
children: (
|
||||
<TaskListCustom
|
||||
<TaskListTable
|
||||
key={groupId}
|
||||
groupId={groupId}
|
||||
tasks={taskList.tasks}
|
||||
color={color || ''}
|
||||
taskListGroup={taskList}
|
||||
tableId={groupId || ''}
|
||||
visibleColumns={visibleColumns}
|
||||
onTaskSelect={handleTaskSelect}
|
||||
onTaskExpand={handleTaskExpand}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -6,9 +6,7 @@ import { ITaskListConfigV2, ITaskListGroup } from '@/types/tasks/taskList.types'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { fetchTaskGroups } from '@/features/tasks/taskSlice';
|
||||
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||
|
||||
import { columnList } from '@/pages/projects/project-view-1/taskList/taskListTable/columns/columnList';
|
||||
import StatusGroupTables from '../taskList/statusTables/StatusGroupTables';
|
||||
import TaskListTableWrapper from './task-list-table-wrapper/task-list-table-wrapper';
|
||||
|
||||
const TaskList = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -31,6 +29,7 @@ const TaskList = () => {
|
||||
const onTaskExpand = (taskId: string) => {
|
||||
console.log('taskId:', taskId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
const config: ITaskListConfigV2 = {
|
||||
@@ -54,9 +53,15 @@ const TaskList = () => {
|
||||
<Flex vertical gap={16}>
|
||||
<TaskListFilters position="list" />
|
||||
<Skeleton active loading={loadingGroups}>
|
||||
{/* {taskGroups.map((group: ITaskListGroup) => (
|
||||
|
||||
))} */}
|
||||
{taskGroups.map((group: ITaskListGroup) => (
|
||||
<TaskListTableWrapper
|
||||
key={group.id}
|
||||
taskList={group}
|
||||
groupId={group.id}
|
||||
name={group.name}
|
||||
color={group.color_code}
|
||||
/>
|
||||
))}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user