Просмотр исходного кода

[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
roo hutton 1 год назад
Родитель
Сommit
2dcf87e3f6
14 измененных файлов с 280 добавлено и 61 удалено
  1. 13 0
      services/web/frontend/extracted-translations.json
  2. 1 1
      services/web/frontend/js/features/share-project-modal/components/edit-member.jsx
  3. 84 0
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/access-levels-changed.tsx
  4. 44 12
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/edit-member.tsx
  5. 2 2
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/owner-info.jsx
  6. 0 33
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites-notice.jsx
  7. 53 0
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites-notice.tsx
  8. 17 3
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites.jsx
  9. 23 0
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-modal-body.tsx
  10. 3 2
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx
  11. 2 2
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-member.jsx
  12. 1 6
      services/web/frontend/js/shared/context/editor-context.tsx
  13. 24 0
      services/web/frontend/stylesheets/app/editor/share.less
  14. 13 0
      services/web/locales/en.json

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

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

+ 1 - 1
services/web/frontend/js/features/share-project-modal/components/edit-member.jsx

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

+ 84 - 0
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/access-levels-changed.tsx

@@ -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>
+  )
+}

+ 44 - 12
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/edit-member.tsx

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

+ 2 - 2
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/owner-info.jsx

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

+ 0 - 33
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites-notice.jsx

@@ -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,
-}

+ 53 - 0
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites-notice.tsx

@@ -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>
+  }
+}

+ 17 - 3
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites.jsx

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

+ 23 - 0
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-modal-body.tsx

@@ -37,6 +37,26 @@ export default function ShareModalBody() {
     )
   }, [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(() => {
     if (!isProjectOwner || !features) {
       return false
@@ -58,6 +78,8 @@ export default function ShareModalBody() {
         <SendInvites
           canAddCollaborators={canAddCollaborators}
           hasExceededCollaboratorLimit={hasExceededCollaboratorLimit}
+          haveAnyEditorsBeenDowngraded={haveAnyEditorsBeenDowngraded}
+          somePendingEditorsResolved={somePendingEditorsResolved}
         />
       ) : (
         <SendInvitesNotice />
@@ -72,6 +94,7 @@ export default function ShareModalBody() {
             key={member._id}
             member={member}
             hasExceededCollaboratorLimit={hasExceededCollaboratorLimit}
+            hasBeenDowngraded={member.pendingEditor ?? false}
             canAddCollaborators={canAddCollaborators}
           />
         ) : (

+ 3 - 2
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx

@@ -68,7 +68,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
 
   // split test: link-sharing-warning
   // 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(() => {
     const hasExceededCollaboratorLimit = () => {
       if (!isProjectOwner || !project.features) {
@@ -80,7 +80,8 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
       }
       return (
         project.members.filter(member => member.privileges === 'readAndWrite')
-          .length > (project.features.collaborators ?? 1)
+          .length > (project.features.collaborators ?? 1) ||
+        project.members.some(member => member.pendingEditor)
       )
     }
 

+ 2 - 2
services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-member.jsx

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

+ 1 - 6
services/web/frontend/js/shared/context/editor-context.tsx

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

+ 24 - 0
services/web/frontend/stylesheets/app/editor/share.less

@@ -191,4 +191,28 @@
   .invite-warning {
     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;
+  }
 }

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

@@ -39,6 +39,7 @@
   "accepted_invite": "Accepted invite",
   "accepting_invite_as": "You are accepting this invite as",
   "access_denied": "Access Denied",
+  "access_levels_changed": "Access levels changed",
   "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_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_for_educational_purposes": "This license is for educational purposes (applies to students or faculty using __appName__ for teaching)",
   "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_plural": "Limited to __count__ editors per project",
+  "limited_to_n_editors_plural": "Limited to __count__ editors",
   "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.",
   "link": "Link",
@@ -1455,6 +1458,7 @@
   "plans_amper_pricing": "Plans & Pricing",
   "plans_and_pricing": "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_change_primary_to_remove": "Please change your primary email in order to remove",
   "please_check_your_inbox": "Please check your inbox",
@@ -1771,6 +1775,8 @@
   "select_a_project": "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_access_level": "Select access level",
+  "select_access_levels": "Select access levels",
   "select_all": "Select all",
   "select_all_projects": "Select all projects",
   "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_is_a_labs_experiment": "This is a Labs experiment",
   "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 owner’s 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_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 owner’s 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_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_group_subtext": "Monitor and download usage metrics for your group subscription",
   "view_more": "View more",
+  "view_only_downgraded": "View only. Upgrade to restore edit access.",
   "view_options": "View options",
   "view_pdf": "View PDF",
   "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_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_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 can’t add or change your password because your group or organization uses <0>single sign-on (SSO)</0>.",
   "you_cant_join_this_group_subscription": "You can’t join this group subscription",
   "you_cant_reset_password_due_to_sso": "You can’t 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_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_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_projects": "Your Projects",
   "your_questions_answered": "Your questions answered",
@@ -2450,6 +2462,7 @@
   "youre_on_free_trial_which_ends_on": "You’re on a free trial which ends on <0>__date__</0>.",
   "youre_signed_in_as_logout": "You’re signed in as <0>__email__</0>. <1>Log out.</1>",
   "youre_signed_up": "You’re signed up",
+  "youve_lost_edit_access": "You’ve lost edit access",
   "youve_unlinked_all_users": "You’ve unlinked all users",
   "zh-CN": "Chinese",
   "zip_contents_too_large": "Zip contents too large",