Browse Source

Merge pull request #33717 from overleaf/as-new-link-sharing-pass-groups

[web] Pass project owner link sharing settings to frontend

GitOrigin-RevId: ff000f38ef36a3d448a2934d0c5963139852eee2
MoxAmber 2 months ago
parent
commit
886985d91c

+ 13 - 1
services/web/app/src/Features/Project/ProjectController.mjs

@@ -572,9 +572,20 @@ const _ProjectController = {
                 )
               ).isMember)()
           : false,
+        activeGroupSubscriptions:
+          SubscriptionLocator.promises.getUserActiveGroupSubscriptions(userId, {
+            _id: 1,
+            teamName: 1,
+            sharingPermissions: 1,
+          }),
       })
 
-      const { project, userValues, userIsMemberOfGroupSubscription } = responses
+      const {
+        project,
+        userValues,
+        userIsMemberOfGroupSubscription,
+        activeGroupSubscriptions,
+      } = responses
 
       await Promise.all([
         InactiveProjectManager.promises.reactivateProjectIfRequired(project),
@@ -959,6 +970,7 @@ const _ProjectController = {
           ),
           isMemberOfGroupSubscription: userIsMemberOfGroupSubscription,
           hasInstitutionLicence: userHasInstitutionLicence,
+          activeGroupSubscriptions,
         },
         initialLoadingScreenTheme,
         userSettings,

+ 21 - 0
services/web/app/src/Features/Subscription/SubscriptionLocator.mjs

@@ -180,6 +180,27 @@ const SubscriptionLocator = {
     }
   },
 
