소스 검색

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
M Fahru 2 년 전
부모
커밋
0639f266d8

+ 11 - 0
services/web/app/src/Features/Subscription/SubscriptionLocator.js

@@ -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

+ 6 - 0
services/web/app/src/Features/Subscription/SubscriptionRouter.js

@@ -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(),

+ 16 - 0
services/web/app/src/Features/Subscription/TeamInvitesController.js

@@ -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),

+ 10 - 0
services/web/app/views/subscriptions/team/group-invites.pug

@@ -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

+ 3 - 0
services/web/frontend/extracted-translations.json

@@ -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": "",

+ 28 - 0
services/web/frontend/js/features/subscription/components/group-invites/group-invites-item-footer.tsx

@@ -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>
+  )
+}

+ 31 - 0
services/web/frontend/js/features/subscription/components/group-invites/group-invites-item.tsx

@@ -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>
+  )
+}

+ 14 - 0
services/web/frontend/js/features/subscription/components/group-invites/group-invites-root.tsx

@@ -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

+ 34 - 0
services/web/frontend/js/features/subscription/components/group-invites/group-invites.tsx

@@ -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

+ 8 - 0
services/web/frontend/js/pages/user/subscription/group-invites.tsx

@@ -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)
+}

+ 44 - 0
services/web/frontend/stories/subscription/group-invites/group-invites.stories.tsx

@@ -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],
+}

+ 2 - 0
services/web/locales/en.json

@@ -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",

+ 1 - 1
services/web/types/admin/subscription.ts

@@ -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

+ 0 - 3
services/web/types/admin/team-invite.ts

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

+ 7 - 0
services/web/types/team-invite.ts

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