Skip to content

Commit 32827e7

Browse files
feat/FE: optimize re-render workspace when open task detail (#25)
1 parent 3b4dddd commit 32827e7

File tree

9 files changed

+104
-66
lines changed

9 files changed

+104
-66
lines changed

frontend/src/pages/WorkspaceBoard.tsx renamed to frontend/src/components/board/BoardContent.tsx

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import BoardClosedBanner from '@/components/board/BoardClosedBanner';
22
import BoardNavbar from '@/components/board/BoardNavbar';
33
import DroppableColumn from '@/components/board/DroppableColumn';
4-
import TaskDetailModal from '@/components/board/TaskDetailModal';
54
import LoadingContent from '@/components/ui/LoadingContent';
65
import { useBoardData } from '@/hooks/useBoardData';
76
import { useBoardOperations } from '@/hooks/useBoardOperations';
@@ -11,7 +10,7 @@ import { useAppSelector } from '@/store';
1110
import { selectActiveColumns } from '@/store/selectors/columnsSelector';
1211
import { selectTaskById, selectTasksByColumns } from '@/store/selectors/tasksSelectors';
1312
import { columnsReordered, setColumns } from '@/store/slices/columnsSlice';
14-
import type { Column, Task } from '@/types';
13+
import type { Column } from '@/types';
1514
import {
1615
DndContext,
1716
DragOverlay,
@@ -33,16 +32,14 @@ import {
3332
sortableKeyboardCoordinates,
3433
} from '@dnd-kit/sortable';
3534
import { GripVertical, Plus } from 'lucide-react';
36-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
35+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
3736
import { useDispatch } from 'react-redux';
3837
import { useParams } from 'react-router-dom';
3938

4039
const WorkspaceBoard = () => {
4140

4241
const { boardId } = useParams();
4342
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
44-
const [showDetailModal, setShowDetailModal] = useState(false);
45-
const [activeItem, setActiveItem] = useState<Task | null>(null);
4643
const containerRef = useRef<HTMLDivElement>(null);
4744
const dispatch = useDispatch();
4845
const columns = useAppSelector(selectActiveColumns);
@@ -408,15 +405,7 @@ const WorkspaceBoard = () => {
408405
setCardTitle('');
409406
}, [isBoardClosed]);
410407

411-
const handleHideDetailModal = useCallback(() => {
412-
setShowDetailModal(false);
413-
}, []);
414-
415-
const handleShowDetailModal = useCallback(() => {
416-
setShowDetailModal(true);
417-
}, []);
418-
419-
console.log("worspace board")
408+
console.log("worspace board: " + boardId);
420409
return (
421410
<div className='bg-[#283449] w-full h-full flex flex-col'>
422411
{
@@ -457,8 +446,6 @@ const WorkspaceBoard = () => {
457446
onUpdateColumnTitle={updateColumnTitle}
458447
onArchiveColumn={archiveColumn}
459448
onArchiveAllItems={archiveAllTasksInColumn}
460-
handleShowDetailTask={handleShowDetailModal}
461-
setActiveItem={setActiveItem}
462449
/>
463450
))}
464451
</SortableContext>
@@ -501,20 +488,10 @@ const WorkspaceBoard = () => {
501488
</DragOverlay>
502489
</DndContext>
503490
</div>
504-
{
505-
showDetailModal &&
506-
<TaskDetailModal
507-
onClose={handleHideDetailModal}
508-
itemId={activeItem?.id || 0}
509-
// item={activeItem}
510-
onArchive={archiveTask}
511-
onUpdate={updateTask}
512-
/>
513-
}
514491
</>
515492
}
516493
</div>
517494
);
518495
};
519496

520-
export default WorkspaceBoard;
497+
export default memo(WorkspaceBoard);