+  async getUserActiveGroupSubscriptions(userOrId, projection = {}) {
+    if (!Features.hasFeature('saas')) return []
+
+    const userId = SubscriptionLocator._getUserId(userOrId)
+    return await Subscription.find(
+      {
+        groupPlan: true,
+        $and: [
+          { $or: [{ admin_id: userId }, { member_ids: userId }] },
+          {
+            $or: [
+              { 'recurlyStatus.state': 'active' },
+              { 'paymentProvider.state': 'active' },
+            ],
+          },
+        ],
+      },
+      projection
+    ).exec()
+  },
+
   async getUserSubscriptionStatus(userId) {
     let usersSubscription = { personal: false, group: false }
 

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

@@ -161,6 +161,7 @@
   "anyone": "",
   "anyone_in_x": "",
   "anyone_in_x_with_the_link": "",
+  "anyone_in_your_group_with_the_link": "",
   "anyone_with_link_can_edit": "",
   "anyone_with_link_can_view": "",
   "anyone_with_the_link": "",

+ 1 - 1
services/web/frontend/js/features/share-project-modal/components/copy-sharing-link-button.tsx

@@ -16,7 +16,7 @@ export default function CopySharingLinkButton() {
   const isCopyBtnEnabled =
     Boolean(navigator.clipboard?.writeText) &&
     Boolean(sharingLinkData?.token) &&
-    (projectAccess === 'anyoneInXyzWithTheLink' ||
+    (projectAccess?.startsWith('anyoneInXyzWithTheLink') ||
       projectAccess === 'anyoneWithTheLink')
 
   const handleCopyClick = () => {

+ 54 - 26
services/web/frontend/js/features/share-project-modal/components/project-access.tsx

@@ -28,6 +28,7 @@ import {
   useShareProjectContext,
 } from '@/features/share-project-modal/components/share-project-modal'
 import { ExcludeStrict } from '@ol-types/utils'
+import getMeta from '@/utils/meta'
 
 type ProjectAccessProps = {
   setIsInvitedPeopleScreen: React.Dispatch<React.SetStateAction<boolean>>
@@ -48,8 +49,7 @@ function ProjectAccess({
     null
   )
   const { isProjectOwner } = useEditorContext()
-  // TODO get the group name from backend
-  const groupName = 'your group'
+  const { activeGroupSubscriptions } = getMeta('ol-user')
 
   const {
     monitorRequest,
@@ -69,13 +69,11 @@ function ProjectAccess({
     let reqBody: Pick<SharingLinkData, 'privileges' | 'subscriptionId'>
     if (newAccess === 'onlyInvitedPeople') {
       reqBody = { privileges: false }
-    } else if (
-      newAccess === 'anyoneInXyzWithTheLink' &&
-      sharingLinkData?.subscriptionId
-    ) {
+    } else if (newAccess.startsWith('anyoneInXyzWithTheLink')) {
+      const newSubscriptionId = newAccess.split('.')[1]
       reqBody = {
         privileges: privileges || 'readOnly',
-        subscriptionId: sharingLinkData.subscriptionId,
+        subscriptionId: newSubscriptionId,
       }
     } else if (newAccess === 'anyoneWithTheLink') {
       reqBody = { privileges: privileges || 'readOnly' }
@@ -125,14 +123,34 @@ function ProjectAccess({
     })
   }
 
+  const getGroupLinkText = (id?: string) => {
+    if (
+      !id ||
+      !activeGroupSubscriptions ||
+      activeGroupSubscriptions.length === 0
+    ) {
+      return ''
+    }
+    const subscription = activeGroupSubscriptions.find(sub => sub._id === id)
+    if (subscription?.teamName) {
+      return t('anyone_in_x_with_the_link', {
+        groupName: subscription.teamName,
+      })
+    } else {
+      return t('anyone_in_your_group_with_the_link')
+    }
+  }
+
   const getProjectAccessDropdownToggleText = () => {
-    switch (projectAccess) {
+    if (!projectAccess) return ''
+    const [accessType, subscriptionId] = projectAccess.split('.')
+    switch (accessType) {
       case 'legacyLinkSharing':
         return t('via_sharing_links_legacy')
       case 'onlyInvitedPeople':
         return t('only_invited_people')
       case 'anyoneInXyzWithTheLink':
-        return t('anyone_in_x_with_the_link', { groupName })
+        return getGroupLinkText(subscriptionId)
       case 'anyoneWithTheLink':
         return t('anyone_with_the_link')
       default:
@@ -167,7 +185,7 @@ function ProjectAccess({
             {projectAccess === 'onlyInvitedPeople' && (
               <MaterialIcon type="lock" unfilled />
             )}
-            {projectAccess === 'anyoneInXyzWithTheLink' && (
+            {projectAccess.startsWith('anyoneInXyzWithTheLink') && (
               <MaterialIcon type="domain" unfilled />
             )}
             {projectAccess === 'anyoneWithTheLink' && (
@@ -217,23 +235,33 @@ function ProjectAccess({
                     {t('only_invited_people')}
                   </DropdownItem>
                 </DropdownListItem>
-                {sharingLinkData?.subscriptionId && (
-                  <DropdownListItem className="d-flex align-items-center">
-                    <DropdownItem
-                      as="button"
-                      eventKey="anyoneInXyzWithTheLink"
-                      leadingIcon={<MaterialIcon type="domain" unfilled />}
-                      trailingIcon={
-                        projectAccess === 'anyoneInXyzWithTheLink'
-                          ? 'check'
-                          : undefined
-                      }
-                      active={projectAccess === 'anyoneInXyzWithTheLink'}
+                {activeGroupSubscriptions &&
+                  activeGroupSubscriptions.map(subscription => (
+                    <DropdownListItem
+                      className="d-flex align-items-center"
+                      key={subscription._id}
                     >
-                      {t('anyone_in_x_with_the_link', { groupName })}
-                    </DropdownItem>
-                  </DropdownListItem>
-                )}
+                      <DropdownItem
+                        as="button"
+                        eventKey={`anyoneInXyzWithTheLink.${subscription._id}`}
+                        leadingIcon={<MaterialIcon type="domain" unfilled />}
+                        trailingIcon={
+                          projectAccess ===
+                          `anyoneInXyzWithTheLink.${subscription._id}`
+                            ? 'check'
+                            : undefined
+                        }
+                        active={
+                          projectAccess ===
+                          `anyoneInXyzWithTheLink.${subscription._id}`
+                        }
+                      >
+                        {t('anyone_in_x_with_the_link', {
+                          groupName: subscription.teamName || 'your group',
+                        })}
+                      </DropdownItem>
+                    </DropdownListItem>
+                  ))}
                 <DropdownListItem className="d-flex align-items-center gap-2">
                   <DropdownItem
                     as="button"

+ 1 - 1
services/web/frontend/js/features/share-project-modal/components/remove-sharing-links-modal.tsx

@@ -28,7 +28,7 @@ function RemoveSharingLinksModal({
       'this_change_will_permanently_remove_your_original_links_you_can_still_share_new_link'
     )
   } else if (
-    pendingAccess === 'anyoneInXyzWithTheLink' ||
+    pendingAccess.startsWith('anyoneInXyzWithTheLink') ||
     pendingAccess === 'anyoneWithTheLink'
   ) {
     confirmationModalBodyText = t(

+ 2 - 1
services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx

@@ -137,8 +137,9 @@ function ShareProjectModalContentInner({
           {isSharingUpdatesEnabled ? (
             <>
               {!isInvitedPeopleScreen &&
+                projectAccess &&
                 (projectAccess === 'onlyInvitedPeople' ||
-                  projectAccess === 'anyoneInXyzWithTheLink' ||
+                  projectAccess.startsWith('anyoneInXyzWithTheLink') ||
                   projectAccess === 'anyoneWithTheLink') && (
                   <CopySharingLinkButton />
                 )}

+ 2 - 2
services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx

@@ -20,7 +20,7 @@ import {
 export type ProjectAccessType =
   | 'legacyLinkSharing'
   | 'onlyInvitedPeople'
-  | 'anyoneInXyzWithTheLink'
+  | `anyoneInXyzWithTheLink.${string}`
   | 'anyoneWithTheLink'
 
 export type ShareProjectContextValue = {
@@ -161,7 +161,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
       if (!data.privileges) {
         setProjectAccess('onlyInvitedPeople')
       } else if (data.subscriptionId) {
-        setProjectAccess('anyoneInXyzWithTheLink')
+        setProjectAccess(`anyoneInXyzWithTheLink.${data.subscriptionId}`)
       } else {
         setProjectAccess('anyoneWithTheLink')
       }

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

@@ -207,6 +207,7 @@
   "anyone": "Anyone",
   "anyone_in_x": "Anyone in __groupName__",
   "anyone_in_x_with_the_link": "Anyone in __groupName__ with the link",
+  "anyone_in_your_group_with_the_link": "Anyone in your group with the link",
   "anyone_with_link_can_edit": "Anyone with this link can edit this project",
   "anyone_with_link_can_view": "Anyone with this link can view this project",
   "anyone_with_the_link": "Anyone with the link",

+ 2 - 1
services/web/test/frontend/features/chat/components/chat.test.tsx

@@ -9,10 +9,11 @@ import fetchMock from 'fetch-mock'
 import ChatPane from '../../../../../frontend/js/features/chat/components/chat-pane'
 import { renderWithEditorContext } from '../../../helpers/render-with-context'
 import { stubMathJax, tearDownMathJaxStubs } from './stubs'
+import { UserId } from '@ol-types/user'
 
 describe('<ChatPane />', function () {
   const user = {
-    id: 'fake_user',
+    id: 'fake_user' as UserId,
     first_name: 'fake_user_first_name',
     email: 'fake@example.com',
   }

+ 11 - 7
services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx

@@ -253,7 +253,7 @@ describe('<ShareProjectModal/>', function () {
     renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
       ...createContextProps({ publicAccessLevel: 'tokenBased', invites }),
       user: {
-        id: 'non-project-owner',
+        id: 'non-project-owner' as UserId,
         email: 'non-project-owner@example.com',
       },
     })
@@ -283,7 +283,7 @@ describe('<ShareProjectModal/>', function () {
     renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
       ...createContextProps({ publicAccessLevel: 'private', invites }),
       user: {
-        id: 'non-project-owner',
+        id: 'non-project-owner' as UserId,
         email: 'non-project-owner@example.com',
       },
     })
@@ -1057,10 +1057,14 @@ describe('<ShareProjectModal/>', function () {
         subscriptionId: 'sub-123',
       })
 
-      renderWithEditorContext(
-        <ShareProjectModal {...modalProps} />,
-        createContextProps({ publicAccessLevel: 'private' })
-      )
+      renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
+        ...createContextProps({ publicAccessLevel: 'private' }),
+        user: {
+          id: USER_ID,
+          email: USER_EMAIL,
+          activeGroupSubscriptions: [{ _id: 'sub-123' }],
+        },
+      })
 
       await screen.findByText('Anyone in your group with the link')
     })
@@ -1331,7 +1335,7 @@ describe('<ShareProjectModal/>', function () {
       renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
         ...createContextProps(),
         user: {
-          id: 'non-project-owner',
+          id: 'non-project-owner' as UserId,
           email: 'non-project-owner@example.com',
         },
       })

+ 3 - 2
services/web/test/frontend/features/source-editor/components/codemirror-editor-autocomplete.spec.tsx

@@ -8,6 +8,7 @@ import { TestContainer } from '../helpers/test-container'
 import { FC } from 'react'
 import { MetadataContext } from '@/features/ide-react/context/metadata-context'
 import { ReferencesContext } from '@/features/ide-react/context/references-context'
+import { UserId } from '@ol-types/user'
 
 describe('autocomplete', { scrollBehavior: false }, function () {
   beforeEach(function () {
@@ -764,7 +765,7 @@ describe('autocomplete', { scrollBehavior: false }, function () {
 
     window.metaAttributesCache.set('ol-showSymbolPalette', true)
     const user = {
-      id: '123abd',
+      id: '123abd' as UserId,
       email: 'testuser@example.com',
     }
     cy.mount(
@@ -794,7 +795,7 @@ describe('autocomplete', { scrollBehavior: false }, function () {
 
     window.metaAttributesCache.set('ol-showSymbolPalette', false)
     const user = {
-      id: '123abd',
+      id: '123abd' as UserId,
       email: 'testuser@example.com',
     }
     cy.mount(

+ 9 - 7
services/web/test/frontend/helpers/editor-providers.tsx

@@ -45,7 +45,7 @@ import {
   ProjectMetadata,
   ProjectUpdate,
 } from '@/shared/context/types/project-metadata'
-import { UserId } from '../../../types/user'
+import { User, UserId } from '../../../types/user'
 import { ProjectCompiler } from '../../../types/project-settings'
 import { ReferencesContext } from '@/features/ide-react/context/references-context'
 import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
@@ -70,12 +70,14 @@ const defaultUserSettings = {
 } satisfies UserSettings
 
 export type EditorProvidersProps = {
-  user?: {
-    id: string
-    email: string
-    signUpDate?: string
-    isProfessionalGroupPlan?: boolean
-  }
+  user?: Pick<
+    User,
+    | 'id'
+    | 'email'
+    | 'signUpDate'
+    | 'activeGroupSubscriptions'
+    | 'isProfessionalGroupPlan'
+  >
   projectId?: string
   projectName?: string
   projectOwner?: ProjectMetadata['owner']

+ 1 - 0
services/web/test/unit/src/Project/ProjectController.test.mjs

@@ -71,6 +71,7 @@ describe('ProjectController', function () {
     ctx.SubscriptionLocator = {
       promises: {
         getUsersSubscription: sinon.stub().resolves(),
+        getUserActiveGroupSubscriptions: sinon.stub().resolves(),
       },
     }
     ctx.SubscriptionController = {

+ 6 - 0
services/web/types/user.ts

@@ -1,3 +1,4 @@
+import { SharingPermissions } from '../modules/sharing-permissions/app/src/types'
 import { Brand } from './helpers/brand'
 
 export type RefProviders = {
@@ -64,6 +65,11 @@ export type User = {
   isMemberOfGroupSubscription?: boolean
   isProfessionalGroupPlan?: boolean
   hasInstitutionLicence?: boolean
+  activeGroupSubscriptions?: {
+    _id: string
+    teamName?: string
+    sharingPermissions?: SharingPermissions
+  }[]
 }
 
 export type LoggedOutUser = {