[web] Share modal shows downgraded editors (#20015)

* add hasBeenDowngraded prop for EditMember

* reduce padding on share modal collab row, add prompt to hasBeenDowngraded Select

* share modal select styling tweaks to allow for inline warning icon

* always show editor limit subtitle when in downgraded state

* add AccessLevelsChanged warning, tweak owner row styling

* conditionally set hasBeenDowngraded prop. make invited member row styling more consistent between warning/enforcement

* add an info state for access level changed notification

* add notification for lost edit access on collaborator share modal, TSify SendInvitesNotice

* fix member privilege alignment in collaborator share modal

* show blue upgrade CTA when some pending editors have been resolved

* automatically show share modal to owners when has pending editors or is over collab limit

* only show lost edit access warning in read-only share modal to pending editors

---------

Co-authored-by: Thomas <thomas-@users.noreply.github.com>
GitOrigin-RevId: e3b88052a48b8f598299ffc55b7c24cb793da151
This commit is contained in:
roo hutton 2024-08-26 14:03:37 +01:00 committed by Copybot
parent 3e06c0086e
commit 2dcf87e3f6
14 changed files with 280 additions and 61 deletions

View file

@ -29,6 +29,7 @@
"accepted_invite": "", "accepted_invite": "",
"accepting_invite_as": "", "accepting_invite_as": "",
"access_denied": "", "access_denied": "",
"access_levels_changed": "",
"account_has_been_link_to_institution_account": "", "account_has_been_link_to_institution_account": "",
"account_has_past_due_invoice_change_plan_warning": "", "account_has_past_due_invoice_change_plan_warning": "",
"account_managed_by_group_administrator": "", "account_managed_by_group_administrator": "",
@ -745,8 +746,10 @@
"library": "", "library": "",
"license_for_educational_purposes": "", "license_for_educational_purposes": "",
"limited_offer": "", "limited_offer": "",
"limited_to_n_editors": "",
"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": "",
"line": "", "line": "",
"line_height": "", "line_height": "",
"line_width_is_the_width_of_the_line_in_the_current_environment": "", "line_width_is_the_width_of_the_line_in_the_current_environment": "",
@ -985,6 +988,7 @@
"percent_is_the_percentage_of_the_line_width": "", "percent_is_the_percentage_of_the_line_width": "",
"plan": "", "plan": "",
"plan_tooltip": "", "plan_tooltip": "",
"please_ask_the_project_owner_to_upgrade_more_editors": "",
"please_ask_the_project_owner_to_upgrade_to_track_changes": "", "please_ask_the_project_owner_to_upgrade_to_track_changes": "",
"please_change_primary_to_remove": "", "please_change_primary_to_remove": "",
"please_check_your_inbox": "", "please_check_your_inbox": "",
@ -1228,6 +1232,8 @@
"select_a_project": "", "select_a_project": "",
"select_a_project_figure_modal": "", "select_a_project_figure_modal": "",
"select_a_row_or_a_column_to_delete": "", "select_a_row_or_a_column_to_delete": "",
"select_access_level": "",
"select_access_levels": "",
"select_all": "", "select_all": "",
"select_all_projects": "", "select_all_projects": "",
"select_an_output_file": "", "select_an_output_file": "",
@ -1451,7 +1457,9 @@
"this_field_is_required": "", "this_field_is_required": "",
"this_grants_access_to_features_2": "", "this_grants_access_to_features_2": "",
"this_is_a_labs_experiment": "", "this_is_a_labs_experiment": "",
"this_project_already_has_maximum_editors": "",
"this_project_exceeded_compile_timeout_limit_on_free_plan": "", "this_project_exceeded_compile_timeout_limit_on_free_plan": "",
"this_project_exceeded_editor_limit": "",
"this_project_has_more_than_max_collabs": "", "this_project_has_more_than_max_collabs": "",
"this_project_is_public": "", "this_project_is_public": "",
"this_project_is_public_read_only": "", "this_project_is_public_read_only": "",
@ -1652,6 +1660,7 @@
"view_metrics_commons_subtext": "", "view_metrics_commons_subtext": "",
"view_metrics_group_subtext": "", "view_metrics_group_subtext": "",
"view_more": "", "view_more": "",
"view_only_downgraded": "",
"view_options": "", "view_options": "",
"view_pdf": "", "view_pdf": "",
"view_your_invoices": "", "view_your_invoices": "",
@ -1722,6 +1731,8 @@
"you_can_only_add_n_people_to_edit_a_project": "", "you_can_only_add_n_people_to_edit_a_project": "",
"you_can_only_add_n_people_to_edit_a_project_plural": "", "you_can_only_add_n_people_to_edit_a_project_plural": "",
"you_can_request_a_maximum_of_limit_fixes_per_day": "", "you_can_request_a_maximum_of_limit_fixes_per_day": "",
"you_can_select_or_invite": "",
"you_can_select_or_invite_plural": "",
"you_cant_add_or_change_password_due_to_sso": "", "you_cant_add_or_change_password_due_to_sso": "",
"you_cant_join_this_group_subscription": "", "you_cant_join_this_group_subscription": "",
"you_dont_have_any_repositories": "", "you_dont_have_any_repositories": "",
@ -1756,6 +1767,7 @@
"your_plan_is_limited_to_n_editors": "", "your_plan_is_limited_to_n_editors": "",
"your_plan_is_limited_to_n_editors_plural": "", "your_plan_is_limited_to_n_editors_plural": "",
"your_project_exceeded_compile_timeout_limit_on_free_plan": "", "your_project_exceeded_compile_timeout_limit_on_free_plan": "",
"your_project_exceeded_editor_limit": "",
"your_project_near_compile_timeout_limit": "", "your_project_near_compile_timeout_limit": "",
"your_projects": "", "your_projects": "",
"your_role": "", "your_role": "",
@ -1769,6 +1781,7 @@
"youre_joining": "", "youre_joining": "",
"youre_on_free_trial_which_ends_on": "", "youre_on_free_trial_which_ends_on": "",
"youre_signed_in_as_logout": "", "youre_signed_in_as_logout": "",
"youve_lost_edit_access": "",
"youve_unlinked_all_users": "", "youve_unlinked_all_users": "",
"zoom_in": "", "zoom_in": "",
"zoom_out": "", "zoom_out": "",

View file

@ -54,7 +54,7 @@ export default function EditMember({ member }) {
return ( return (
<Form horizontal id="share-project-form" onSubmit={handleSubmit}> <Form horizontal id="share-project-form" onSubmit={handleSubmit}>
<FormGroup className="project-member"> <FormGroup className="project-member row">
<Col xs={7}> <Col xs={7}>
<FormControl.Static>{member.email}</FormControl.Static> <FormControl.Static>{member.email}</FormControl.Static>
</Col> </Col>

View file

@ -0,0 +1,84 @@
import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Notification from '@/shared/components/notification'
import { upgradePlan } from '../../../../main/account-upgrade'
import { useProjectContext } from '@/shared/context/project-context'
import { useUserContext } from '@/shared/context/user-context'
import { sendMB } from '@/infrastructure/event-tracking'
import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
type AccessLevelsChangedProps = {
somePendingEditorsResolved: boolean
}
export default function AccessLevelsChanged({
somePendingEditorsResolved,
}: AccessLevelsChangedProps) {
const { t } = useTranslation()
const { features } = useProjectContext()
const user = useUserContext()
return (
<div className="add-collaborators-upgrade">
<Notification
isActionBelowContent
type={somePendingEditorsResolved ? 'info' : 'warning'}
title={
somePendingEditorsResolved
? t('select_access_levels')
: t('access_levels_changed')
}
content={
somePendingEditorsResolved ? (
<p>{t('your_project_exceeded_editor_limit')}</p>
) : (
<p>
{t('this_project_exceeded_editor_limit')}{' '}
{t('you_can_select_or_invite', {
count: features.collaborators,
})}
</p>
)
}
action={
<div className="upgrade-actions">
{user.allowedFreeTrial ? (
<StartFreeTrialButton
buttonProps={{ variant: 'secondary', size: 'small' }}
source="project-sharing"
variant="exceeds"
>
{t('upgrade')}
</StartFreeTrialButton>
) : (
<Button
bsSize="sm"
className="btn-secondary"
onClick={() => {
upgradePlan('project-sharing')
}}
>
{t('upgrade')}
</Button>
)}
<Button
href="https://www.overleaf.com/blog/changes-to-project-sharing"
bsSize="sm"
className="btn-link"
target="_blank"
rel="noreferrer"
onClick={() => {
sendMB('paywall-info-click', {
'paywall-type': 'project-sharing',
content: 'blog',
variant: 'exceeds',
})
}}
>
{t('read_more')}
</Button>
</div>
}
/>
</div>
)
}

View file

@ -13,11 +13,12 @@ import type { ProjectContextMember } from '@/shared/context/types/project-contex
import { PermissionsLevel } from '@/features/ide-react/types/permissions' import { PermissionsLevel } from '@/features/ide-react/types/permissions'
import { linkSharingEnforcementDate } from '../../utils/link-sharing' import { linkSharingEnforcementDate } from '../../utils/link-sharing'
type PermissionsOption = PermissionsLevel | 'removeAccess' type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded'
type EditMemberProps = { type EditMemberProps = {
member: ProjectContextMember member: ProjectContextMember
hasExceededCollaboratorLimit: boolean hasExceededCollaboratorLimit: boolean
hasBeenDowngraded: boolean
canAddCollaborators: boolean canAddCollaborators: boolean
} }
@ -29,6 +30,7 @@ type Privilege = {
export default function EditMember({ export default function EditMember({
member, member,
hasExceededCollaboratorLimit, hasExceededCollaboratorLimit,
hasBeenDowngraded,
canAddCollaborators, canAddCollaborators,
}: EditMemberProps) { }: EditMemberProps) {
const [privileges, setPrivileges] = useState<PermissionsOption>( const [privileges, setPrivileges] = useState<PermissionsOption>(
@ -133,7 +135,7 @@ export default function EditMember({
<div className="email-warning"> <div className="email-warning">
{member.email} {member.email}
{member.pendingEditor && ( {member.pendingEditor && (
<div className="subtitle">Pending editor</div> <div className="subtitle">{t('view_only_downgraded')}</div>
)} )}
{shouldWarnMember() && ( {shouldWarnMember() && (
<div className="subtitle"> <div className="subtitle">
@ -146,7 +148,7 @@ export default function EditMember({
</div> </div>
</Col> </Col>
<Col xs={2}> <Col xs={1}>
{privileges !== member.privileges && privilegeChangePending && ( {privileges !== member.privileges && privilegeChangePending && (
<ChangePrivilegesActions <ChangePrivilegesActions
handleReset={() => setPrivileges(member.privileges)} handleReset={() => setPrivileges(member.privileges)}
@ -154,7 +156,9 @@ export default function EditMember({
)} )}
</Col> </Col>
<Col xs={3}> <Col xs={4} className="project-member-select">
{hasBeenDowngraded && <Icon type="warning" fw />}
<SelectPrivilege <SelectPrivilege
value={privileges} value={privileges}
handleChange={value => { handleChange={value => {
@ -162,6 +166,7 @@ export default function EditMember({
handlePrivilegeChange(value.key) handlePrivilegeChange(value.key)
} }
}} }}
hasBeenDowngraded={hasBeenDowngraded}
canAddCollaborators={canAddCollaborators} canAddCollaborators={canAddCollaborators}
/> />
</Col> </Col>
@ -182,12 +187,14 @@ EditMember.propTypes = {
type SelectPrivilegeProps = { type SelectPrivilegeProps = {
value: string value: string
handleChange: (item: Privilege | null | undefined) => void handleChange: (item: Privilege | null | undefined) => void
hasBeenDowngraded: boolean
canAddCollaborators: boolean canAddCollaborators: boolean
} }
function SelectPrivilege({ function SelectPrivilege({
value, value,
handleChange, handleChange,
hasBeenDowngraded,
canAddCollaborators, canAddCollaborators,
}: SelectPrivilegeProps) { }: SelectPrivilegeProps) {
const { t } = useTranslation() const { t } = useTranslation()
@ -203,25 +210,50 @@ function SelectPrivilege({
[t] [t]
) )
const downgradedPseudoPrivilege: Privilege = {
key: 'downgraded',
label: t('select_access_level'),
}
function getPrivilegeSubtitle(privilege: PermissionsOption) { function getPrivilegeSubtitle(privilege: PermissionsOption) {
return !canAddCollaborators && if (!hasBeenDowngraded) {
privilege === 'readAndWrite' && return !canAddCollaborators &&
value !== 'readAndWrite' privilege === 'readAndWrite' &&
? t('limited_to_n_editors_per_project', { count: features.collaborators }) value !== 'readAndWrite'
? t('limited_to_n_editors_per_project', {
count: features.collaborators,
})
: ''
}
return privilege === 'readAndWrite'
? t('limited_to_n_editors', {
count: features.collaborators,
})
: '' : ''
} }
function isPrivilegeDisabled(privilege: PermissionsOption) {
return (
!canAddCollaborators &&
privilege === 'readAndWrite' &&
(hasBeenDowngraded || value !== 'readAndWrite')
)
}
return ( return (
<Select <Select
items={privileges} items={privileges}
itemToKey={item => item.key} itemToKey={item => item.key}
itemToString={item => (item ? item.label : '')} itemToString={item => (item ? item.label : '')}
itemToSubtitle={item => (item ? getPrivilegeSubtitle(item.key) : '')} itemToSubtitle={item => (item ? getPrivilegeSubtitle(item.key) : '')}
itemToDisabled={item => itemToDisabled={item => (item ? isPrivilegeDisabled(item.key) : false)}
item ? getPrivilegeSubtitle(item.key) !== '' : false
}
defaultItem={privileges.find(item => item.key === value)} defaultItem={privileges.find(item => item.key === value)}
selected={privileges.find(item => item.key === value)} selected={
hasBeenDowngraded
? downgradedPseudoPrivilege
: privileges.find(item => item.key === value)
}
name="privileges" name="privileges"
onSelectedItemChanged={handleChange} onSelectedItemChanged={handleChange}
selectedIcon selectedIcon

View file

@ -9,13 +9,13 @@ export default function OwnerInfo() {
return ( return (
<Row className="project-member"> <Row className="project-member">
<Col xs={9}> <Col xs={8}>
<div className="project-member-email-icon"> <div className="project-member-email-icon">
<Icon type="user" fw /> <Icon type="user" fw />
<div className="email-warning">{owner?.email}</div> <div className="email-warning">{owner?.email}</div>
</div> </div>
</Col> </Col>
<Col xs={3} className="text-left"> <Col xs={4} className="text-right">
{t('owner')} {t('owner')}
</Col> </Col>
</Row> </Row>

View file

@ -1,33 +0,0 @@
import { Col, Row } from 'react-bootstrap'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import { useProjectContext } from '@/shared/context/project-context'
export default function SendInvitesNotice() {
const { publicAccessLevel } = useProjectContext()
return (
<Row className="public-access-level public-access-level--notice">
<Col xs={12} className="text-center">
<AccessLevel level={publicAccessLevel} />
</Col>
</Row>
)
}
function AccessLevel({ level }) {
const { t } = useTranslation()
switch (level) {
case 'private':
return t('to_add_more_collaborators')
case 'tokenBased':
return t('to_change_access_permissions')
default:
return null
}
}
AccessLevel.propTypes = {
level: PropTypes.string,
}

View file

@ -0,0 +1,53 @@
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { useProjectContext } from '@/shared/context/project-context'
import Notification from '@/shared/components/notification'
import { PublicAccessLevel } from '../../../../../../types/public-access-level'
import { useEditorContext } from '@/shared/context/editor-context'
export default function SendInvitesNotice() {
const { publicAccessLevel } = useProjectContext()
const { isPendingEditor } = useEditorContext()
const { t } = useTranslation()
return (
<div>
{isPendingEditor && (
<Notification
isActionBelowContent
type="info"
title={t('youve_lost_edit_access')}
content={
<div>
<p>{t('this_project_already_has_maximum_editors')}</p>
<p>{t('please_ask_the_project_owner_to_upgrade_more_editors')}</p>
</div>
}
/>
)}
<Row className="public-access-level public-access-level--notice">
<Col xs={12} className="text-center">
<AccessLevel level={publicAccessLevel} />
</Col>
</Row>
</div>
)
}
type AccessLevelProps = {
level: PublicAccessLevel | undefined
}
function AccessLevel({ level }: AccessLevelProps) {
const { t } = useTranslation()
switch (level) {
case 'private':
return <span>{t('to_add_more_collaborators')}</span>
case 'tokenBased':
return <span>{t('to_change_access_permissions')}</span>
default:
return <span>''</span>
}
}

View file

@ -2,18 +2,30 @@ import { Row } from 'react-bootstrap'
import AddCollaborators from './add-collaborators' import AddCollaborators from './add-collaborators'
import AddCollaboratorsUpgrade from './add-collaborators-upgrade' import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
import CollaboratorsLimitUpgrade from './collaborators-limit-upgrade' import CollaboratorsLimitUpgrade from './collaborators-limit-upgrade'
import AccessLevelsChanged from './access-levels-changed'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
export default function SendInvites({ export default function SendInvites({
canAddCollaborators, canAddCollaborators,
hasExceededCollaboratorLimit, hasExceededCollaboratorLimit,
haveAnyEditorsBeenDowngraded,
somePendingEditorsResolved,
}) { }) {
return ( return (
<Row className="invite-controls"> <Row className="invite-controls">
{hasExceededCollaboratorLimit && <AddCollaboratorsUpgrade />} {hasExceededCollaboratorLimit && !haveAnyEditorsBeenDowngraded && (
{!canAddCollaborators && !hasExceededCollaboratorLimit && ( <AddCollaboratorsUpgrade />
<CollaboratorsLimitUpgrade />
)} )}
{haveAnyEditorsBeenDowngraded && (
<AccessLevelsChanged
somePendingEditorsResolved={somePendingEditorsResolved}
/>
)}
{!canAddCollaborators &&
!hasExceededCollaboratorLimit &&
!haveAnyEditorsBeenDowngraded && <CollaboratorsLimitUpgrade />}
<AddCollaborators readOnly={!canAddCollaborators} /> <AddCollaborators readOnly={!canAddCollaborators} />
</Row> </Row>
) )
@ -22,4 +34,6 @@ export default function SendInvites({
SendInvites.propTypes = { SendInvites.propTypes = {
canAddCollaborators: PropTypes.bool, canAddCollaborators: PropTypes.bool,
hasExceededCollaboratorLimit: PropTypes.bool, hasExceededCollaboratorLimit: PropTypes.bool,
haveAnyEditorsBeenDowngraded: PropTypes.bool,
somePendingEditorsResolved: PropTypes.bool,
} }

View file

@ -37,6 +37,26 @@ export default function ShareModalBody() {
) )
}, [members, invites, features, isProjectOwner]) }, [members, invites, features, isProjectOwner])
// determine if some but not all pending editors' permissions have been resolved,
// for moving between warning and info notification states etc.
const somePendingEditorsResolved = useMemo(() => {
return (
members.some(member => member.privileges === 'readAndWrite') &&
members.some(member => member.pendingEditor)
)
}, [members])
const haveAnyEditorsBeenDowngraded = useMemo(() => {
if (!isProjectOwner || !features) {
return false
}
if (features.collaborators === -1) {
return false
}
return members.some(member => member.pendingEditor)
}, [features, isProjectOwner, members])
const hasExceededCollaboratorLimit = useMemo(() => { const hasExceededCollaboratorLimit = useMemo(() => {
if (!isProjectOwner || !features) { if (!isProjectOwner || !features) {
return false return false
@ -58,6 +78,8 @@ export default function ShareModalBody() {
<SendInvites <SendInvites
canAddCollaborators={canAddCollaborators} canAddCollaborators={canAddCollaborators}
hasExceededCollaboratorLimit={hasExceededCollaboratorLimit} hasExceededCollaboratorLimit={hasExceededCollaboratorLimit}
haveAnyEditorsBeenDowngraded={haveAnyEditorsBeenDowngraded}
somePendingEditorsResolved={somePendingEditorsResolved}
/> />
) : ( ) : (
<SendInvitesNotice /> <SendInvitesNotice />
@ -72,6 +94,7 @@ export default function ShareModalBody() {
key={member._id} key={member._id}
member={member} member={member}
hasExceededCollaboratorLimit={hasExceededCollaboratorLimit} hasExceededCollaboratorLimit={hasExceededCollaboratorLimit}
hasBeenDowngraded={member.pendingEditor ?? false}
canAddCollaborators={canAddCollaborators} canAddCollaborators={canAddCollaborators}
/> />
) : ( ) : (

View file

@ -68,7 +68,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
// split test: link-sharing-warning // split test: link-sharing-warning
// show the new share modal if project owner // show the new share modal if project owner
// is over collaborator limit (once every 24 hours) // is over collaborator limit or has pending editors (once every 24 hours)
useEffect(() => { useEffect(() => {
const hasExceededCollaboratorLimit = () => { const hasExceededCollaboratorLimit = () => {
if (!isProjectOwner || !project.features) { if (!isProjectOwner || !project.features) {
@ -80,7 +80,8 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
} }
return ( return (
project.members.filter(member => member.privileges === 'readAndWrite') project.members.filter(member => member.privileges === 'readAndWrite')
.length > (project.features.collaborators ?? 1) .length > (project.features.collaborators ?? 1) ||
project.members.some(member => member.pendingEditor)
) )
} }

View file

@ -6,13 +6,13 @@ import Icon from '@/shared/components/icon'
export default function ViewMember({ member }) { export default function ViewMember({ member }) {
return ( return (
<Row className="project-member"> <Row className="project-member">
<Col xs={9}> <Col xs={8}>
<div className="project-member-email-icon"> <div className="project-member-email-icon">
<Icon type="user" fw /> <Icon type="user" fw />
<div className="email-warning">{member.email}</div> <div className="email-warning">{member.email}</div>
</div> </div>
</Col> </Col>
<Col xs={3} className="text-left"> <Col xs={4} className="text-right">
<MemberPrivileges privileges={member.privileges} /> <MemberPrivileges privileges={member.privileges} />
</Col> </Col>
</Row> </Row>

View file

@ -101,12 +101,7 @@ export const EditorProvider: FC = ({ children }) => {
const isPendingEditor = useMemo( const isPendingEditor = useMemo(
() => () =>
members?.some( members?.some(member => member._id === userId && member.pendingEditor),
member =>
member._id === userId &&
member.pendingEditor &&
member.privileges === 'readAndWrite'
),
[members, userId] [members, userId]
) )

View file

@ -191,4 +191,28 @@
.invite-warning { .invite-warning {
margin-bottom: @line-height-computed / 2; margin-bottom: @line-height-computed / 2;
} }
.project-member-select {
padding: 0;
.fa-warning {
color: @brand-warning;
}
}
.project-member.form-group,
.project-member.row {
margin: 0 -30px;
}
.project-member .select-wrapper {
display: inline-block;
position: absolute;
right: 0;
.select-trigger {
gap: @spacing-02;
}
}
.project-member.row {
padding-right: 1.28571429em;
}
} }

View file

@ -39,6 +39,7 @@
"accepted_invite": "Accepted invite", "accepted_invite": "Accepted invite",
"accepting_invite_as": "You are accepting this invite as", "accepting_invite_as": "You are accepting this invite as",
"access_denied": "Access Denied", "access_denied": "Access Denied",
"access_levels_changed": "Access levels changed",
"account": "Account", "account": "Account",
"account_has_been_link_to_institution_account": "Your __appName__ account on <b>__email__</b> has been linked to your <b>__institutionName__</b> institutional account.", "account_has_been_link_to_institution_account": "Your __appName__ account on <b>__email__</b> has been linked to your <b>__institutionName__</b> institutional account.",
"account_has_past_due_invoice_change_plan_warning": "Your account currently has a past due invoice. You will not be able to change your plan until this is resolved.", "account_has_past_due_invoice_change_plan_warning": "Your account currently has a past due invoice. You will not be able to change your plan until this is resolved.",
@ -1085,8 +1086,10 @@
"license": "License", "license": "License",
"license_for_educational_purposes": "This license is for educational purposes (applies to students or faculty using __appName__ for teaching)", "license_for_educational_purposes": "This license is for educational purposes (applies to students or faculty using __appName__ for teaching)",
"limited_offer": "Limited offer", "limited_offer": "Limited offer",
"limited_to_n_editors": "Limited to __count__ editor",
"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",
"line_height": "Line Height", "line_height": "Line Height",
"line_width_is_the_width_of_the_line_in_the_current_environment": "Line width is the width of the line in the current environment. e.g. a full page width in single-column layout or half a page width in a two-column layout.", "line_width_is_the_width_of_the_line_in_the_current_environment": "Line width is the width of the line in the current environment. e.g. a full page width in single-column layout or half a page width in a two-column layout.",
"link": "Link", "link": "Link",
@ -1455,6 +1458,7 @@
"plans_amper_pricing": "Plans & Pricing", "plans_amper_pricing": "Plans & Pricing",
"plans_and_pricing": "Plans and Pricing", "plans_and_pricing": "Plans and Pricing",
"plans_and_pricing_lowercase": "plans and pricing", "plans_and_pricing_lowercase": "plans and pricing",
"please_ask_the_project_owner_to_upgrade_more_editors": "Please ask the project owner to upgrade their plan to allow more editors.",
"please_ask_the_project_owner_to_upgrade_to_track_changes": "Please ask the project owner to upgrade to use track changes", "please_ask_the_project_owner_to_upgrade_to_track_changes": "Please ask the project owner to upgrade to use track changes",
"please_change_primary_to_remove": "Please change your primary email in order to remove", "please_change_primary_to_remove": "Please change your primary email in order to remove",
"please_check_your_inbox": "Please check your inbox", "please_check_your_inbox": "Please check your inbox",
@ -1771,6 +1775,8 @@
"select_a_project": "Select a Project", "select_a_project": "Select a Project",
"select_a_project_figure_modal": "Select a project", "select_a_project_figure_modal": "Select a project",
"select_a_row_or_a_column_to_delete": "Select a row or a column to delete", "select_a_row_or_a_column_to_delete": "Select a row or a column to delete",
"select_access_level": "Select access level",
"select_access_levels": "Select access levels",
"select_all": "Select all", "select_all": "Select all",
"select_all_projects": "Select all projects", "select_all_projects": "Select all projects",
"select_an_output_file": "Select an Output File", "select_an_output_file": "Select an Output File",
@ -2064,7 +2070,9 @@
"this_grants_access_to_features_2": "This grants you access to <0>__appName__</0> <0>__featureType__</0> features.", "this_grants_access_to_features_2": "This grants you access to <0>__appName__</0> <0>__featureType__</0> features.",
"this_is_a_labs_experiment": "This is a Labs experiment", "this_is_a_labs_experiment": "This is a Labs experiment",
"this_is_your_template": "This is your template from your project", "this_is_your_template": "This is your template from your project",
"this_project_already_has_maximum_editors": "This project already has the maximum number of editors permitted on the owners plan. This means you can view but not edit the project.",
"this_project_exceeded_compile_timeout_limit_on_free_plan": "This project exceeded the compile timeout limit on our free plan.", "this_project_exceeded_compile_timeout_limit_on_free_plan": "This project exceeded the compile timeout limit on our free plan.",
"this_project_exceeded_editor_limit": "This project exceeded the editor limit for your plan. All collaborators now have view-only access.",
"this_project_has_more_than_max_collabs": "This project has more than the maximum number of collaborators allowed on the project owners Overleaf plan. This means you could lose edit access from __linkSharingDate__.", "this_project_has_more_than_max_collabs": "This project has more than the maximum number of collaborators allowed on the project owners Overleaf plan. This means you could lose edit access from __linkSharingDate__.",
"this_project_is_public": "This project is public and can be edited by anyone with the URL.", "this_project_is_public": "This project is public and can be edited by anyone with the URL.",
"this_project_is_public_read_only": "This project is public and can be viewed but not edited by anyone with the URL", "this_project_is_public_read_only": "This project is public and can be viewed but not edited by anyone with the URL",
@ -2300,6 +2308,7 @@
"view_metrics_commons_subtext": "Monitor and download usage metrics for your Commons subscription", "view_metrics_commons_subtext": "Monitor and download usage metrics for your Commons subscription",
"view_metrics_group_subtext": "Monitor and download usage metrics for your group subscription", "view_metrics_group_subtext": "Monitor and download usage metrics for your group subscription",
"view_more": "View more", "view_more": "View more",
"view_only_downgraded": "View only. Upgrade to restore edit access.",
"view_options": "View options", "view_options": "View options",
"view_pdf": "View PDF", "view_pdf": "View PDF",
"view_source": "View Source", "view_source": "View Source",
@ -2391,6 +2400,8 @@
"you_can_only_add_n_people_to_edit_a_project_plural": "You can only add __count__ people to edit a project with you on your current plan. Upgrade to add more.", "you_can_only_add_n_people_to_edit_a_project_plural": "You can only add __count__ people to edit a project with you on your current plan. Upgrade to add more.",
"you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page": "You can <0>opt in and out</0> of the program at any time on this page", "you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page": "You can <0>opt in and out</0> of the program at any time on this page",
"you_can_request_a_maximum_of_limit_fixes_per_day": "You can request a maximum of __limit__ fixes per day. Please try again tomorrow.", "you_can_request_a_maximum_of_limit_fixes_per_day": "You can request a maximum of __limit__ fixes per day. Please try again tomorrow.",
"you_can_select_or_invite": "You can select or invite __count__ editor on your current plan, or upgrade to get more.",
"you_can_select_or_invite_plural": "You can select or invite __count__ editors on your current plan, or upgrade to get more.",
"you_cant_add_or_change_password_due_to_sso": "You cant add or change your password because your group or organization uses <0>single sign-on (SSO)</0>.", "you_cant_add_or_change_password_due_to_sso": "You cant add or change your password because your group or organization uses <0>single sign-on (SSO)</0>.",
"you_cant_join_this_group_subscription": "You cant join this group subscription", "you_cant_join_this_group_subscription": "You cant join this group subscription",
"you_cant_reset_password_due_to_sso": "You cant reset your password because your group or organization uses SSO. <0>Log in with SSO</0>.", "you_cant_reset_password_due_to_sso": "You cant reset your password because your group or organization uses SSO. <0>Log in with SSO</0>.",
@ -2434,6 +2445,7 @@
"your_plan_is_limited_to_n_editors": "Your plan allows __count__ collaborator with edit access and unlimited viewers.", "your_plan_is_limited_to_n_editors": "Your plan allows __count__ collaborator with edit access and unlimited viewers.",
"your_plan_is_limited_to_n_editors_plural": "Your plan allows __count__ collaborators with edit access and unlimited viewers.", "your_plan_is_limited_to_n_editors_plural": "Your plan allows __count__ collaborators with edit access and unlimited viewers.",
"your_project_exceeded_compile_timeout_limit_on_free_plan": "Your project exceeded the compile timeout limit on our free plan.", "your_project_exceeded_compile_timeout_limit_on_free_plan": "Your project exceeded the compile timeout limit on our free plan.",
"your_project_exceeded_editor_limit": "Your project exceeded the editor limit and access levels were changed. Select a new access level for your collaborators, or upgrade to add more editors.",
"your_project_near_compile_timeout_limit": "Your project is near the compile timeout limit for our free plan.", "your_project_near_compile_timeout_limit": "Your project is near the compile timeout limit for our free plan.",
"your_projects": "Your Projects", "your_projects": "Your Projects",
"your_questions_answered": "Your questions answered", "your_questions_answered": "Your questions answered",
@ -2450,6 +2462,7 @@
"youre_on_free_trial_which_ends_on": "Youre on a free trial which ends on <0>__date__</0>.", "youre_on_free_trial_which_ends_on": "Youre on a free trial which ends on <0>__date__</0>.",
"youre_signed_in_as_logout": "Youre signed in as <0>__email__</0>. <1>Log out.</1>", "youre_signed_in_as_logout": "Youre signed in as <0>__email__</0>. <1>Log out.</1>",
"youre_signed_up": "Youre signed up", "youre_signed_up": "Youre signed up",
"youve_lost_edit_access": "Youve lost edit access",
"youve_unlinked_all_users": "Youve unlinked all users", "youve_unlinked_all_users": "Youve unlinked all users",
"zh-CN": "Chinese", "zh-CN": "Chinese",
"zip_contents_too_large": "Zip contents too large", "zip_contents_too_large": "Zip contents too large",