From bdb9c9ca28c5fa23eb9ed1f181e53eb115a2f783 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 9 Jun 2025 13:13:45 +0530 Subject: [PATCH] feat(project-subscribers): implement project subscriber management and loading state - Added `getProjectSubscribers` method in `TasksControllerV2` to retrieve project subscribers with user details. - Updated socket command to handle project subscription changes, ensuring no duplicate entries on conflict. - Enhanced `ProjectViewHeader` to manage subscription loading state, providing user feedback during subscription updates. - Implemented error handling and timeout for subscription requests to improve user experience. --- .../src/controllers/tasks-controller-v2.ts | 15 ++++++ .../commands/on-project-subscriber-change.ts | 5 +- .../projectView/project-view-header.tsx | 46 +++++++++++++++++-- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/worklenz-backend/src/controllers/tasks-controller-v2.ts b/worklenz-backend/src/controllers/tasks-controller-v2.ts index 6e01c686..10c556d3 100644 --- a/worklenz-backend/src/controllers/tasks-controller-v2.ts +++ b/worklenz-backend/src/controllers/tasks-controller-v2.ts @@ -610,6 +610,21 @@ export default class TasksControllerV2 extends TasksControllerBase { return this.createTagList(result.rows); } + public static async getProjectSubscribers(projectId: string) { + const q = ` + SELECT u.name, u.avatar_url, ps.user_id, ps.team_member_id, ps.project_id + FROM project_subscribers ps + LEFT JOIN users u ON ps.user_id = u.id + WHERE ps.project_id = $1; + `; + const result = await db.query(q, [projectId]); + + for (const member of result.rows) + member.color_code = getColor(member.name); + + return this.createTagList(result.rows); + } + public static async checkUserAssignedToTask(taskId: string, userId: string, teamId: string) { const q = ` SELECT EXISTS( diff --git a/worklenz-backend/src/socket.io/commands/on-project-subscriber-change.ts b/worklenz-backend/src/socket.io/commands/on-project-subscriber-change.ts index 6057e88f..bbe90425 100644 --- a/worklenz-backend/src/socket.io/commands/on-project-subscriber-change.ts +++ b/worklenz-backend/src/socket.io/commands/on-project-subscriber-change.ts @@ -19,7 +19,8 @@ export async function on_project_subscriber_change(_io: Server, socket: Socket, const isSubscribe = data.mode == 0; const q = isSubscribe ? `INSERT INTO project_subscribers (user_id, project_id, team_member_id) - VALUES ($1, $2, $3);` + VALUES ($1, $2, $3) + ON CONFLICT (user_id, project_id, team_member_id) DO NOTHING;` : `DELETE FROM project_subscribers WHERE user_id = $1 @@ -27,7 +28,7 @@ export async function on_project_subscriber_change(_io: Server, socket: Socket, AND team_member_id = $3;`; await db.query(q, [data.user_id, data.project_id, data.team_member_id]); - const subscribers = await TasksControllerV2.getTaskSubscribers(data.project_id); + const subscribers = await TasksControllerV2.getProjectSubscribers(data.project_id); socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), subscribers); return; diff --git a/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx b/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx index 5b5d32ff..b2a17504 100644 --- a/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx @@ -67,6 +67,7 @@ const ProjectViewHeader = () => { const { loadingGroups, groupBy } = useAppSelector(state => state.taskReducer); const [creatingTask, setCreatingTask] = useState(false); + const [subscriptionLoading, setSubscriptionLoading] = useState(false); const handleRefresh = () => { if (!projectId) return; @@ -98,17 +99,51 @@ const ProjectViewHeader = () => { }; const handleSubscribe = () => { - if (selectedProject?.id) { + if (!selectedProject?.id || !socket || subscriptionLoading) return; + + try { + setSubscriptionLoading(true); const newSubscriptionState = !selectedProject.subscribed; - dispatch(setProject({ ...selectedProject, subscribed: newSubscriptionState })); - - socket?.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), { + // Emit socket event first, then update state based on response + socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), { project_id: selectedProject.id, user_id: currentSession?.id, team_member_id: currentSession?.team_member_id, - mode: newSubscriptionState ? 1 : 0, + mode: newSubscriptionState ? 0 : 1, // Fixed: 0 for subscribe, 1 for unsubscribe }); + + // Listen for the response to confirm the operation + socket.once(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), (response) => { + try { + // Update the project state with the confirmed subscription status + dispatch(setProject({ + ...selectedProject, + subscribed: newSubscriptionState + })); + } catch (error) { + logger.error('Error handling project subscription response:', error); + // Revert optimistic update on error + dispatch(setProject({ + ...selectedProject, + subscribed: selectedProject.subscribed + })); + } finally { + setSubscriptionLoading(false); + } + }); + + // Add timeout in case socket response never comes + setTimeout(() => { + if (subscriptionLoading) { + setSubscriptionLoading(false); + logger.error('Project subscription timeout - no response from server'); + } + }, 5000); + + } catch (error) { + logger.error('Error updating project subscription:', error); + setSubscriptionLoading(false); } }; @@ -239,6 +274,7 @@ const ProjectViewHeader = () => {