Merge pull request #245 from shancds/test/row-kanban-board-v1.1.4

Test/row kanban board v1.1.4
This commit is contained in:
Chamika J
2025-07-08 17:00:19 +05:30
committed by GitHub
5 changed files with 234 additions and 243 deletions

View File

@@ -5,29 +5,25 @@ import { PlusOutlined, UserAddOutlined } from '@ant-design/icons';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types'; import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
import { useSocket } from '@/socket/socketContext'; import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events'; import { SocketEvents } from '@/shared/socket-events';
import { useAuthService } from '@/hooks/useAuth'; import { useAuthService } from '@/hooks/useAuth';
import { Avatar, Checkbox } from '@/components'; import { Avatar, Button, Checkbox } from '@/components';
import { sortTeamMembers } from '@/utils/sort-team-members'; import { sortTeamMembers } from '@/utils/sort-team-members';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice'; import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
import { updateTaskAssignees } from '@/features/task-management/task-management.slice'; import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
interface AssigneeSelectorProps { interface AssigneeSelectorProps {
task: IProjectTask; task: IProjectTask;
groupId?: string | null; groupId?: string | null;
isDarkMode?: boolean; isDarkMode?: boolean;
kanbanMode?: boolean; // <-- Add this prop
} }
const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
task, task,
groupId = null, groupId = null,
isDarkMode = false, isDarkMode = false
kanbanMode = false, // <-- Default to false
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -35,12 +31,6 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
const [optimisticAssignees, setOptimisticAssignees] = useState<string[]>([]); // For optimistic updates const [optimisticAssignees, setOptimisticAssignees] = useState<string[]>([]); // For optimistic updates
const [pendingChanges, setPendingChanges] = useState<Set<string>>(new Set()); // Track pending member changes const [pendingChanges, setPendingChanges] = useState<Set<string>>(new Set()); // Track pending member changes
// Initialize optimistic assignees from task data on mount or when task changes
useEffect(() => {
const currentAssigneeIds = task?.assignees?.map(a => a.team_member_id) || [];
setOptimisticAssignees(currentAssigneeIds);
}, [task?.assignees]);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
@@ -61,16 +51,9 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
const updateDropdownPosition = useCallback(() => { const updateDropdownPosition = useCallback(() => {
if (buttonRef.current) { if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect(); const rect = buttonRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const dropdownHeight = 280; // More accurate height: header(40) + max-height(192) + footer(40) + padding
// Check if dropdown would go below viewport
const spaceBelow = viewportHeight - rect.bottom;
const shouldShowAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
setDropdownPosition({ setDropdownPosition({
top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4, top: rect.bottom + window.scrollY + 2,
left: rect.left, left: rect.left + window.scrollX,
}); });
} }
}, []); }, []);
@@ -78,23 +61,29 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
// Close dropdown when clicking outside and handle scroll // Close dropdown when clicking outside and handle scroll
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if ( if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) &&
dropdownRef.current && buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
!dropdownRef.current.contains(event.target as Node) &&
buttonRef.current &&
!buttonRef.current.contains(event.target as Node)
) {
setIsOpen(false); setIsOpen(false);
} }
}; };
const handleScroll = (event: Event) => { const handleScroll = () => {
if (isOpen) { if (isOpen) {
// Only close dropdown if scrolling happens outside the dropdown // Check if the button is still visible in the viewport
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
const isVisible = rect.top >= 0 && rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth;
if (isVisible) {
updateDropdownPosition();
} else {
// Hide dropdown if button is not visible
setIsOpen(false); setIsOpen(false);
} }
} }
}
}; };
const handleResize = () => { const handleResize = () => {
@@ -126,14 +115,11 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
if (!isOpen) { if (!isOpen) {
updateDropdownPosition(); updateDropdownPosition();
// Prepare team members data when opening - use optimistic assignees for current state // Prepare team members data when opening
const currentAssigneeIds = optimisticAssignees.length > 0 const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
? optimisticAssignees const membersData = (members?.data || []).map(member => ({
: task?.assignees?.map(assignee => assignee.team_member_id) || [];
const membersData: (ITeamMembersViewModel & { selected?: boolean })[] = (members?.data || []).map(member => ({
...member, ...member,
selected: currentAssigneeIds.includes(member.id), selected: assignees?.includes(member.id),
})); }));
const sortedMembers = sortTeamMembers(membersData); const sortedMembers = sortTeamMembers(membersData);
setTeamMembers({ data: sortedMembers }); setTeamMembers({ data: sortedMembers });
@@ -154,20 +140,16 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
// Add to pending changes for visual feedback // Add to pending changes for visual feedback
setPendingChanges(prev => new Set(prev).add(memberId)); setPendingChanges(prev => new Set(prev).add(memberId));
// Get the current list of assignees, prioritizing optimistic updates for immediate feedback // OPTIMISTIC UPDATE: Update local state immediately for instant UI feedback
const currentAssigneeIds = optimisticAssignees.length > 0 const currentAssignees = task?.assignees?.map(a => a.team_member_id) || [];
? optimisticAssignees
: task?.assignees?.map(a => a.team_member_id) || [];
let newAssigneeIds: string[]; let newAssigneeIds: string[];
if (checked) { if (checked) {
// Adding assignee: ensure no duplicates // Adding assignee
const uniqueIds = new Set([...currentAssigneeIds, memberId]); newAssigneeIds = [...currentAssignees, memberId];
newAssigneeIds = Array.from(uniqueIds);
} else { } else {
// Removing assignee // Removing assignee
newAssigneeIds = currentAssigneeIds.filter(id => id !== memberId); newAssigneeIds = currentAssignees.filter(id => id !== memberId);
} }
// Update optimistic state for immediate UI feedback in dropdown // Update optimistic state for immediate UI feedback in dropdown
@@ -177,8 +159,10 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
setTeamMembers(prev => ({ setTeamMembers(prev => ({
...prev, ...prev,
data: (prev.data || []).map(member => data: (prev.data || []).map(member =>
member.id === memberId ? { ...member, selected: checked } : member member.id === memberId
), ? { ...member, selected: checked }
: member
)
})); }));
const body = { const body = {
@@ -192,35 +176,17 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
// Emit socket event - the socket handler will update Redux with proper types // Emit socket event - the socket handler will update Redux with proper types
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body)); socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
socket?.once(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), (data: any) => { socket?.once(
// Instead of updating enhancedKanbanSlice, update the main taskManagementSlice SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
// Filter members to get the actual InlineMember objects for the new assignees (data: any) => {
const updatedAssigneeNames: InlineMember[] = (members?.data || [])
.filter((member): member is ITeamMemberViewModel & { id: string; name: string } => {
return typeof member.id === 'string' && typeof member.name === 'string' && newAssigneeIds.includes(member.id);
})
.map(member => ({
name: member.name || '',
id: member.id || '',
team_member_id: member.id || '',
avatar_url: member.avatar_url || '',
color_code: member.color_code || '',
}));
dispatch(updateTaskAssignees({
taskId: task.id || '',
assigneeIds: newAssigneeIds,
assigneeNames: updatedAssigneeNames,
}));
if (kanbanMode) {
dispatch(updateEnhancedKanbanTaskAssignees(data)); dispatch(updateEnhancedKanbanTaskAssignees(data));
} }
}); );
// Remove from pending changes after a short delay (optimistic) // Remove from pending changes after a short delay (optimistic)
setTimeout(() => { setTimeout(() => {
setPendingChanges(prev => { setPendingChanges(prev => {
const newSet = new Set<string>(Array.from(prev)); const newSet = new Set(prev);
newSet.delete(memberId); newSet.delete(memberId);
return newSet; return newSet;
}); });
@@ -229,8 +195,11 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
const checkMemberSelected = (memberId: string) => { const checkMemberSelected = (memberId: string) => {
if (!memberId) return false; if (!memberId) return false;
// Always use optimistic assignees for dropdown display // Use optimistic assignees if available, otherwise fall back to task assignees
return optimisticAssignees.includes(memberId); const assignees = optimisticAssignees.length > 0
? optimisticAssignees
: task?.assignees?.map(assignee => assignee.team_member_id) || [];
return assignees.includes(memberId);
}; };
const handleInviteProjectMemberDrawer = () => { const handleInviteProjectMemberDrawer = () => {
@@ -246,8 +215,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
className={` className={`
w-5 h-5 rounded-full border border-dashed flex items-center justify-center w-5 h-5 rounded-full border border-dashed flex items-center justify-center
transition-colors duration-200 transition-colors duration-200
${ ${isOpen
isOpen
? isDarkMode ? isDarkMode
? 'border-blue-500 bg-blue-900/20 text-blue-400' ? 'border-blue-500 bg-blue-900/20 text-blue-400'
: 'border-blue-500 bg-blue-50 text-blue-600' : 'border-blue-500 bg-blue-50 text-blue-600'
@@ -260,14 +228,16 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
<PlusOutlined className="text-xs" /> <PlusOutlined className="text-xs" />
</button> </button>
{isOpen && {isOpen && createPortal(
createPortal(
<div <div
ref={dropdownRef} ref={dropdownRef}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
className={` className={`
fixed z-9999 w-72 rounded-md shadow-lg border fixed z-9999 w-72 rounded-md shadow-lg border
${isDarkMode ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-200'} ${isDarkMode
? 'bg-gray-800 border-gray-600'
: 'bg-white border-gray-200'
}
`} `}
style={{ style={{
top: dropdownPosition.top, top: dropdownPosition.top,
@@ -280,12 +250,11 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
ref={searchInputRef} ref={searchInputRef}
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={e => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search members..." placeholder="Search members..."
className={` className={`
w-full px-2 py-1 text-xs rounded border w-full px-2 py-1 text-xs rounded border
${ ${isDarkMode
isDarkMode
? 'bg-gray-700 border-gray-600 text-gray-100 placeholder-gray-400 focus:border-blue-500' ? 'bg-gray-700 border-gray-600 text-gray-100 placeholder-gray-400 focus:border-blue-500'
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-500 focus:border-blue-500' : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500 focus:border-blue-500'
} }
@@ -297,13 +266,12 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
{/* Members List */} {/* Members List */}
<div className="max-h-48 overflow-y-auto"> <div className="max-h-48 overflow-y-auto">
{filteredMembers && filteredMembers.length > 0 ? ( {filteredMembers && filteredMembers.length > 0 ? (
filteredMembers.map(member => ( filteredMembers.map((member) => (
<div <div
key={member.id} key={member.id}
className={` className={`
flex items-center gap-2 p-2 cursor-pointer transition-colors flex items-center gap-2 p-2 cursor-pointer transition-colors
${ ${member.pending_invitation
member.pending_invitation
? 'opacity-50 cursor-not-allowed' ? 'opacity-50 cursor-not-allowed'
: isDarkMode : isDarkMode
? 'hover:bg-gray-700' ? 'hover:bg-gray-700'
@@ -325,24 +293,18 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
<span onClick={e => e.stopPropagation()}> <span onClick={e => e.stopPropagation()}>
<Checkbox <Checkbox
checked={checkMemberSelected(member.id || '')} checked={checkMemberSelected(member.id || '')}
onChange={checked => handleMemberToggle(member.id || '', checked)} onChange={(checked) => handleMemberToggle(member.id || '', checked)}
disabled={ disabled={member.pending_invitation || pendingChanges.has(member.id || '')}
member.pending_invitation || pendingChanges.has(member.id || '')
}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
/> />
</span> </span>
{pendingChanges.has(member.id || '') && ( {pendingChanges.has(member.id || '') && (
<div <div className={`absolute inset-0 flex items-center justify-center ${
className={`absolute inset-0 flex items-center justify-center ${
isDarkMode ? 'bg-gray-800/50' : 'bg-white/50' isDarkMode ? 'bg-gray-800/50' : 'bg-white/50'
}`} }`}>
> <div className={`w-3 h-3 border border-t-transparent rounded-full animate-spin ${
<div
className={`w-3 h-3 border border-t-transparent rounded-full animate-spin ${
isDarkMode ? 'border-blue-400' : 'border-blue-600' isDarkMode ? 'border-blue-400' : 'border-blue-600'
}`} }`} />
/>
</div> </div>
)} )}
</div> </div>
@@ -355,14 +317,10 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div <div className={`text-xs font-medium truncate ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}>
className={`text-xs font-medium truncate ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}
>
{member.name} {member.name}
</div> </div>
<div <div className={`text-xs truncate ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
className={`text-xs truncate ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
>
{member.email} {member.email}
{member.pending_invitation && ( {member.pending_invitation && (
<span className="text-red-400 ml-1">(Pending)</span> <span className="text-red-400 ml-1">(Pending)</span>
@@ -372,9 +330,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
</div> </div>
)) ))
) : ( ) : (
<div <div className={`p-4 text-center ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
className={`p-4 text-center ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
>
<div className="text-xs">No members found</div> <div className="text-xs">No members found</div>
</div> </div>
)} )}
@@ -386,7 +342,10 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
className={` className={`
w-full flex items-center justify-center gap-1 px-2 py-1 text-xs rounded w-full flex items-center justify-center gap-1 px-2 py-1 text-xs rounded
transition-colors transition-colors
${isDarkMode ? 'text-blue-400 hover:bg-gray-700' : 'text-blue-600 hover:bg-blue-50'} ${isDarkMode
? 'text-blue-400 hover:bg-gray-700'
: 'text-blue-600 hover:bg-blue-50'
}
`} `}
onClick={handleInviteProjectMemberDrawer} onClick={handleInviteProjectMemberDrawer}
> >

View File

@@ -263,6 +263,11 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
setDragType(null); setDragType(null);
}; };
const handleDragEnd = () => {
setHoveredGroupId(null);
setHoveredTaskIdx(null);
};
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) return;
@@ -332,6 +337,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
onTaskDragStart={handleTaskDragStart} onTaskDragStart={handleTaskDragStart}
onTaskDragOver={handleTaskDragOver} onTaskDragOver={handleTaskDragOver}
onTaskDrop={handleTaskDrop} onTaskDrop={handleTaskDrop}
onDragEnd={handleDragEnd}
hoveredTaskIdx={hoveredGroupId === group.id ? hoveredTaskIdx : null} hoveredTaskIdx={hoveredGroupId === group.id ? hoveredTaskIdx : null}
hoveredGroupId={hoveredGroupId} hoveredGroupId={hoveredGroupId}
/> />

View File

@@ -34,6 +34,7 @@ interface KanbanGroupProps {
onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void; onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void;
onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number | null) => void; onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number | null) => void;
onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number | null) => void; onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number | null) => void;
onDragEnd: (e: React.DragEvent) => void;
hoveredTaskIdx: number | null; hoveredTaskIdx: number | null;
hoveredGroupId: string | null; hoveredGroupId: string | null;
} }
@@ -46,6 +47,7 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
onTaskDragStart, onTaskDragStart,
onTaskDragOver, onTaskDragOver,
onTaskDrop, onTaskDrop,
onDragEnd,
hoveredTaskIdx, hoveredTaskIdx,
hoveredGroupId hoveredGroupId
}) => { }) => {
@@ -197,7 +199,10 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
setIsEditable(true); setIsEditable(true);
setShowDropdown(false); setShowDropdown(false);
setTimeout(() => { setTimeout(() => {
inputRef.current?.focus(); if (inputRef.current) {
inputRef.current.focus();
inputRef.current.select(); // Select all text on focus
}
}, 100); }, 100);
}; };
@@ -259,6 +264,7 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
onDragStart={e => onGroupDragStart(e, group.id)} onDragStart={e => onGroupDragStart(e, group.id)}
onDragOver={onGroupDragOver} onDragOver={onGroupDragOver}
onDrop={e => onGroupDrop(e, group.id)} onDrop={e => onGroupDrop(e, group.id)}
onDragEnd={onDragEnd}
> >
<div <div
className="flex items-center justify-between w-full font-semibold rounded-md" className="flex items-center justify-between w-full font-semibold rounded-md"
@@ -517,17 +523,46 @@ const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
)} )}
{group.tasks.map((task, idx) => ( {group.tasks.map((task, idx) => (
<React.Fragment key={task.id}>
{/* Drop indicator before this card */}
{hoveredGroupId === group.id && hoveredTaskIdx === idx && (
<div
onDragOver={e => onTaskDragOver(e, group.id, idx)}
onDrop={e => onTaskDrop(e, group.id, idx)}
>
<div className="w-full h-full bg-red-500" style={{
height: 80,
background: themeMode === 'dark' ? '#2a2a2a' : '#E2EAF4',
borderRadius: 6,
border: `5px`
}}></div>
</div>
)}
<TaskCard <TaskCard
key={task.id}
task={task} task={task}
onTaskDragStart={onTaskDragStart} onTaskDragStart={onTaskDragStart}
onTaskDragOver={onTaskDragOver} onTaskDragOver={onTaskDragOver}
onTaskDrop={onTaskDrop} onTaskDrop={onTaskDrop}
groupId={group.id} groupId={group.id}
isDropIndicator={hoveredGroupId === group.id && hoveredTaskIdx === idx}
idx={idx} idx={idx}
onDragEnd={onDragEnd}
/> />
</React.Fragment>
))} ))}
{/* Drop indicator at the end of the group */}
{hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && (
<div
onDragOver={e => onTaskDragOver(e, group.id, group.tasks.length)}
onDrop={e => onTaskDrop(e, group.id, group.tasks.length)}
>
<div className="w-full h-full bg-red-500" style={{
height: 80,
background: themeMode === 'dark' ? '#2a2a2a' : '#E2EAF4',
borderRadius: 6,
border: `5px`
}}></div>
</div>
)}
{/* Create card at bottom */} {/* Create card at bottom */}
{showNewCardBottom && ( {showNewCardBottom && (

View File

@@ -28,8 +28,8 @@ interface TaskCardProps {
onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number) => void; onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number) => void;
onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void; onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void;
groupId: string; groupId: string;
isDropIndicator: boolean;
idx: number; idx: number;
onDragEnd: (e: React.DragEvent) => void; // <-- add this
} }
function getDaysInMonth(year: number, month: number) { function getDaysInMonth(year: number, month: number) {
@@ -46,8 +46,8 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
onTaskDragOver, onTaskDragOver,
onTaskDrop, onTaskDrop,
groupId, groupId,
isDropIndicator, idx,
idx onDragEnd // <-- add this
}) => { }) => {
const { socket } = useSocket(); const { socket } = useSocket();
const themeMode = useSelector((state: RootState) => state.themeReducer.mode); const themeMode = useSelector((state: RootState) => state.themeReducer.mode);
@@ -198,31 +198,24 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
while (week.length < 7) week.push(null); while (week.length < 7) week.push(null);
weeks.push(week); weeks.push(week);
} }
const [isDown, setIsDown] = useState(false);
return ( return (
<> <>
{isDropIndicator && (
<div
onDragStart={e => onTaskDragStart(e, task.id!, groupId)}
onDragOver={e => onTaskDragOver(e, groupId, idx)}
onDrop={e => onTaskDrop(e, groupId, idx)}
>
<div className="w-full h-full bg-red-500"style={{
height: 80,
background: themeMode === 'dark' ? '#2a2a2a' : '#f0f0f0',
borderRadius: 6,
border: `5px`
}}></div>
</div>
)}
<div className="enhanced-kanban-task-card" style={{ background, color, display: 'block' }} > <div className="enhanced-kanban-task-card" style={{ background, color, display: 'block' }} >
<div <div
draggable draggable
onDragStart={e => onTaskDragStart(e, task.id!, groupId)} onDragStart={e => onTaskDragStart(e, task.id!, groupId)}
onDragOver={e => onTaskDragOver(e, groupId, idx)} onDragOver={e => {
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
const offsetY = e.clientY - rect.top;
const isDown = offsetY > rect.height / 2;
setIsDown(isDown);
onTaskDragOver(e, groupId, isDown ? idx + 1 : idx);
}}
onDrop={e => onTaskDrop(e, groupId, idx)} onDrop={e => onTaskDrop(e, groupId, idx)}
onDragEnd={onDragEnd} // <-- add this
onClick={e => handleCardClick(e, task.id!)} onClick={e => handleCardClick(e, task.id!)}
> >
<div className="task-content"> <div className="task-content">

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import EnhancedKanbanBoard from '@/components/enhanced-kanban/EnhancedKanbanBoard';
import EnhancedKanbanBoardNativeDnD from '@/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD'; import EnhancedKanbanBoardNativeDnD from '@/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD';
const ProjectViewEnhancedBoard: React.FC = () => { const ProjectViewEnhancedBoard: React.FC = () => {
@@ -12,7 +11,6 @@ const ProjectViewEnhancedBoard: React.FC = () => {
return ( return (
<div className="project-view-enhanced-board"> <div className="project-view-enhanced-board">
{/* <EnhancedKanbanBoard projectId={project.id} /> */}
<EnhancedKanbanBoardNativeDnD projectId={project.id} /> <EnhancedKanbanBoardNativeDnD projectId={project.id} />
</div> </div>
); );