Merge pull request #24225 from overleaf/em-reviewers-share-modal

Count reviewers towards collaborator limit in share modal

GitOrigin-RevId: 27ec3a787124be7590791412d914ec6da78bab35
This commit is contained in:
Eric Mc Sween
2025-03-24 10:51:09 +00:00
committed by Copybot
parent f5c92cb627
commit e4dae982d2
7 changed files with 77 additions and 19 deletions
@@ -897,6 +897,10 @@
"limited_document_history": "", "limited_document_history": "",
"limited_offer": "", "limited_offer": "",
"limited_to_n_editors": "", "limited_to_n_editors": "",
"limited_to_n_editors_or_reviewers": "",
"limited_to_n_editors_or_reviewers_per_project": "",
"limited_to_n_editors_or_reviewers_per_project_plural": "",
"limited_to_n_editors_or_reviewers_plural": "",
"limited_to_n_editors_per_project": "", "limited_to_n_editors_per_project": "",
"limited_to_n_editors_per_project_plural": "", "limited_to_n_editors_per_project_plural": "",
"limited_to_n_editors_plural": "", "limited_to_n_editors_plural": "",
@@ -52,7 +52,7 @@ export default function AddCollaborators({ readOnly }) {
const { reset, selectedItems } = multipleSelectionProps const { reset, selectedItems } = multipleSelectionProps
useEffect(() => { useEffect(() => {
if (readOnly && privileges === 'readAndWrite') { if (readOnly && privileges !== 'readOnly') {
setPrivileges('readOnly') setPrivileges('readOnly')
} }
}, [privileges, readOnly]) }, [privileges, readOnly])
@@ -171,7 +171,9 @@ export default function AddCollaborators({ readOnly }) {
{t('can_edit')} {t('can_edit')}
</option> </option>
{getMeta('ol-isReviewerRoleEnabled') && ( {getMeta('ol-isReviewerRoleEnabled') && (
<option value="review">{t('can_review')}</option> <option disabled={readOnly} value="review">
{t('can_review')}
</option>
)} )}
<option value="readOnly">{t('can_view')}</option> <option value="readOnly">{t('can_view')}</option>
</OLFormSelect> </OLFormSelect>
@@ -16,6 +16,7 @@ import OLCol from '@/features/ui/components/ol/ol-col'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
import getMeta from '@/utils/meta' import getMeta from '@/utils/meta'
import { useUserContext } from '@/shared/context/user-context' import { useUserContext } from '@/shared/context/user-context'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded' type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded'
@@ -249,28 +250,41 @@ function SelectPrivilege({
} }
function getPrivilegeSubtitle(privilege: PermissionsOption) { function getPrivilegeSubtitle(privilege: PermissionsOption) {
if (!hasBeenDowngraded) { if (!['readAndWrite', 'review'].includes(privilege)) {
return !canAddCollaborators && return ''
privilege === 'readAndWrite' &&
value !== 'readAndWrite'
? t('limited_to_n_editors_per_project', {
count: features.collaborators,
})
: ''
} }
return privilege === 'readAndWrite' if (hasBeenDowngraded) {
? t('limited_to_n_editors', { if (isSplitTestEnabled('reviewer-role')) {
return t('limited_to_n_editors_or_reviewers', {
count: features.collaborators, count: features.collaborators,
}) })
: '' } else {
return t('limited_to_n_editors', { count: features.collaborators })
}
} else if (
!canAddCollaborators &&
!['readAndWrite', 'review'].includes(value)
) {
if (isSplitTestEnabled('reviewer-role')) {
return t('limited_to_n_editors_or_reviewers_per_project', {
count: features.collaborators,
})
} else {
return t('limited_to_n_editors_per_project', {
count: features.collaborators,
})
}
} else {
return ''
}
} }
function isPrivilegeDisabled(privilege: PermissionsOption) { function isPrivilegeDisabled(privilege: PermissionsOption) {
return ( return (
!canAddCollaborators && !canAddCollaborators &&
privilege === 'readAndWrite' && ['readAndWrite', 'review'].includes(privilege) &&
(hasBeenDowngraded || value !== 'readAndWrite') (hasBeenDowngraded || !['readAndWrite', 'review'].includes(value))
) )
} }
@@ -27,11 +27,11 @@ export default function ShareModalBody() {
} }
const editorInvites = invites.filter( const editorInvites = invites.filter(
invite => invite.privileges === 'readAndWrite' invite => invite.privileges !== 'readOnly'
).length ).length
return ( return (
members.filter(member => member.privileges === 'readAndWrite').length + members.filter(member => member.privileges !== 'readOnly').length +
editorInvites < editorInvites <
(features.collaborators ?? 1) (features.collaborators ?? 1)
) )
@@ -67,7 +67,7 @@ export default function ShareModalBody() {
} }
return ( return (
members.filter(member => member.privileges === 'readAndWrite').length > members.filter(member => member.privileges !== 'readOnly').length >
(features.collaborators ?? 1) (features.collaborators ?? 1)
) )
}, [features, isProjectOwner, members]) }, [features, isProjectOwner, members])
@@ -4,7 +4,7 @@ import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
export type ProjectContextMember = { export type ProjectContextMember = {
_id: UserId _id: UserId
privileges: 'readOnly' | 'readAndWrite' privileges: 'readOnly' | 'readAndWrite' | 'review'
email: string email: string
first_name: string first_name: string
last_name: string last_name: string
+4
View File
@@ -1180,6 +1180,10 @@
"license": "License", "license": "License",
"limited_document_history": "Limited document history", "limited_document_history": "Limited document history",
"limited_to_n_editors": "Limited to __count__ editor", "limited_to_n_editors": "Limited to __count__ editor",
"limited_to_n_editors_or_reviewers": "Limited to __count__ editor or reviewer",
"limited_to_n_editors_or_reviewers_per_project": "Limited to __count__ editor or reviewer per project",
"limited_to_n_editors_or_reviewers_per_project_plural": "Limited to __count__ editors or reviewers per project",
"limited_to_n_editors_or_reviewers_plural": "Limited to __count__ editors or reviewers",
"limited_to_n_editors_per_project": "Limited to __count__ editor per project", "limited_to_n_editors_per_project": "Limited to __count__ editor per project",
"limited_to_n_editors_per_project_plural": "Limited to __count__ editors per project", "limited_to_n_editors_per_project_plural": "Limited to __count__ editors per project",
"limited_to_n_editors_plural": "Limited to __count__ editors", "limited_to_n_editors_plural": "Limited to __count__ editors",
@@ -101,6 +101,7 @@ describe('<ShareProjectModal/>', function () {
fetchMock.get('/user/contacts', { contacts }) fetchMock.get('/user/contacts', { contacts })
window.metaAttributesCache.set('ol-user', { allowedFreeTrial: true }) window.metaAttributesCache.set('ol-user', { allowedFreeTrial: true })
window.metaAttributesCache.set('ol-showUpgradePrompt', true) window.metaAttributesCache.set('ol-showUpgradePrompt', true)
window.metaAttributesCache.set('ol-isReviewerRoleEnabled', true)
}) })
afterEach(function () { afterEach(function () {
@@ -691,6 +692,39 @@ describe('<ShareProjectModal/>', function () {
await screen.findByText('Add more editors') await screen.findByText('Add more editors')
expect(screen.getByRole('option', { name: 'Can edit' }).disabled).to.be.true expect(screen.getByRole('option', { name: 'Can edit' }).disabled).to.be.true
expect(screen.getByRole('option', { name: 'Can review' }).disabled).to.be
.true
expect(screen.getByRole('option', { name: 'Can view' }).disabled).to.be
.false
screen.getByText(
/Upgrade to add more editors and access collaboration features like track changes and full project history/
)
})
it('counts reviewers towards the collaborator limit', async function () {
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
features: {
collaborators: 1,
},
members: [
{
_id: 'reviewer-id',
email: 'reviewer@example.com',
privileges: 'review',
},
],
},
},
})
await screen.findByText('Add more editors')
expect(screen.getByRole('option', { name: 'Can edit' }).disabled).to.be.true
expect(screen.getByRole('option', { name: 'Can review' }).disabled).to.be
.true
expect(screen.getByRole('option', { name: 'Can view' }).disabled).to.be expect(screen.getByRole('option', { name: 'Can view' }).disabled).to.be
.false .false