Merge pull request #14756 from overleaf/mf-group-invite-new-user-redirection

[web] Redirect to invite screen if new user register with a pending group invitations

GitOrigin-RevId: 39aeffd65b9d793c87e53398a700ad140794594e
This commit is contained in:
M Fahru 2023-11-09 09:33:13 -07:00 committed by Copybot
parent c4bea21ee2
commit 0639f266d8
15 changed files with 215 additions and 4 deletions

View file

@ -99,6 +99,14 @@ const SubscriptionLocator = {
Subscription.find({ invited_emails: email }, callback)
},
getGroupsWithTeamInvitesEmail(email, callback) {
Subscription.find(
{ teamInvites: { $elemMatch: { email } } },
{ teamInvites: 1 },
callback
)
},
getGroupWithV1Id(v1TeamId, callback) {
Subscription.findOne({ 'overleaf.id': v1TeamId }, callback)
},
@ -151,6 +159,9 @@ SubscriptionLocator.promises = {
getGroupsWithEmailInvite: promisify(
SubscriptionLocator.getGroupsWithEmailInvite
),
getGroupsWithTeamInvitesEmail: promisify(
SubscriptionLocator.getGroupsWithTeamInvitesEmail
),
getGroupWithV1Id: promisify(SubscriptionLocator.getGroupWithV1Id),
getUserDeletedSubscriptions: promisify(
SubscriptionLocator.getUserDeletedSubscriptions

View file

@ -64,6 +64,12 @@ module.exports = {
PermissionsController.useCapabilities(),
TeamInvitesController.viewInvite
)
webRouter.get(
'/subscription/invites/',
AuthenticationController.requireLogin(),
PermissionsController.useCapabilities(),
TeamInvitesController.viewInvites
)
webRouter.put(
'/subscription/invites/:token/',
AuthenticationController.requireLogin(),

View file

@ -171,6 +171,21 @@ async function viewInvite(req, res, next) {
}
}
async function viewInvites(req, res, next) {
const userId = SessionManager.getLoggedInUserId(req.session)
const userEmail = await UserGetter.promises.getUserEmail(userId)
const groupSubscriptions =
await SubscriptionLocator.promises.getGroupsWithTeamInvitesEmail(userEmail)
const teamInvites = groupSubscriptions.map(groupSubscription =>
groupSubscription.teamInvites.find(invite => invite.email === userEmail)
)
return res.render('subscriptions/team/group-invites', {
teamInvites,
})
}
async function acceptInvite(req, res, next) {
const { token } = req.params
const userId = SessionManager.getLoggedInUserId(req.session)
@ -255,6 +270,7 @@ async function resendInvite(req, res, next) {
module.exports = {
createInvite: expressify(createInvite),
viewInvite: expressify(viewInvite),
viewInvites: expressify(viewInvites),
acceptInvite: expressify(acceptInvite),
revokeInvite,
resendInvite: expressify(resendInvite),

View file

@ -0,0 +1,10 @@
extends ../../layout-marketing
block entrypointVar
- entrypoint = 'pages/user/subscription/group-invites'
block append meta
meta(name="ol-teamInvites" data-type="json" content=teamInvites)
block content
main.content.content-alt.team-invite#group-invites-root

View file

@ -454,6 +454,7 @@
"go_to_pdf_location_in_code": "",
"go_to_settings": "",
"group_admin": "",
"group_invitations": "",
"group_invite_has_been_sent_to_email": "",
"group_libraries": "",
"group_managed_by_group_administrator": "",
@ -580,6 +581,7 @@
"is_email_affiliated": "",
"join_now": "",
"join_project": "",
"join_team_explanation": "",
"joining": "",
"keep_current_plan": "",
"keep_personal_projects_separate": "",
@ -1372,6 +1374,7 @@
"view_hub": "",
"view_hub_subtext": "",
"view_in_template_gallery": "",
"view_invitation": "",
"view_logs": "",
"view_metrics": "",
"view_metrics_commons_subtext": "",

View file

@ -0,0 +1,28 @@
import { useTranslation } from 'react-i18next'
import type { TeamInvite } from '../../../../../../types/team-invite'
type GroupInvitesItemFooterProps = {
teamInvite: TeamInvite
}
export default function GroupInvitesItemFooter({
teamInvite,
}: GroupInvitesItemFooterProps) {
const { t } = useTranslation()
return (
<div>
<p data-cy="group-invites-item-footer-text">
{t('join_team_explanation')}
</p>
<div data-cy="group-invites-item-footer-link">
<a
className="btn btn-primary"
href={`/subscription/invites/${teamInvite.token}`}
>
{t('view_invitation')}
</a>
</div>
</div>
)
}

View file

@ -0,0 +1,31 @@
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import GroupInvitesItemFooter from './group-invites-item-footer'
import type { TeamInvite } from '../../../../../../types/team-invite'
type GroupInvitesItemProps = {
teamInvite: TeamInvite
}
export default function GroupInvitesItem({
teamInvite,
}: GroupInvitesItemProps) {
const { t } = useTranslation()
return (
<Row className="row-spaced">
<Col md={8} mdOffset={2} className="text-center">
<div className="card">
<div className="page-header">
<h2>
{t('invited_to_group', {
inviterName: teamInvite.inviterName,
})}
</h2>
</div>
<GroupInvitesItemFooter teamInvite={teamInvite} />
</div>
</Col>
</Row>
)
}

View file

@ -0,0 +1,14 @@
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
import GroupInvites from './group-invites'
function GroupInvitesRoot() {
const { isReady } = useWaitForI18n()
if (!isReady) {
return null
}
return <GroupInvites />
}
export default GroupInvitesRoot

View file

@ -0,0 +1,34 @@
import { useEffect } from 'react'
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
import { useLocation } from '@/shared/hooks/use-location'
import GroupInvitesItem from './group-invites-item'
import type { TeamInvite } from '../../../../../../types/team-invite'
function GroupInvites() {
const { t } = useTranslation()
const teamInvites: TeamInvite[] = getMeta('ol-teamInvites')
const location = useLocation()
useEffect(() => {
if (teamInvites.length === 0) {
location.assign('/project')
}
}, [teamInvites, location])
return (
<div className="container">
<Row>
<Col md={8} mdOffset={2}>
<h1>{t('group_invitations')}</h1>
</Col>
</Row>
{teamInvites.map(teamInvite => (
<GroupInvitesItem teamInvite={teamInvite} key={teamInvite._id} />
))}
</div>
)
}
export default GroupInvites

View file

@ -0,0 +1,8 @@
import './base'
import ReactDOM from 'react-dom'
import GroupInvitesRoot from '@/features/subscription/components/group-invites/group-invites-root'
const element = document.getElementById('group-invites-root')
if (element) {
ReactDOM.render(<GroupInvitesRoot />, element)
}

View file

@ -0,0 +1,44 @@
import GroupInvites from '@/features/subscription/components/group-invites/group-invites'
import type { TeamInvite } from '../../../../types/team-invite'
import { useMeta } from '../../hooks/use-meta'
import { ScopeDecorator } from '../../decorators/scope'
export const GroupInvitesDefault = () => {
const teamInvites: TeamInvite[] = [
{
email: 'email1@exammple.com',
token: 'token123',
inviterName: 'inviter1@example.com',
sentAt: new Date(),
_id: '123abc',
},
{
email: 'email2@exammple.com',
token: 'token456',
inviterName: 'inviter2@example.com',
sentAt: new Date(),
_id: '456bcd',
},
]
useMeta({ 'ol-teamInvites': teamInvites })
return (
<div className="content content-alt team-invite">
<GroupInvites />
</div>
)
}
export default {
title: 'Subscription / Group Invites',
component: GroupInvites,
args: {
show: true,
},
argTypes: {
handleHide: { action: 'close modal' },
onDisableSSO: { action: 'callback' },
},
decorators: [ScopeDecorator],
}

View file

@ -718,6 +718,7 @@
"group_admins_get_access_to": "Group admins get access to",
"group_admins_get_access_to_info": "Special features available only on group plans.",
"group_full": "This group is already full",
"group_invitations": "Group Invitations",
"group_invite_has_been_sent_to_email": "Group invite has been sent to <0>__email__</0>",
"group_libraries": "Group Libraries",
"group_managed_by_group_administrator": "User accounts in this group are managed by the group administrator.",
@ -2021,6 +2022,7 @@
"view_hub": "View Admin Hub",
"view_hub_subtext": "Access and download subscription statistics and a list of users",
"view_in_template_gallery": "View it in the template gallery",
"view_invitation": "View Invitation",
"view_logs": "View logs",
"view_metrics": "View metrics",
"view_metrics_commons_subtext": "Monitor and download usage metrics for your Commons subscription",

View file

@ -1,5 +1,5 @@
import { GroupPolicy } from '../subscription/dashboard/subscription'
import { TeamInvite } from './team-invite'
import { TeamInvite } from '../team-invite'
export type Subscription = {
_id: string

View file

@ -1,3 +0,0 @@
export type TeamInvite = {
email: string
}

View file

@ -0,0 +1,7 @@
export type TeamInvite = {
email: string
token: string
inviterName: string
sentAt: Date
_id: string
}