diff --git a/frontend/src/pages/WorkspaceBoard.tsx b/frontend/src/components/board/BoardContent.tsx similarity index 94% rename from frontend/src/pages/WorkspaceBoard.tsx rename to frontend/src/components/board/BoardContent.tsx index bbe3071..157696f 100644 --- a/frontend/src/pages/WorkspaceBoard.tsx +++ b/frontend/src/components/board/BoardContent.tsx @@ -1,7 +1,6 @@ import BoardClosedBanner from '@/components/board/BoardClosedBanner'; import BoardNavbar from '@/components/board/BoardNavbar'; import DroppableColumn from '@/components/board/DroppableColumn'; -import TaskDetailModal from '@/components/board/TaskDetailModal'; import LoadingContent from '@/components/ui/LoadingContent'; import { useBoardData } from '@/hooks/useBoardData'; import { useBoardOperations } from '@/hooks/useBoardOperations'; @@ -11,7 +10,7 @@ import { useAppSelector } from '@/store'; import { selectActiveColumns } from '@/store/selectors/columnsSelector'; import { selectTaskById, selectTasksByColumns } from '@/store/selectors/tasksSelectors'; import { columnsReordered, setColumns } from '@/store/slices/columnsSlice'; -import type { Column, Task } from '@/types'; +import type { Column } from '@/types'; import { DndContext, DragOverlay, @@ -33,7 +32,7 @@ import { sortableKeyboardCoordinates, } from '@dnd-kit/sortable'; import { GripVertical, Plus } from 'lucide-react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; @@ -41,8 +40,6 @@ const WorkspaceBoard = () => { const { boardId } = useParams(); const [activeId, setActiveId] = useState(null); - const [showDetailModal, setShowDetailModal] = useState(false); - const [activeItem, setActiveItem] = useState(null); const containerRef = useRef(null); const dispatch = useDispatch(); const columns = useAppSelector(selectActiveColumns); @@ -408,15 +405,7 @@ const WorkspaceBoard = () => { setCardTitle(''); }, [isBoardClosed]); - const handleHideDetailModal = useCallback(() => { - setShowDetailModal(false); - }, []); - - const handleShowDetailModal = useCallback(() => { - setShowDetailModal(true); - }, []); - - console.log("worspace board") + console.log("worspace board: " + boardId); return (
{ @@ -457,8 +446,6 @@ const WorkspaceBoard = () => { onUpdateColumnTitle={updateColumnTitle} onArchiveColumn={archiveColumn} onArchiveAllItems={archiveAllTasksInColumn} - handleShowDetailTask={handleShowDetailModal} - setActiveItem={setActiveItem} /> ))} @@ -501,20 +488,10 @@ const WorkspaceBoard = () => {
- { - showDetailModal && - - } } ); }; -export default WorkspaceBoard; \ No newline at end of file +export default memo(WorkspaceBoard); \ No newline at end of file diff --git a/frontend/src/components/board/DraggableItem.tsx b/frontend/src/components/board/DraggableItem.tsx index 4060ff8..27cafe6 100644 --- a/frontend/src/components/board/DraggableItem.tsx +++ b/frontend/src/components/board/DraggableItem.tsx @@ -1,26 +1,22 @@ -import type { Item, Task } from '@/types'; import { detectUrl } from '@/utils/UrlPreviewUtils'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { X } from 'lucide-react'; import UrlPreview from './UrlPreview'; -import { useAppSelector, type RootState } from '@/store'; +import { useAppDispatch, useAppSelector, type RootState } from '@/store'; import { useSelector } from 'react-redux'; import { selectTaskById } from '@/store/selectors/tasksSelectors'; +import { showTaskModal } from '@/store/slices/taskModalSlice'; interface DraggableItemProps { onDelete: (itemId: number) => void; - handleShowDetailTask: () => void; label?: string; // e.g., "FE", "BE" - setActiveItem: (item: Task) => void; itemId: number; } const DraggableItem: React.FC = ({ onDelete, - handleShowDetailTask, label, - setActiveItem, itemId }) => { const item = useAppSelector((state) => selectTaskById(itemId)(state)); @@ -45,6 +41,7 @@ const DraggableItem: React.FC = ({ }; const members = useSelector((state: RootState) => state.members); + const dispatch = useAppDispatch(); // console.log("Rendering DraggableItem:", item.id, item.name); return ( @@ -54,8 +51,7 @@ const DraggableItem: React.FC = ({ {...attributes} {...listeners} onClick={() => { - detectUrl(item.name) ? undefined : handleShowDetailTask(); - setActiveItem(item); + detectUrl(item.name) ? undefined : dispatch(showTaskModal(item.id)); }} className={` select-none bg-[#222f44] p-2 rounded-lg diff --git a/frontend/src/components/board/DroppableColumn.tsx b/frontend/src/components/board/DroppableColumn.tsx index 4fd2e30..bcdb3d7 100644 --- a/frontend/src/components/board/DroppableColumn.tsx +++ b/frontend/src/components/board/DroppableColumn.tsx @@ -32,8 +32,6 @@ interface DroppableColumnProps { onUpdateColumnTitle: (columnId: number, newTitle: string) => void; onArchiveColumn: (column: Column) => void; onArchiveAllItems: (columnId: number) => void; - handleShowDetailTask: () => void; - setActiveItem: (item: Task) => void; } const DroppableColumnComponent: React.FC = ({ @@ -49,8 +47,6 @@ const DroppableColumnComponent: React.FC = ({ onUpdateColumnTitle, onArchiveColumn, onArchiveAllItems, - handleShowDetailTask, - setActiveItem, }) => { const [isEditingTitle, setIsEditingTitle] = useState(false); const [showOptionsMenu, setShowOptionsMenu] = useState(false); @@ -296,8 +292,6 @@ const DroppableColumnComponent: React.FC = ({ key={item.id} itemId={item.id} onDelete={onDeleteItem} - handleShowDetailTask={handleShowDetailTask} - setActiveItem={setActiveItem} /> ))} diff --git a/frontend/src/components/board/TaskDetailModal.tsx b/frontend/src/components/board/TaskDetailModal.tsx index 5080bae..131a2de 100644 --- a/frontend/src/components/board/TaskDetailModal.tsx +++ b/frontend/src/components/board/TaskDetailModal.tsx @@ -1,43 +1,66 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { X, Calendar, Users, List, Edit3, Archive, // Eye, Paperclip, Copy, Trash2, MessageSquareText } from 'lucide-react'; import LoadingContent from '../ui/LoadingContent'; -import type { ItemDetail } from '@/types'; +import type { ItemDetail, UpdateItemRequest } from '@/types'; import taskService from '@/services/taskService'; import AssignMembers from './AssignMembers'; +import { hideTaskModal } from '@/store/slices/taskModalSlice'; +import { useAppDispatch } from '@/store'; +import { useSelector } from 'react-redux'; +import { selectTaskModalState } from '@/store/selectors/modalSelector'; +import { notify } from '@/services/toastService'; -interface TaskModalProps { - onClose: () => void; - itemId: number; - onUpdate: (itemId: number, updates: any) => void; - onArchive: (itemId: number) => void; -} - -const TaskDetailModal: React.FC = ({ - onClose, - itemId, - onUpdate, - onArchive, -}) => { +const TaskDetailModal: React.FC = () => { const [isLoading, setIsLoading] = useState(false); const [item, setItem] = useState(null as any); const [isEditingTitle, setIsEditingTitle] = useState(false); const [isEditingDescription, setIsEditingDescription] = useState(false); // const [comment, setComment] = useState(''); + const dispatch = useAppDispatch(); + const { taskId } = useSelector(selectTaskModalState); + + const onModalClose = useCallback(() => { + dispatch(hideTaskModal()); + }, [dispatch]); + + const archiveTask = useCallback( + async (taskId: number) => { + try { + const result = await taskService.archiveTask(taskId); + notify.success(result.message); + } catch (error: any) { + notify.error(error.response?.data?.message || 'Failed to archive task'); + } + }, + [] + ); + + const updateTask = useCallback( + async (taskId: number, data: UpdateItemRequest) => { + try { + const result = await taskService.updateTask(taskId, data); + notify.success(result.message); + } catch (error: any) { + notify.error(error.response?.data?.message || 'Failed to update task'); + } + }, + [] + ); const handleUpdate = () => { setIsEditingTitle(false); - onUpdate(item.id, item); + updateTask(item.id, { name: item.name, description: item.description as string }); }; const fetchTaskDetail = async () => { setIsLoading(true); try { - const response = await taskService.getTaskDetail(itemId); + const response = await taskService.getTaskDetail(Number(taskId)); setItem(response.data); } catch (error) { @@ -48,12 +71,20 @@ const TaskDetailModal: React.FC = ({ } }; - console.log("Rendering TaskDetailModal for itemId:", itemId, item); + // Close modal on Escape key press + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onModalClose(); + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onModalClose]); + console.log("Rendering TaskDetailModal for taskId:", taskId, item); useEffect(() => { fetchTaskDetail(); - }, [itemId]); + }, [taskId]); return (
@@ -88,7 +119,7 @@ const TaskDetailModal: React.FC = ({ )}