From 69e2a57769846927bb555ad65e725c9f53ab43d7 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:14:43 +0300 Subject: [PATCH] Merge pull request #26141 from overleaf/ii-managed-users-consent-screen [web] Joining managed group from projects page GitOrigin-RevId: 191203559fba94cad45f35de1af2427b2abb9326 --- .../Notifications/NotificationsController.mjs | 22 ++++++ services/web/app/src/router.mjs | 6 ++ .../use-group-invitation-notification.tsx | 76 ++++++++++--------- .../notifications/group-invitation.spec.tsx | 7 ++ .../NotificationsController.test.mjs | 21 +++++ 5 files changed, 98 insertions(+), 34 deletions(-) diff --git a/services/web/app/src/Features/Notifications/NotificationsController.mjs b/services/web/app/src/Features/Notifications/NotificationsController.mjs index ae1d9208f3..35b5f0a677 100644 --- a/services/web/app/src/Features/Notifications/NotificationsController.mjs +++ b/services/web/app/src/Features/Notifications/NotificationsController.mjs @@ -33,4 +33,26 @@ export default { res.sendStatus(200) ) }, + + getNotification(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const { notificationId } = req.params + NotificationsHandler.getUserNotifications( + userId, + function (err, unreadNotifications) { + if (err) { + return next(err) + } + const notification = unreadNotifications.find( + n => n._id === notificationId + ) + + if (!notification) { + return res.status(404).end() + } + + res.json(notification) + } + ) + }, } diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs index a7e8d5e05f..7851a4a66f 100644 --- a/services/web/app/src/router.mjs +++ b/services/web/app/src/router.mjs @@ -915,6 +915,12 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) { NotificationsController.markNotificationAsRead ) + webRouter.get( + '/user/notification/:notificationId', + AuthenticationController.requireLogin(), + NotificationsController.getNotification + ) + // Deprecated in favour of /internal/project/:project_id but still used by versioning privateApiRouter.get( '/project/:project_id/details', diff --git a/services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/hooks/use-group-invitation-notification.tsx b/services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/hooks/use-group-invitation-notification.tsx index 15248f8c42..f62571b722 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/hooks/use-group-invitation-notification.tsx +++ b/services/web/frontend/js/features/project-list/components/notifications/groups/group-invitation/hooks/use-group-invitation-notification.tsx @@ -9,6 +9,7 @@ import type { NotificationGroupInvitation } from '../../../../../../../../../typ import useAsync from '../../../../../../../shared/hooks/use-async' import { FetchError, + getJSON, postJSON, putJSON, } from '../../../../../../../infrastructure/fetch-json' @@ -43,17 +44,12 @@ type UseGroupInvitationNotificationReturnType = { export function useGroupInvitationNotification( notification: NotificationGroupInvitation ): UseGroupInvitationNotificationReturnType { - const { - _id: notificationId, - messageOpts: { token, managedUsersEnabled }, - } = notification - + const { _id: notificationId } = notification const [groupInvitationStatus, setGroupInvitationStatus] = useState(GroupInvitationStatus.Idle) - const { runAsync, isLoading: isAcceptingInvitation } = useAsync< - never, - FetchError - >() + const { runAsync, isLoading } = useAsync() + const { runAsync: runAsyncNotification, isLoading: isLoadingNotification } = + useAsync() const location = useLocation() const { handleDismiss } = useAsyncDismiss() @@ -72,31 +68,41 @@ export function useGroupInvitationNotification( }, [hasIndividualPaidSubscription]) const acceptGroupInvite = useCallback(() => { - if (managedUsersEnabled) { - location.assign(`/subscription/invites/${token}/`) - } else { - runAsync( - putJSON(`/subscription/invites/${token}/`, { - body: { - _csrf: getMeta('ol-csrfToken'), - }, - }) - ) - .then(() => { - setGroupInvitationStatus(GroupInvitationStatus.SuccessfullyJoined) - }) - .catch(err => { - debugConsole.error(err) - setGroupInvitationStatus(GroupInvitationStatus.Error) - }) - .finally(() => { - // remove notification automatically in the browser - window.setTimeout(() => { - setGroupInvitationStatus(GroupInvitationStatus.NotificationIsHidden) - }, SUCCESSFUL_NOTIF_TIME_BEFORE_HIDDEN) - }) - } - }, [runAsync, token, location, managedUsersEnabled]) + // Fetch the latest notification data to ensure it's up-to-date + runAsyncNotification(getJSON(`/user/notification/${notificationId}`)) + .then(notification => { + const { + messageOpts: { token, managedUsersEnabled }, + } = notification + if (managedUsersEnabled) { + location.assign(`/subscription/invites/${token}/`) + } else { + runAsync( + putJSON(`/subscription/invites/${token}/`, { + body: { + _csrf: getMeta('ol-csrfToken'), + }, + }) + ) + .then(() => { + setGroupInvitationStatus(GroupInvitationStatus.SuccessfullyJoined) + }) + .catch(err => { + debugConsole.error(err) + setGroupInvitationStatus(GroupInvitationStatus.Error) + }) + .finally(() => { + // remove notification automatically in the browser + window.setTimeout(() => { + setGroupInvitationStatus( + GroupInvitationStatus.NotificationIsHidden + ) + }, SUCCESSFUL_NOTIF_TIME_BEFORE_HIDDEN) + }) + } + }) + .catch(debugConsole.error) + }, [runAsync, runAsyncNotification, notificationId, location]) const cancelPersonalSubscription = useCallback(() => { setGroupInvitationStatus(GroupInvitationStatus.AskToJoin) @@ -114,6 +120,8 @@ export function useGroupInvitationNotification( setGroupInvitationStatus(GroupInvitationStatus.NotificationIsHidden) }, []) + const isAcceptingInvitation = isLoadingNotification || isLoading + return { isAcceptingInvitation, groupInvitationStatus, diff --git a/services/web/test/frontend/components/project-list/notifications/group-invitation.spec.tsx b/services/web/test/frontend/components/project-list/notifications/group-invitation.spec.tsx index 31114a2405..c29de58f98 100644 --- a/services/web/test/frontend/components/project-list/notifications/group-invitation.spec.tsx +++ b/services/web/test/frontend/components/project-list/notifications/group-invitation.spec.tsx @@ -27,6 +27,10 @@ describe('', function () { } beforeEach(function () { + cy.intercept('GET', `/user/notification/${notification._id}`, { + statusCode: 200, + body: notification, + }).as('getNotification') cy.intercept( 'PUT', `/subscription/invites/${notification.messageOpts.token}`, @@ -48,6 +52,7 @@ describe('', function () { cy.findByRole('button', { name: 'Join now' }).click() + cy.wait('@getNotification') cy.wait('@acceptInvite') cy.findByText( @@ -82,6 +87,7 @@ describe('', function () { cy.findByRole('button', { name: 'Join now' }).click() + cy.wait('@getNotification') cy.wait('@acceptInvite') cy.findByText( @@ -116,6 +122,7 @@ describe('', function () { cy.findByRole('button', { name: 'Join now' }).click() + cy.wait('@getNotification') cy.wait('@acceptInvite') cy.findByText( diff --git a/services/web/test/unit/src/Notifications/NotificationsController.test.mjs b/services/web/test/unit/src/Notifications/NotificationsController.test.mjs index 6e1f9177c0..1bc5c51b31 100644 --- a/services/web/test/unit/src/Notifications/NotificationsController.test.mjs +++ b/services/web/test/unit/src/Notifications/NotificationsController.test.mjs @@ -14,6 +14,9 @@ describe('NotificationsController', function () { ctx.handler = { getUserNotifications: sinon.stub().callsArgWith(1), markAsRead: sinon.stub().callsArgWith(2), + promises: { + getUserNotifications: sinon.stub().callsArgWith(1), + }, } ctx.req = { params: { @@ -77,4 +80,22 @@ describe('NotificationsController', function () { }) }) }) + + it('should get a notification by notification id', function (ctx) { + return new Promise(resolve => { + const notification = { _id: notificationId, user_id: userId } + ctx.handler.getUserNotifications = sinon + .stub() + .callsArgWith(1, null, [notification]) + ctx.controller.getNotification(ctx.req, { + json: body => { + body.should.deep.equal(notification) + resolve() + }, + status: () => ({ + end: () => {}, + }), + }) + }) + }) })