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.
This commit is contained in:
@@ -610,6 +610,21 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
return this.createTagList(result.rows);
|
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) {
|
public static async checkUserAssignedToTask(taskId: string, userId: string, teamId: string) {
|
||||||
const q = `
|
const q = `
|
||||||
SELECT EXISTS(
|
SELECT EXISTS(
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export async function on_project_subscriber_change(_io: Server, socket: Socket,
|
|||||||
const isSubscribe = data.mode == 0;
|
const isSubscribe = data.mode == 0;
|
||||||
const q = isSubscribe
|
const q = isSubscribe
|
||||||
? `INSERT INTO project_subscribers (user_id, project_id, team_member_id)
|
? `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
|
: `DELETE
|
||||||
FROM project_subscribers
|
FROM project_subscribers
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
@@ -27,7 +28,7 @@ export async function on_project_subscriber_change(_io: Server, socket: Socket,
|
|||||||
AND team_member_id = $3;`;
|
AND team_member_id = $3;`;
|
||||||
await db.query(q, [data.user_id, data.project_id, data.team_member_id]);
|
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);
|
socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), subscribers);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const ProjectViewHeader = () => {
|
|||||||
const { loadingGroups, groupBy } = useAppSelector(state => state.taskReducer);
|
const { loadingGroups, groupBy } = useAppSelector(state => state.taskReducer);
|
||||||
|
|
||||||
const [creatingTask, setCreatingTask] = useState(false);
|
const [creatingTask, setCreatingTask] = useState(false);
|
||||||
|
const [subscriptionLoading, setSubscriptionLoading] = useState(false);
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
@@ -98,17 +99,51 @@ const ProjectViewHeader = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubscribe = () => {
|
const handleSubscribe = () => {
|
||||||
if (selectedProject?.id) {
|
if (!selectedProject?.id || !socket || subscriptionLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubscriptionLoading(true);
|
||||||
const newSubscriptionState = !selectedProject.subscribed;
|
const newSubscriptionState = !selectedProject.subscribed;
|
||||||
|
|
||||||
dispatch(setProject({ ...selectedProject, subscribed: newSubscriptionState }));
|
// Emit socket event first, then update state based on response
|
||||||
|
socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), {
|
||||||
socket?.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), {
|
|
||||||
project_id: selectedProject.id,
|
project_id: selectedProject.id,
|
||||||
user_id: currentSession?.id,
|
user_id: currentSession?.id,
|
||||||
team_member_id: currentSession?.team_member_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 = () => {
|
|||||||
<Tooltip title={t('subscribe')}>
|
<Tooltip title={t('subscribe')}>
|
||||||
<Button
|
<Button
|
||||||
shape="round"
|
shape="round"
|
||||||
|
loading={subscriptionLoading}
|
||||||
icon={selectedProject?.subscribed ? <BellFilled /> : <BellOutlined />}
|
icon={selectedProject?.subscribed ? <BellFilled /> : <BellOutlined />}
|
||||||
onClick={handleSubscribe}
|
onClick={handleSubscribe}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user