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

[web] Pass project owner link sharing settings to frontend

GitOrigin-RevId: ff000f38ef36a3d448a2934d0c5963139852eee2
This commit is contained in:
MoxAmber
2026-06-12 08:06:12 +00:00
committed by Copybot
parent adf1659c59
commit 886985d91c
15 changed files with 128 additions and 49 deletions
@@ -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,
@@ -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 }
@@ -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": "",
@@ -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 = () => {
@@ -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"
@@ -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(
@@ -137,8 +137,9 @@ function ShareProjectModalContentInner({
{isSharingUpdatesEnabled ? (
<>
{!isInvitedPeopleScreen &&
projectAccess &&
(projectAccess === 'onlyInvitedPeople' ||
projectAccess === 'anyoneInXyzWithTheLink' ||
projectAccess.startsWith('anyoneInXyzWithTheLink') ||
projectAccess === 'anyoneWithTheLink') && (
<CopySharingLinkButton />
)}
@@ -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
View File
@@ -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",
@@ -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',
}
@@ -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',
},
})
@@ -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(
@@ -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']
@@ -71,6 +71,7 @@ describe('ProjectController', function () {
ctx.SubscriptionLocator = {
promises: {
getUsersSubscription: sinon.stub().resolves(),
getUserActiveGroupSubscriptions: sinon.stub().resolves(),
},
}
ctx.SubscriptionController = {
+6
View File
@@ -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 = {