From 886985d91cb0f0593bfcc92324585ae59facd4d7 Mon Sep 17 00:00:00 2001 From: MoxAmber Date: Thu, 11 Jun 2026 10:13:56 +0100 Subject: [PATCH] Merge pull request #33717 from overleaf/as-new-link-sharing-pass-groups [web] Pass project owner link sharing settings to frontend GitOrigin-RevId: ff000f38ef36a3d448a2934d0c5963139852eee2 --- .../Features/Project/ProjectController.mjs | 14 +++- .../Subscription/SubscriptionLocator.mjs | 21 +++++ .../web/frontend/extracted-translations.json | 1 + .../components/copy-sharing-link-button.tsx | 2 +- .../components/project-access.tsx | 80 +++++++++++++------ .../components/remove-sharing-links-modal.tsx | 2 +- .../share-project-modal-content.tsx | 3 +- .../components/share-project-modal.tsx | 4 +- services/web/locales/en.json | 1 + .../features/chat/components/chat.test.tsx | 3 +- .../components/share-project-modal.test.tsx | 18 +++-- .../codemirror-editor-autocomplete.spec.tsx | 5 +- .../frontend/helpers/editor-providers.tsx | 16 ++-- .../src/Project/ProjectController.test.mjs | 1 + services/web/types/user.ts | 6 ++ 15 files changed, 128 insertions(+), 49 deletions(-) diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index b98cd8004b..bce4a5699a 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/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, diff --git a/services/web/app/src/Features/Subscription/SubscriptionLocator.mjs b/services/web/app/src/Features/Subscription/SubscriptionLocator.mjs index 3f4d8d3e88..1374906c1b 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionLocator.mjs +++ b/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 } diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 6d1c814ed2..4bc9c4aeb0 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", diff --git a/services/web/frontend/js/features/share-project-modal/components/copy-sharing-link-button.tsx b/services/web/frontend/js/features/share-project-modal/components/copy-sharing-link-button.tsx index ef5e06df10..972241544a 100644 --- a/services/web/frontend/js/features/share-project-modal/components/copy-sharing-link-button.tsx +++ b/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 = () => { diff --git a/services/web/frontend/js/features/share-project-modal/components/project-access.tsx b/services/web/frontend/js/features/share-project-modal/components/project-access.tsx index 5a22a93dfd..512b8f080a 100644 --- a/services/web/frontend/js/features/share-project-modal/components/project-access.tsx +++ b/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> @@ -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 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' && ( )} - {projectAccess === 'anyoneInXyzWithTheLink' && ( + {projectAccess.startsWith('anyoneInXyzWithTheLink') && ( )} {projectAccess === 'anyoneWithTheLink' && ( @@ -217,23 +235,33 @@ function ProjectAccess({ {t('only_invited_people')} - {sharingLinkData?.subscriptionId && ( - - } - trailingIcon={ - projectAccess === 'anyoneInXyzWithTheLink' - ? 'check' - : undefined - } - active={projectAccess === 'anyoneInXyzWithTheLink'} + {activeGroupSubscriptions && + activeGroupSubscriptions.map(subscription => ( + - {t('anyone_in_x_with_the_link', { groupName })} - - - )} + } + trailingIcon={ + projectAccess === + `anyoneInXyzWithTheLink.${subscription._id}` + ? 'check' + : undefined + } + active={ + projectAccess === + `anyoneInXyzWithTheLink.${subscription._id}` + } + > + {t('anyone_in_x_with_the_link', { + groupName: subscription.teamName || 'your group', + })} + + + ))} {!isInvitedPeopleScreen && + projectAccess && (projectAccess === 'onlyInvitedPeople' || - projectAccess === 'anyoneInXyzWithTheLink' || + projectAccess.startsWith('anyoneInXyzWithTheLink') || projectAccess === 'anyoneWithTheLink') && ( )} diff --git a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx index 6a93cea565..4ce9b78d46 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx +++ b/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') } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index b0c01b3604..f70db5b5d6 100644 --- a/services/web/locales/en.json +++ b/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", diff --git a/services/web/test/frontend/features/chat/components/chat.test.tsx b/services/web/test/frontend/features/chat/components/chat.test.tsx index bac3f4f856..69c1a17aed 100644 --- a/services/web/test/frontend/features/chat/components/chat.test.tsx +++ b/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('', function () { const user = { - id: 'fake_user', + id: 'fake_user' as UserId, first_name: 'fake_user_first_name', email: 'fake@example.com', } diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx index f558b2ebc9..55f80db900 100644 --- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx +++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx @@ -253,7 +253,7 @@ describe('', function () { renderWithEditorContext(, { ...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('', function () { renderWithEditorContext(, { ...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('', function () { subscriptionId: 'sub-123', }) - renderWithEditorContext( - , - createContextProps({ publicAccessLevel: 'private' }) - ) + renderWithEditorContext(, { + ...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('', function () { renderWithEditorContext(, { ...createContextProps(), user: { - id: 'non-project-owner', + id: 'non-project-owner' as UserId, email: 'non-project-owner@example.com', }, }) diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-autocomplete.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-autocomplete.spec.tsx index 62c063c2e8..abbc3a4b83 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-autocomplete.spec.tsx +++ b/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( diff --git a/services/web/test/frontend/helpers/editor-providers.tsx b/services/web/test/frontend/helpers/editor-providers.tsx index 7553bd7de7..ceaa0b0f3c 100644 --- a/services/web/test/frontend/helpers/editor-providers.tsx +++ b/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'] diff --git a/services/web/test/unit/src/Project/ProjectController.test.mjs b/services/web/test/unit/src/Project/ProjectController.test.mjs index 18a2286455..8de721ded5 100644 --- a/services/web/test/unit/src/Project/ProjectController.test.mjs +++ b/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 = { diff --git a/services/web/types/user.ts b/services/web/types/user.ts index ebb9f8eb17..fbeb968e2c 100644 --- a/services/web/types/user.ts +++ b/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 = {