frontend/src/components/board/DraggableItem.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1-
import type { Item, Task } from '@/types';
21
import { detectUrl } from '@/utils/UrlPreviewUtils';
32
import { useSortable } from '@dnd-kit/sortable';
43
import { CSS } from '@dnd-kit/utilities';
54
import { X } from 'lucide-react';
65
import UrlPreview from './UrlPreview';
7-
import { useAppSelector, type RootState } from '@/store';
6+
import { useAppDispatch, useAppSelector, type RootState } from '@/store';
87
import { useSelector } from 'react-redux';
98
import { selectTaskById } from '@/store/selectors/tasksSelectors';
9+
import { showTaskModal } from '@/store/slices/taskModalSlice';
1010

1111
interface DraggableItemProps {
1212
onDelete: (itemId: number) => void;
13-
handleShowDetailTask: () => void;
1413
label?: string; // e.g., "FE", "BE"
15-
setActiveItem: (item: Task) => void;
1614
itemId: number;
1715
}
1816

1917
const DraggableItem: React.FC<DraggableItemProps> = ({
2018
onDelete,
21-
handleShowDetailTask,
2219
label,
23-
setActiveItem,
2420
itemId
2521
}) => {
2622
const item = useAppSelector((state) => selectTaskById(itemId)(state));
@@ -45,6 +41,7 @@ const DraggableItem: React.FC<DraggableItemProps> = ({
4541
};
4642

4743
const members = useSelector((state: RootState) => state.members);
44+
const dispatch = useAppDispatch();
4845

4946
// console.log("Rendering DraggableItem:", item.id, item.name);
5047
return (
@@ -54,8 +51,7 @@ const DraggableItem: React.FC<DraggableItemProps> = ({
5451
{...attributes}
5552
{...listeners}
5653
onClick={() => {
57-
detectUrl(item.name) ? undefined : handleShowDetailTask();
58-
setActiveItem(item);
54+
detectUrl(item.name) ? undefined : dispatch(showTaskModal(item.id));
5955
}}
6056
className={`
6157
select-none bg-[#222f44] p-2 rounded-lg

frontend/src/components/board/DroppableColumn.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ interface DroppableColumnProps {
3232
onUpdateColumnTitle: (columnId: number, newTitle: string) => void;
3333
onArchiveColumn: (column: Column) => void;
3434
onArchiveAllItems: (columnId: number) => void;
35-
handleShowDetailTask: () => void;
36-
setActiveItem: (item: Task) => void;
3735
}
3836

3937
const DroppableColumnComponent: React.FC<DroppableColumnProps> = ({
@@ -49,8 +47,6 @@ const DroppableColumnComponent: React.FC<DroppableColumnProps> = ({
4947
onUpdateColumnTitle,
5048
onArchiveColumn,
5149
onArchiveAllItems,
52-
handleShowDetailTask,
53-
setActiveItem,
5450
}) => {
5551
const [isEditingTitle, setIsEditingTitle] = useState(false);
5652
const [showOptionsMenu, setShowOptionsMenu] = useState(false);
@@ -296,8 +292,6 @@ const DroppableColumnComponent: React.FC<DroppableColumnProps> = ({
296292
key={item.id}
297293
itemId={item.id}
298294
onDelete={onDeleteItem}
299-
handleShowDetailTask={handleShowDetailTask}
300-
setActiveItem={setActiveItem}
301295
/>
302296
))}
303297
</div>

frontend/src/components/board/TaskDetailModal.tsx

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,66 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useState } from 'react';
22
import {
33
X, Calendar, Users,
44
List, Edit3, Archive,
55
// Eye, Paperclip, Copy, Trash2, MessageSquareText
66
} from 'lucide-react';
77
import LoadingContent from '../ui/LoadingContent';
8-
import type { ItemDetail } from '@/types';
8+
import type { ItemDetail, UpdateItemRequest } from '@/types';
99
import taskService from '@/services/taskService';
1010
import AssignMembers from './AssignMembers';
11+
import { hideTaskModal } from '@/store/slices/taskModalSlice';
12+
import { useAppDispatch } from '@/store';
13+
import { useSelector } from 'react-redux';
14+
import { selectTaskModalState } from '@/store/selectors/modalSelector';
15+
import { notify } from '@/services/toastService';
1116

12-
interface TaskModalProps {
13-
onClose: () => void;
14-
itemId: number;
15-
onUpdate: (itemId: number, updates: any) => void;
16-
onArchive: (itemId: number) => void;
17-
}
18-
19-
const TaskDetailModal: React.FC<TaskModalProps> = ({
20-
onClose,
21-
itemId,
22-
onUpdate,
23-
onArchive,
24-
}) => {
17+
const TaskDetailModal: React.FC = () => {
2518
const [isLoading, setIsLoading] = useState(false);
2619
const [item, setItem] = useState<ItemDetail>(null as any);
2720
const [isEditingTitle, setIsEditingTitle] = useState(false);
2821
const [isEditingDescription, setIsEditingDescription] = useState(false);
2922
// const [comment, setComment] = useState('');
3023

24+
const dispatch = useAppDispatch();
25+
const { taskId } = useSelector(selectTaskModalState);
26+
27+
const onModalClose = useCallback(() => {
28+
dispatch(hideTaskModal());
29+
}, [dispatch]);
30+
31+
const archiveTask = useCallback(
32+
async (taskId: number) => {
33+
try {
34+
const result = await taskService.archiveTask(taskId);
35+
notify.success(result.message);
36+
} catch (error: any) {
37+
notify.error(error.response?.data?.message || 'Failed to archive task');
38+
}
39+
},
40+
[]
41+
);
42+
43+
const updateTask = useCallback(
44+
async (taskId: number, data: UpdateItemRequest) => {
45+
try {
46+
const result = await taskService.updateTask(taskId, data);
47+
notify.success(result.message);
48+
} catch (error: any) {
49+
notify.error(error.response?.data?.message || 'Failed to update task');
50+
}
51+
},
52+
[]
53+
);
3154

3255
const handleUpdate = () => {
3356
setIsEditingTitle(false);
34-
onUpdate(item.id, item);
57+
updateTask(item.id, { name: item.name, description: item.description as string });
3558
};
3659

3760
const fetchTaskDetail = async () => {
3861
setIsLoading(true);
3962
try {
40-
const response = await taskService.getTaskDetail(itemId);
63+
const response = await taskService.getTaskDetail(Number(taskId));
4164
setItem(response.data);
4265
}
4366
catch (error) {
@@ -48,12 +71,20 @@ const TaskDetailModal: React.FC<TaskModalProps> = ({
4871
}
4972
};
5073

51-
console.log("Rendering TaskDetailModal for itemId:", itemId, item);
74+
// Close modal on Escape key press
75+
useEffect(() => {
76+
const handleKeyDown = (e: KeyboardEvent) => {
77+
if (e.key === "Escape") onModalClose();
78+
};
79+
document.addEventListener("keydown", handleKeyDown);
80+
return () => document.removeEventListener("keydown", handleKeyDown);
81+
}, [onModalClose]);
5282

83+
console.log("Rendering TaskDetailModal for taskId:", taskId, item);
5384

5485
useEffect(() => {
5586
fetchTaskDetail();
56-
}, [itemId]);
87+
}, [taskId]);
5788

5889
return (
5990
<div className="fixed inset-0 bg-black/50 flex items-start justify-center z-40 pt-8 px-4">
@@ -88,7 +119,7 @@ const TaskDetailModal: React.FC<TaskModalProps> = ({
88119
</h1>
89120
)}
90121
<button
91-
onClick={onClose}
122+
onClick={onModalClose}
92123
className="text-gray-400 hover:text-white p-1 rounded hover:bg-gray-600 hover:bg-opacity-50"
93124
>
94125
<X size={18} />
@@ -117,7 +148,7 @@ const TaskDetailModal: React.FC<TaskModalProps> = ({
117148
<div className="pl-6 flex flex-wrap gap-2">
118149

119150
{/* Member section */}
120-
<AssignMembers assignedMembers={item?.assignedMembers} taskId={itemId}/>
151+
<AssignMembers assignedMembers={item?.assignedMembers} taskId={Number(taskId)} />
121152

122153
{/* Labels Section */}
123154
<div className="pb-4">
@@ -272,8 +303,8 @@ const TaskDetailModal: React.FC<TaskModalProps> = ({
272303
<button
273304
className="w-full text-left px-2 py-2 text-sm text-gray-300 hover:bg-gray-600 hover:bg-opacity-50 rounded flex items-center gap-2"
274305
onClick={() => {
275-
onArchive(item.id);
276-
onClose();
306+
archiveTask(item.id);
307+
onModalClose();
277308
}}
278309
>
279310
<Archive size={14} />

frontend/src/pages/Board.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useSelector } from "react-redux";
2+
import WorkspaceBoard from "../components/board/BoardContent";
3+
import { selectTaskModalState } from "@/store/selectors/modalSelector";
4+
import TaskDetailModal from "@/components/board/TaskDetailModal";
5+
6+
const Board = () => {
7+
const { isTaskModalShow } = useSelector(selectTaskModalState);
8+
return (
9+
<>
10+
<WorkspaceBoard />
11+
{isTaskModalShow && <TaskDetailModal />}
12+
</>
13+
);
14+
}
15+
16+
export default Board;

frontend/src/router/AppRouter.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import Template from '@/pages/Template';
2020
import Home from '@/pages/Home';
2121
import Member from '@/pages/workspace/Member';
2222
import Setting from '@/pages/workspace/Setting';
23-
import WorkspaceBoard from '@/pages/WorkspaceBoard';
23+
import WorkspaceBoard from '@/components/board/BoardContent';
2424
import HomeBoards from '@/pages/HomeBoards';
2525
import Boards from '@/pages/workspace/Board';
2626
import WorkspaceDetail from '@/pages/workspace/WorkspaceDetail';
2727
import SearchPage from '@/pages/SearchPage';
28+
import Board from '@/pages/Board';
2829

2930
// Layout wrappers for different roles
3031
const AdminLayout = () => (
@@ -105,7 +106,7 @@ const router = createBrowserRouter([
105106
},
106107
{
107108
path: 'board/:boardId',
108-
element: <WorkspaceBoard />,
109+
element: <Board />,
109110
},
110111
{
111112
path: 'workspace',

frontend/src/store/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import tasksReducer from "./slices/tasksSlice";
88
import membersReducer from "./slices/membersSlice";
99
import archivedColumnsReducer from "./slices/archiveColumnsSlice";
1010
import archivedTasksReducer from "./slices/archiveTasksSlice";
11+
import taskModalReducer from "./slices/taskModalSlice";
12+
1113
import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux';
1214

1315
// Persist configuration
@@ -24,6 +26,7 @@ const rootReducer = combineReducers({
2426
archivedColumns: archivedColumnsReducer,
2527
archivedTasks: archivedTasksReducer,
2628
members: membersReducer,
29+
taskModal: taskModalReducer,
2730
});
2831

2932
const persistedReducer = persistReducer(persistConfig, rootReducer);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { RootState } from "..";
2+
3+
export const selectTaskModalState = (state: RootState) => state.taskModal;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createSlice } from '@reduxjs/toolkit';
2+
const taskModalSlice = createSlice({
3+
name: 'taskModal',
4+
initialState: { isTaskModalShow: false, taskId: null },
5+
reducers: {
6+
showTaskModal: (state, action) => {
7+
state.isTaskModalShow = true;
8+
state.taskId = action.payload;
9+
},
10+
hideTaskModal: (state) => {
11+
state.isTaskModalShow = false;
12+
state.taskId = null;
13+
}
14+
}
15+
});
16+
export const { showTaskModal, hideTaskModal } = taskModalSlice.actions;
17+
export default taskModalSlice.reducer;

0 commit comments

Comments
 (0)