feat(TaskCard): add subtask expansion functionality and improve UI interactions
- Implemented subtask expansion logic with dispatch actions for toggling visibility and fetching subtasks. - Enhanced UI to include a button for showing/hiding subtasks, improving user interaction. - Updated task display to show subtask counts and loading states, ensuring better feedback during data fetching.
This commit is contained in:
@@ -13,6 +13,8 @@ import { createPortal } from 'react-dom';
|
|||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import { getUserSession } from '@/utils/session-helper';
|
import { getUserSession } from '@/utils/session-helper';
|
||||||
|
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||||
|
import { toggleTaskExpansion, fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
|
||||||
// Simple Portal component
|
// Simple Portal component
|
||||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
@@ -67,6 +69,7 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
const d = selectedDate || new Date();
|
const d = selectedDate || new Date();
|
||||||
return new Date(d.getFullYear(), d.getMonth(), 1);
|
return new Date(d.getFullYear(), d.getMonth(), 1);
|
||||||
});
|
});
|
||||||
|
const [showSubtasks, setShowSubtasks] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedDate(task.end_date ? new Date(task.end_date) : null);
|
setSelectedDate(task.end_date ? new Date(task.end_date) : null);
|
||||||
@@ -157,6 +160,24 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
handleDateChange(nextWeek);
|
handleDateChange(nextWeek);
|
||||||
}, [handleDateChange]);
|
}, [handleDateChange]);
|
||||||
|
|
||||||
|
const handleSubTaskExpand = useCallback(() => {
|
||||||
|
if (task && task.id && projectId) {
|
||||||
|
if (task.sub_tasks && task.sub_tasks.length > 0 && task.sub_tasks_count && task.sub_tasks_count > 0) {
|
||||||
|
dispatch(toggleTaskExpansion(task.id));
|
||||||
|
} else if (task.sub_tasks_count && task.sub_tasks_count > 0) {
|
||||||
|
dispatch(toggleTaskExpansion(task.id));
|
||||||
|
dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
|
||||||
|
} else {
|
||||||
|
dispatch(toggleTaskExpansion(task.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [task, projectId, dispatch]);
|
||||||
|
|
||||||
|
const handleSubtaskButtonClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSubTaskExpand();
|
||||||
|
}, [handleSubTaskExpand]);
|
||||||
|
|
||||||
// Calendar rendering helpers
|
// Calendar rendering helpers
|
||||||
const year = calendarMonth.getFullYear();
|
const year = calendarMonth.getFullYear();
|
||||||
const month = calendarMonth.getMonth();
|
const month = calendarMonth.getMonth();
|
||||||
@@ -193,13 +214,13 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
onDrop={e => onTaskDrop(e, groupId, idx)}
|
onDrop={e => onTaskDrop(e, groupId, idx)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<div className="enhanced-kanban-task-card" style={{ background, color, display: 'block'}} >
|
||||||
<div
|
<div
|
||||||
className="enhanced-kanban-task-card"
|
|
||||||
draggable
|
draggable
|
||||||
onDragStart={e => onTaskDragStart(e, task.id!, groupId)}
|
onDragStart={e => onTaskDragStart(e, task.id!, groupId)}
|
||||||
onDragOver={e => onTaskDragOver(e, groupId, idx)}
|
onDragOver={e => onTaskDragOver(e, groupId, idx)}
|
||||||
onDrop={e => onTaskDrop(e, groupId, idx)}
|
onDrop={e => onTaskDrop(e, groupId, idx)}
|
||||||
style={{ background, color }}
|
|
||||||
onClick={e => handleCardClick(e, task.id!)}
|
onClick={e => handleCardClick(e, task.id!)}
|
||||||
>
|
>
|
||||||
<div className="task-content">
|
<div className="task-content">
|
||||||
@@ -361,10 +382,84 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
<LazyAssigneeSelectorWrapper task={task} groupId={groupId} isDarkMode={themeMode === 'dark'} />
|
<LazyAssigneeSelectorWrapper task={task} groupId={groupId} isDarkMode={themeMode === 'dark'} />
|
||||||
|
{(task.sub_tasks_count ?? 0) > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
"ml-2 px-2 py-0.5 rounded-full flex items-center gap-1 text-xs font-medium transition-colors " +
|
||||||
|
(task.show_sub_tasks
|
||||||
|
? "bg-gray-100 dark:bg-gray-800"
|
||||||
|
: "bg-white dark:bg-[#1e1e1e] hover:bg-gray-50 dark:hover:bg-gray-700")
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
}}
|
||||||
|
onClick={handleSubtaskButtonClick}
|
||||||
|
title={task.show_sub_tasks ? t('hideSubtasks') || 'Hide Subtasks' : t('showSubtasks') || 'Show Subtasks'}
|
||||||
|
>
|
||||||
|
{/* Fork/branch icon */}
|
||||||
|
<svg style={{ color: '#888' }} className="w-2 h-2" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 20 20">
|
||||||
|
<path d="M6 3v2a2 2 0 002 2h4a2 2 0 012 2v2" strokeLinecap="round" />
|
||||||
|
<circle cx="6" cy="3" r="2" fill="currentColor" />
|
||||||
|
<circle cx="16" cy="9" r="2" fill="currentColor" />
|
||||||
|
<circle cx="6" cy="17" r="2" fill="currentColor" />
|
||||||
|
<path d="M6 5v10" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#888',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
display: 'inline-block',
|
||||||
|
}}>{task.sub_tasks_count ?? 0}</span>
|
||||||
|
{/* Caret icon */}
|
||||||
|
{task.show_sub_tasks ? (
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 20 20">
|
||||||
|
<path d="M6 8l4 4 4-4" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 20 20">
|
||||||
|
<path d="M8 6l4 4-4 4" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{task.show_sub_tasks && (
|
||||||
|
<div className="mt-2 border-t border-gray-100 dark:border-gray-700 pt-2">
|
||||||
|
{/* Loading state */}
|
||||||
|
{task.sub_tasks_loading && (
|
||||||
|
<div className="py-2 text-xs text-gray-400 dark:text-gray-500">Loading...</div>
|
||||||
|
)}
|
||||||
|
{/* Loaded subtasks */}
|
||||||
|
{!task.sub_tasks_loading && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0 && (
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{task.sub_tasks.map(sub => (
|
||||||
|
<li key={sub.id} className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||||
|
<span className="flex-1 truncate text-xs text-gray-800 dark:text-gray-100">{sub.name}</span>
|
||||||
|
{sub.names && sub.names.length > 0 && (
|
||||||
|
<AvatarGroup
|
||||||
|
members={sub.names}
|
||||||
|
maxCount={2}
|
||||||
|
isDarkMode={themeMode === 'dark'}
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{/* Empty state */}
|
||||||
|
{!task.sub_tasks_loading && (!Array.isArray(task.sub_tasks) || task.sub_tasks.length === 0) && (
|
||||||
|
<div className="py-2 text-xs text-gray-400 dark:text-gray-500">{t('noSubtasks', 'No subtasks')}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user