diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 94d4aaae98..9f04b5a38d 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -72,7 +72,6 @@
"add_error_assist_annual_to_your_projects": "",
"add_error_assist_to_your_projects": "",
"add_files": "",
- "add_more_collaborators": "",
"add_more_editors": "",
"add_more_managers": "",
"add_more_members": "",
@@ -123,7 +122,6 @@
"all_these_experiments_are_available_exclusively": "",
"allows_to_search_by_author_title_etc_possible_to_pull_results_directly_from_your_reference_manager_if_connected": "",
"already_subscribed_try_refreshing_the_page": "",
- "also": "",
"an_email_has_already_been_sent_to": "",
"an_error_occured_while_restoring_project": "",
"an_error_occurred_when_verifying_the_coupon_code": "",
@@ -866,7 +864,6 @@
"link_accounts_and_add_email": "",
"link_institutional_email_get_started": "",
"link_sharing": "",
- "link_sharing_is_off": "",
"link_sharing_is_off_short": "",
"link_sharing_is_on": "",
"link_to_github": "",
@@ -986,7 +983,6 @@
"need_more_than_x_licenses": "",
"need_to_add_new_primary_before_remove": "",
"need_to_leave": "",
- "need_to_upgrade_for_more_collabs": "",
"new_compile_domain_notice": "",
"new_file": "",
"new_folder": "",
@@ -1284,7 +1280,6 @@
"remove": "",
"remove_access": "",
"remove_add_on": "",
- "remove_collaborator": "",
"remove_from_group": "",
"remove_link": "",
"remove_manager": "",
@@ -1448,7 +1443,6 @@
"setup_another_account_under_a_personal_email_address": "",
"share": "",
"share_project": "",
- "share_with_your_collabs": "",
"shared_with_you": "",
"sharelatex_beta_program": "",
"shortcut_to_open_advanced_reference_search": "",
diff --git a/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx b/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx
index fa2b8b2523..d8e19acb22 100644
--- a/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx
+++ b/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx
@@ -3,9 +3,9 @@ import { useOnlineUsersContext } from '@/features/ide-react/context/online-users
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import * as eventTracking from '@/infrastructure/event-tracking'
import EditorNavigationToolbarRoot from '@/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root'
-import ShareProjectModal from '@/features/share-project-modal/components/restricted-link-sharing/share-project-modal'
-import EditorOverLimitModal from '@/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal'
-import ViewOnlyAccessModal from '@/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal'
+import ShareProjectModal from '@/features/share-project-modal/components/share-project-modal'
+import EditorOverLimitModal from '@/features/share-project-modal/components/editor-over-limit-modal'
+import ViewOnlyAccessModal from '@/features/share-project-modal/components/view-only-access-modal'
function EditorNavigationToolbar() {
const [showShareModal, setShowShareModal] = useState(false)
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/access-levels-changed.tsx b/services/web/frontend/js/features/share-project-modal/components/access-levels-changed.tsx
similarity index 100%
rename from services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/access-levels-changed.tsx
rename to services/web/frontend/js/features/share-project-modal/components/access-levels-changed.tsx
diff --git a/services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.tsx b/services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.tsx
index 8e485d6d35..9ecb7d5583 100644
--- a/services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.tsx
+++ b/services/web/frontend/js/features/share-project-modal/components/add-collaborators-upgrade.tsx
@@ -1,104 +1,74 @@
-import { useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { useUserContext } from '../../../shared/context/user-context'
+import Notification from '@/shared/components/notification'
import { upgradePlan } from '@/main/account-upgrade'
-import StartFreeTrialButton from '../../../shared/components/start-free-trial-button'
-import Icon from '../../../shared/components/icon'
-import { useFeatureFlag } from '../../../shared/context/split-test-context'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import MaterialIcon from '@/shared/components/material-icon'
+import { linkSharingEnforcementDate } from '../utils/link-sharing'
+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'
import OLButton from '@/features/ui/components/ol/ol-button'
export default function AddCollaboratorsUpgrade() {
const { t } = useTranslation()
+ const { features } = useProjectContext()
const user = useUserContext()
- const [startedFreeTrial, setStartedFreeTrial] = useState(false)
- const hasNewPaywallCta = useFeatureFlag('paywall-cta')
-
return (
-
- {t('need_to_upgrade_for_more_collabs')}. {t('also')}:
-
-
- -
- }
- bs5={}
- />
-
- {t('unlimited_projects')}
-
- -
- }
- bs5={}
- />
-
- {t('collabs_per_proj', {
- collabcount: 'Multiple',
- })}
-
- -
- }
- bs5={}
- />
-
- {t('full_doc_history')}
-
- -
- }
- bs5={}
- />
-
- {t('sync_to_dropbox')}
-
- -
- }
- bs5={}
- />
-
- {t('sync_to_github')}
-
- -
- }
- bs5={}
- />
-
- {t('compile_larger_projects')}
-
-
-
- {user.allowedFreeTrial ? (
- setStartedFreeTrial(true)}
- source="project-sharing"
- >
- {hasNewPaywallCta
- ? t('add_more_collaborators')
- : t('start_free_trial')}
-
- ) : (
- {
- upgradePlan('project-sharing')
- setStartedFreeTrial(true)
- }}
- >
- {t('upgrade')}
-
- )}
-
- {startedFreeTrial && (
-
{t('refresh_page_after_starting_free_trial')}
- )}
+
+ {t('your_plan_is_limited_to_n_editors', {
+ count: features.collaborators,
+ })}{' '}
+ {t('from_enforcement_date', {
+ enforcementDate: linkSharingEnforcementDate,
+ })}
+
+ }
+ action={
+
+ {user.allowedFreeTrial ? (
+
+ {t('upgrade')}
+
+ ) : (
+
{
+ upgradePlan('project-sharing')
+ }}
+ >
+ {t('upgrade')}
+
+ )}
+
{
+ sendMB('paywall-info-click', {
+ 'paywall-type': 'project-sharing',
+ content: 'blog',
+ variant: 'exceeds',
+ })
+ }}
+ >
+ {t('read_more')}
+
+
+ }
+ />
)
}
diff --git a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx
index 3a109b335d..98714bb487 100644
--- a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx
@@ -1,20 +1,22 @@
-import { useState, useMemo, useCallback } from 'react'
+import { useEffect, useState, useMemo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useMultipleSelection } from 'downshift'
import { useShareProjectContext } from './share-project-modal'
import SelectCollaborators from './select-collaborators'
import { resendInvite, sendInvite } from '../utils/api'
import { useUserContacts } from '../hooks/use-user-contacts'
-import useIsMounted from '../../../shared/hooks/use-is-mounted'
-import { useProjectContext } from '../../../shared/context/project-context'
-import { sendMB } from '../../../infrastructure/event-tracking'
+import useIsMounted from '@/shared/hooks/use-is-mounted'
+import { useProjectContext } from '@/shared/context/project-context'
+import { sendMB } from '@/infrastructure/event-tracking'
import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
+import PropTypes from 'prop-types'
import OLForm from '@/features/ui/components/ol/ol-form'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
import OLButton from '@/features/ui/components/ol/ol-button'
+import getMeta from '@/utils/meta'
-export default function AddCollaborators() {
+export default function AddCollaborators({ readOnly }) {
const [privileges, setPrivileges] = useState('readAndWrite')
const isMounted = useIsMounted()
@@ -49,6 +51,12 @@ export default function AddCollaborators() {
const { reset, selectedItems } = multipleSelectionProps
+ useEffect(() => {
+ if (readOnly && privileges === 'readAndWrite') {
+ setPrivileges('readOnly')
+ }
+ }, [privileges, readOnly])
+
const handleSubmit = useCallback(async () => {
if (!selectedItems.length) {
return
@@ -142,12 +150,12 @@ export default function AddCollaborators() {
])
return (
-
+
@@ -162,8 +170,13 @@ export default function AddCollaborators() {
bsSize: 'sm',
}}
>
-
-
+
+
+ {getMeta('ol-isReviewerRoleEnabled') && (
+
+ )}
- {t('share')}
+ {t('invite')}
)
}
+
+AddCollaborators.propTypes = {
+ readOnly: PropTypes.bool,
+}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx b/services/web/frontend/js/features/share-project-modal/components/collaborators-limit-upgrade.tsx
similarity index 100%
rename from services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx
rename to services/web/frontend/js/features/share-project-modal/components/collaborators-limit-upgrade.tsx
diff --git a/services/web/frontend/js/features/share-project-modal/components/edit-member.jsx b/services/web/frontend/js/features/share-project-modal/components/edit-member.jsx
deleted file mode 100644
index bffd42dc87..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/edit-member.jsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import { useState, useEffect } from 'react'
-import PropTypes from 'prop-types'
-import { useTranslation } from 'react-i18next'
-import { useShareProjectContext } from './share-project-modal'
-import TransferOwnershipModal from './transfer-ownership-modal'
-import { removeMemberFromProject, updateMember } from '../utils/api'
-import Icon from '../../../shared/components/icon'
-import { useProjectContext } from '../../../shared/context/project-context'
-import { sendMB } from '../../../infrastructure/event-tracking'
-import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
-import OLCol from '@/features/ui/components/ol/ol-col'
-import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
-import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
-import OLButton from '@/features/ui/components/ol/ol-button'
-import MaterialIcon from '@/shared/components/material-icon'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-
-export default function EditMember({ member }) {
- const [privileges, setPrivileges] = useState(member.privileges)
- const [confirmingOwnershipTransfer, setConfirmingOwnershipTransfer] =
- useState(false)
-
- // update the local state if the member's privileges change externally
- useEffect(() => {
- setPrivileges(member.privileges)
- }, [member.privileges])
-
- const { updateProject, monitorRequest } = useShareProjectContext()
- const { _id: projectId, members } = useProjectContext()
-
- function handleSubmit(event) {
- event.preventDefault()
-
- if (privileges === 'owner') {
- setConfirmingOwnershipTransfer(true)
- } else {
- monitorRequest(() =>
- updateMember(projectId, member, {
- privilegeLevel: privileges,
- })
- ).then(() => {
- updateProject({
- members: members.map(item =>
- item._id === member._id ? { ...item, privileges } : item
- ),
- })
- })
- }
- }
-
- if (confirmingOwnershipTransfer) {
- return (
- setConfirmingOwnershipTransfer(false)}
- />
- )
- }
-
- return (
-
- )
-}
-EditMember.propTypes = {
- member: PropTypes.shape({
- _id: PropTypes.string.isRequired,
- email: PropTypes.string.isRequired,
- privileges: PropTypes.string.isRequired,
- }),
-}
-
-function SelectPrivilege({ value, handleChange }) {
- const { t } = useTranslation()
-
- return (
-
-
-
-
-
- )
-}
-
-SelectPrivilege.propTypes = {
- value: PropTypes.string.isRequired,
- handleChange: PropTypes.func.isRequired,
-}
-
-function RemoveMemberAction({ member }) {
- const { t } = useTranslation()
- const { updateProject, monitorRequest } = useShareProjectContext()
- const { _id: projectId, members, invites } = useProjectContext()
-
- function handleClick(event) {
- event.preventDefault()
-
- monitorRequest(() => removeMemberFromProject(projectId, member)).then(
- () => {
- const updatedMembers = members.filter(existing => existing !== member)
- updateProject({
- members: updatedMembers,
- })
- sendMB('collaborator-removed', {
- project_id: projectId,
- current_collaborators_amount: updatedMembers.length,
- current_invites_amount: invites.length,
- })
- }
- )
- }
-
- return (
-
-
-
- }
- bs5={}
- />
-
-
-
- )
-}
-
-RemoveMemberAction.propTypes = {
- member: PropTypes.shape({
- _id: PropTypes.string.isRequired,
- email: PropTypes.string.isRequired,
- privileges: PropTypes.string.isRequired,
- }),
-}
-
-function ChangePrivilegesActions({ handleReset }) {
- const { t } = useTranslation()
-
- return (
-
-
- {t('change_or_cancel-change')}
-
-
- {t('change_or_cancel-or')}
-
-
- {t('change_or_cancel-cancel')}
-
-
-
- )
-}
-
-ChangePrivilegesActions.propTypes = {
- handleReset: PropTypes.func.isRequired,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/edit-member.tsx b/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx
similarity index 97%
rename from services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/edit-member.tsx
rename to services/web/frontend/js/features/share-project-modal/components/edit-member.tsx
index fad0781b36..26d5409b6e 100644
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/edit-member.tsx
+++ b/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx
@@ -3,14 +3,14 @@ import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import { useShareProjectContext } from './share-project-modal'
import TransferOwnershipModal from './transfer-ownership-modal'
-import { removeMemberFromProject, updateMember } from '../../utils/api'
+import { removeMemberFromProject, updateMember } from '../utils/api'
import Icon from '@/shared/components/icon'
import { useProjectContext } from '@/shared/context/project-context'
import { sendMB } from '@/infrastructure/event-tracking'
import { Select } from '@/shared/components/select'
import type { ProjectContextMember } from '@/shared/context/types/project-context'
import { PermissionsLevel } from '@/features/ide-react/types/permissions'
-import { linkSharingEnforcementDate } from '../../utils/link-sharing'
+import { linkSharingEnforcementDate } from '../utils/link-sharing'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLCol from '@/features/ui/components/ol/ol-col'
@@ -128,7 +128,9 @@ export default function EditMember({
id="share-project-form"
onSubmit={e => {
e.preventDefault()
- commitPrivilegeChange(privileges)
+ if (privilegeChangePending) {
+ commitPrivilegeChange(privileges)
+ }
}}
>
-
+
{invite.email}
-
{t('invite_not_accepted')}
.
@@ -30,12 +29,12 @@ export default function Invite({ invite, isProjectOwner }) {
-
+
{isProjectOwner && (
-
+
)}
diff --git a/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx b/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx
index 11b5e4839f..fd306b15ae 100644
--- a/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/link-sharing.jsx
@@ -1,16 +1,16 @@
import { useCallback, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
-import Icon from '../../../shared/components/icon'
+import Icon from '@/shared/components/icon'
import { useShareProjectContext } from './share-project-modal'
import { setProjectAccessLevel } from '../utils/api'
import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
-import { useProjectContext } from '../../../shared/context/project-context'
+import { useProjectContext } from '@/shared/context/project-context'
import * as eventTracking from '../../../infrastructure/event-tracking'
-import { useUserContext } from '../../../shared/context/user-context'
+import { useUserContext } from '@/shared/context/user-context'
import { sendMB } from '../../../infrastructure/event-tracking'
import { getJSON } from '../../../infrastructure/fetch-json'
-import useAbortController from '../../../shared/hooks/use-abort-controller'
+import useAbortController from '@/shared/hooks/use-abort-controller'
import { debugConsole } from '@/utils/debugging'
import getMeta from '@/utils/meta'
import OLRow from '@/features/ui/components/ol/ol-row'
@@ -22,6 +22,7 @@ import MaterialIcon from '@/shared/components/material-icon'
export default function LinkSharing() {
const [inflight, setInflight] = useState(false)
+ const [showLinks, setShowLinks] = useState(true)
const { monitorRequest } = useShareProjectContext()
@@ -57,6 +58,7 @@ export default function LinkSharing() {
setAccessLevel={setAccessLevel}
inflight={inflight}
projectId={projectId}
+ setShowLinks={setShowLinks}
/>
)
@@ -66,6 +68,8 @@ export default function LinkSharing() {
)
@@ -85,12 +89,12 @@ export default function LinkSharing() {
}
}
-function PrivateSharing({ setAccessLevel, inflight, projectId }) {
+function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
const { t } = useTranslation()
return (
- {t('link_sharing_is_off')}
+ {t('link_sharing_is_off_short')}
{
setAccessLevel('tokenBased')
eventTracking.sendMB('link-sharing-click', { projectId })
+ setShowLinks(true)
}}
disabled={inflight}
>
@@ -114,9 +119,15 @@ PrivateSharing.propTypes = {
setAccessLevel: PropTypes.func.isRequired,
inflight: PropTypes.bool,
projectId: PropTypes.string,
+ setShowLinks: PropTypes.func.isRequired,
}
-function TokenBasedSharing({ setAccessLevel, inflight }) {
+function TokenBasedSharing({
+ setAccessLevel,
+ inflight,
+ setShowLinks,
+ showLinks,
+}) {
const { t } = useTranslation()
const { _id: projectId } = useProjectContext()
@@ -145,27 +156,43 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
-
-
-
-
{t('anyone_with_link_can_edit')}
-
setShowLinks(!showLinks)}
+ >
+ }
+ bs5={
+
+ }
/>
-
-
-
{t('anyone_with_link_can_view')}
-
-
+
+ {showLinks && (
+
+
+
{t('anyone_with_link_can_edit')}
+
+
+
+
{t('anyone_with_link_can_view')}
+
+
+
+ )}
)
}
@@ -173,6 +200,8 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
TokenBasedSharing.propTypes = {
setAccessLevel: PropTypes.func.isRequired,
inflight: PropTypes.bool,
+ setShowLinks: PropTypes.func.isRequired,
+ showLinks: PropTypes.bool,
}
function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
diff --git a/services/web/frontend/js/features/share-project-modal/components/owner-info.jsx b/services/web/frontend/js/features/share-project-modal/components/owner-info.jsx
index bb0e8e99c6..e8dea7e6cd 100644
--- a/services/web/frontend/js/features/share-project-modal/components/owner-info.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/owner-info.jsx
@@ -1,7 +1,10 @@
-import { useProjectContext } from '../../../shared/context/project-context'
+import { useProjectContext } from '@/shared/context/project-context'
import { useTranslation } from 'react-i18next'
+import Icon from '@/shared/components/icon'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
export default function OwnerInfo() {
const { t } = useTranslation()
@@ -9,8 +12,16 @@ export default function OwnerInfo() {
return (
- {owner?.email}
-
+
+
+
}
+ bs5={
}
+ />
+
{owner?.email}
+
+
+
{t('owner')}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators-upgrade.tsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators-upgrade.tsx
deleted file mode 100644
index 6619b41a5d..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators-upgrade.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import Notification from '@/shared/components/notification'
-import { upgradePlan } from '@/main/account-upgrade'
-import { linkSharingEnforcementDate } from '../../utils/link-sharing'
-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'
-import OLButton from '@/features/ui/components/ol/ol-button'
-
-export default function AddCollaboratorsUpgrade() {
- const { t } = useTranslation()
- const { features } = useProjectContext()
- const user = useUserContext()
-
- return (
-
-
- {t('your_plan_is_limited_to_n_editors', {
- count: features.collaborators,
- })}{' '}
- {t('from_enforcement_date', {
- enforcementDate: linkSharingEnforcementDate,
- })}
-
- }
- action={
-
- {user.allowedFreeTrial ? (
-
- {t('upgrade')}
-
- ) : (
-
{
- upgradePlan('project-sharing')
- }}
- >
- {t('upgrade')}
-
- )}
-
{
- sendMB('paywall-info-click', {
- 'paywall-type': 'project-sharing',
- content: 'blog',
- variant: 'exceeds',
- })
- }}
- >
- {t('read_more')}
-
-
- }
- />
-
- )
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators.jsx
deleted file mode 100644
index eea0e7af94..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/add-collaborators.jsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import { useEffect, useState, useMemo, useCallback } from 'react'
-import { useTranslation } from 'react-i18next'
-import { useMultipleSelection } from 'downshift'
-import { useShareProjectContext } from './share-project-modal'
-import SelectCollaborators from './select-collaborators'
-import { resendInvite, sendInvite } from '../../utils/api'
-import { useUserContacts } from '../../hooks/use-user-contacts'
-import useIsMounted from '@/shared/hooks/use-is-mounted'
-import { useProjectContext } from '@/shared/context/project-context'
-import { sendMB } from '@/infrastructure/event-tracking'
-import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
-import PropTypes from 'prop-types'
-import OLForm from '@/features/ui/components/ol/ol-form'
-import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
-import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
-import OLButton from '@/features/ui/components/ol/ol-button'
-import getMeta from '@/utils/meta'
-
-export default function AddCollaborators({ readOnly }) {
- const [privileges, setPrivileges] = useState('readAndWrite')
-
- const isMounted = useIsMounted()
-
- const { data: contacts } = useUserContacts()
-
- const { t } = useTranslation()
-
- const { updateProject, setInFlight, setError } = useShareProjectContext()
-
- const { _id: projectId, members, invites } = useProjectContext()
-
- const currentMemberEmails = useMemo(
- () => (members || []).map(member => member.email).sort(),
- [members]
- )
-
- const nonMemberContacts = useMemo(() => {
- if (!contacts) {
- return null
- }
-
- return contacts.filter(
- contact => !currentMemberEmails.includes(contact.email)
- )
- }, [contacts, currentMemberEmails])
-
- const multipleSelectionProps = useMultipleSelection({
- initialActiveIndex: 0,
- initialSelectedItems: [],
- })
-
- const { reset, selectedItems } = multipleSelectionProps
-
- useEffect(() => {
- if (readOnly && privileges === 'readAndWrite') {
- setPrivileges('readOnly')
- }
- }, [privileges, readOnly])
-
- const handleSubmit = useCallback(async () => {
- if (!selectedItems.length) {
- return
- }
-
- // reset the selected items
- reset()
-
- setError(undefined)
- setInFlight(true)
-
- for (const contact of selectedItems) {
- // unmounting means can't add any more collaborators
- if (!isMounted.current) {
- break
- }
-
- const email = contact.type === 'user' ? contact.email : contact.display
- const normalisedEmail = email.toLowerCase()
-
- if (currentMemberEmails.includes(normalisedEmail)) {
- continue
- }
-
- let data
-
- try {
- const invite = (invites || []).find(
- invite => invite.email === normalisedEmail
- )
-
- if (invite) {
- data = await resendInvite(projectId, invite)
- } else {
- data = await sendInvite(projectId, email, privileges)
- }
-
- sendMB('collaborator-invited', {
- project_id: projectId,
- // invitation is only populated on successful invite, meaning that for paywall and other cases this will be null
- successful_invite: !!data.invite,
- users_updated: !!(data.users || data.user),
- current_collaborators_amount: members.length,
- current_invites_amount: invites.length,
- })
- } catch (error) {
- setInFlight(false)
- setError(
- error.data?.errorReason ||
- (error.response?.status === 429
- ? 'too_many_requests'
- : 'generic_something_went_wrong')
- )
- break
- }
-
- if (data.error) {
- setError(data.error)
- setInFlight(false)
- } else if (data.invite) {
- updateProject({
- invites: invites.concat(data.invite),
- })
- } else if (data.users) {
- updateProject({
- members: members.concat(data.users),
- })
- } else if (data.user) {
- updateProject({
- members: members.concat(data.user),
- })
- }
-
- // wait for a short time, so canAddCollaborators has time to update with new collaborator information
- await new Promise(resolve => setTimeout(resolve, 100))
- }
-
- setInFlight(false)
- }, [
- currentMemberEmails,
- invites,
- isMounted,
- members,
- privileges,
- projectId,
- reset,
- selectedItems,
- setError,
- setInFlight,
- updateProject,
- ])
-
- return (
-
-
-
-
-
-
-
-
setPrivileges(event.target.value)}
- bs3Props={{
- bsSize: 'sm',
- }}
- >
-
-
- {getMeta('ol-isReviewerRoleEnabled') && (
-
- )}
-
-
-
- {t('invite')}
-
-
-
-
- )
-}
-
-AddCollaborators.propTypes = {
- readOnly: PropTypes.bool,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/invite.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/invite.jsx
deleted file mode 100644
index 14331abd8a..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/invite.jsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { useCallback } from 'react'
-import PropTypes from 'prop-types'
-import { useShareProjectContext } from './share-project-modal'
-import Icon from '@/shared/components/icon'
-import { useTranslation } from 'react-i18next'
-import MemberPrivileges from './member-privileges'
-import { resendInvite, revokeInvite } from '../../utils/api'
-import { useProjectContext } from '@/shared/context/project-context'
-import { sendMB } from '@/infrastructure/event-tracking'
-import OLRow from '@/features/ui/components/ol/ol-row'
-import OLCol from '@/features/ui/components/ol/ol-col'
-import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
-import OLButton from '@/features/ui/components/ol/ol-button'
-import MaterialIcon from '@/shared/components/material-icon'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-import classnames from 'classnames'
-
-export default function Invite({ invite, isProjectOwner }) {
- const { t } = useTranslation()
- return (
-
-
- {invite.email}
-
- {t('invite_not_accepted')}
- .
- {isProjectOwner && }
-
-
-
-
-
-
-
- {isProjectOwner && (
-
-
-
- )}
-
- )
-}
-
-Invite.propTypes = {
- invite: PropTypes.object.isRequired,
- isProjectOwner: PropTypes.bool.isRequired,
-}
-
-function ResendInvite({ invite }) {
- const { t } = useTranslation()
- const { monitorRequest, setError, inFlight } = useShareProjectContext()
- const { _id: projectId } = useProjectContext()
-
- // const buttonRef = useRef(null)
- //
- const handleClick = useCallback(
- () =>
- monitorRequest(() => resendInvite(projectId, invite))
- .catch(error => {
- if (error?.response?.status === 404) {
- setError('invite_expired')
- }
- if (error?.response?.status === 429) {
- setError('invite_resend_limit_hit')
- }
- })
- .finally(() => {
- // NOTE: disabled as react-bootstrap v0.33.1 isn't forwarding the ref to the `button`
- // if (buttonRef.current) {
- // buttonRef.current.blur()
- // }
- document.activeElement.blur()
- }),
- [invite, monitorRequest, projectId, setError]
- )
-
- return (
-
- {t('resend')}
-
- )
-}
-
-ResendInvite.propTypes = {
- invite: PropTypes.object.isRequired,
-}
-
-function RevokeInvite({ invite }) {
- const { t } = useTranslation()
- const { updateProject, monitorRequest } = useShareProjectContext()
- const { _id: projectId, invites, members } = useProjectContext()
-
- function handleClick(event) {
- event.preventDefault()
-
- monitorRequest(() => revokeInvite(projectId, invite)).then(() => {
- const updatedInvites = invites.filter(existing => existing !== invite)
- updateProject({
- invites: updatedInvites,
- })
- sendMB('collaborator-invite-revoked', {
- project_id: projectId,
- current_invites_amount: updatedInvites.length,
- current_collaborators_amount: members.length,
- })
- })
- }
-
- return (
-
-
- }
- bs5={}
- />
-
-
- )
-}
-
-RevokeInvite.propTypes = {
- invite: PropTypes.object.isRequired,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/link-sharing.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/link-sharing.jsx
deleted file mode 100644
index 816ff96a3a..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/link-sharing.jsx
+++ /dev/null
@@ -1,325 +0,0 @@
-import { useCallback, useState, useEffect } from 'react'
-import PropTypes from 'prop-types'
-import { useTranslation } from 'react-i18next'
-import Icon from '@/shared/components/icon'
-import { useShareProjectContext } from './share-project-modal'
-import { setProjectAccessLevel } from '../../utils/api'
-import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
-import { useProjectContext } from '@/shared/context/project-context'
-import * as eventTracking from '../../../../infrastructure/event-tracking'
-import { useUserContext } from '@/shared/context/user-context'
-import { sendMB } from '../../../../infrastructure/event-tracking'
-import { getJSON } from '../../../../infrastructure/fetch-json'
-import useAbortController from '@/shared/hooks/use-abort-controller'
-import { debugConsole } from '@/utils/debugging'
-import getMeta from '@/utils/meta'
-import OLRow from '@/features/ui/components/ol/ol-row'
-import OLCol from '@/features/ui/components/ol/ol-col'
-import OLButton from '@/features/ui/components/ol/ol-button'
-import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import MaterialIcon from '@/shared/components/material-icon'
-
-export default function LinkSharing() {
- const [inflight, setInflight] = useState(false)
- const [showLinks, setShowLinks] = useState(true)
-
- const { monitorRequest } = useShareProjectContext()
-
- const { _id: projectId, publicAccessLevel } = useProjectContext()
-
- // set the access level of a project
- const setAccessLevel = useCallback(
- newPublicAccessLevel => {
- setInflight(true)
- sendMB('link-sharing-click-off', {
- project_id: projectId,
- })
- monitorRequest(() =>
- setProjectAccessLevel(projectId, newPublicAccessLevel)
- )
- .then(() => {
- // NOTE: not calling `updateProject` here as it receives data via
- // project:publicAccessLevel:changed over the websocket connection
- // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
- })
- .finally(() => {
- setInflight(false)
- })
- },
- [monitorRequest, projectId]
- )
-
- switch (publicAccessLevel) {
- // Private (with token-access available)
- case 'private':
- return (
-
- )
-
- // Token-based access
- case 'tokenBased':
- return (
-
- )
-
- // Legacy public-access
- case 'readAndWrite':
- case 'readOnly':
- return (
-
- )
-
- default:
- return null
- }
-}
-
-function PrivateSharing({ setAccessLevel, inflight, projectId, setShowLinks }) {
- const { t } = useTranslation()
- return (
-
-
- {t('link_sharing_is_off_short')}
-
- {
- setAccessLevel('tokenBased')
- eventTracking.sendMB('link-sharing-click', { projectId })
- setShowLinks(true)
- }}
- disabled={inflight}
- >
- {t('turn_on_link_sharing')}
-
-
-
-
-
- )
-}
-
-PrivateSharing.propTypes = {
- setAccessLevel: PropTypes.func.isRequired,
- inflight: PropTypes.bool,
- projectId: PropTypes.string,
- setShowLinks: PropTypes.func.isRequired,
-}
-
-function TokenBasedSharing({
- setAccessLevel,
- inflight,
- setShowLinks,
- showLinks,
-}) {
- const { t } = useTranslation()
- const { _id: projectId } = useProjectContext()
-
- const [tokens, setTokens] = useState(null)
-
- const { signal } = useAbortController()
-
- useEffect(() => {
- getJSON(`/project/${projectId}/tokens`, { signal })
- .then(data => setTokens(data))
- .catch(debugConsole.error)
- }, [projectId, signal])
-
- return (
-
-
- {t('link_sharing_is_on')}
-
- setAccessLevel('private')}
- disabled={inflight}
- >
- {t('turn_off_link_sharing')}
-
-
-
- setShowLinks(!showLinks)}
- >
- }
- bs5={
-
- }
- />
-
-
- {showLinks && (
-
-
-
{t('anyone_with_link_can_edit')}
-
-
-
-
{t('anyone_with_link_can_view')}
-
-
-
- )}
-
- )
-}
-
-TokenBasedSharing.propTypes = {
- setAccessLevel: PropTypes.func.isRequired,
- inflight: PropTypes.bool,
- setShowLinks: PropTypes.func.isRequired,
- showLinks: PropTypes.bool,
-}
-
-function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
- const { t } = useTranslation()
-
- return (
-
-
-
- {accessLevel === 'readAndWrite' && t('this_project_is_public')}
- {accessLevel === 'readOnly' && t('this_project_is_public_read_only')}
-
-
- setAccessLevel('private')}
- disabled={inflight}
- >
- {t('make_private')}
-
-
-
-
-
- )
-}
-
-LegacySharing.propTypes = {
- accessLevel: PropTypes.string.isRequired,
- setAccessLevel: PropTypes.func.isRequired,
- inflight: PropTypes.bool,
-}
-
-export function ReadOnlyTokenLink() {
- const { t } = useTranslation()
- const { _id: projectId } = useProjectContext()
-
- const [tokens, setTokens] = useState(null)
-
- const { signal } = useAbortController()
-
- useEffect(() => {
- getJSON(`/project/${projectId}/tokens`, { signal })
- .then(data => setTokens(data))
- .catch(debugConsole.error)
- }, [projectId, signal])
-
- return (
-
-
-
-
{t('anyone_with_link_can_view')}
-
-
-
-
- )
-}
-
-function AccessToken({ token, tokenHashPrefix, path, tooltipId }) {
- const { t } = useTranslation()
- const { isAdmin } = useUserContext()
-
- if (!token) {
- return (
-
- {t('loading')}…
-
- )
- }
-
- let origin = window.location.origin
- if (isAdmin) {
- origin = getMeta('ol-ExposedSettings').siteUrl
- }
- const link = `${origin}${path}${token}${
- tokenHashPrefix ? `#${tokenHashPrefix}` : ''
- }`
-
- return (
-
- {link}
-
-
- )
-}
-
-AccessToken.propTypes = {
- token: PropTypes.string,
- tokenHashPrefix: PropTypes.string,
- tooltipId: PropTypes.string.isRequired,
- path: PropTypes.string.isRequired,
-}
-
-function LinkSharingInfo() {
- const { t } = useTranslation()
-
- return (
-
-
- }
- bs5={}
- />
-
-
- )
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/member-privileges.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/member-privileges.jsx
deleted file mode 100644
index a968bfc5fd..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/member-privileges.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import PropTypes from 'prop-types'
-import { useTranslation } from 'react-i18next'
-
-export default function MemberPrivileges({ privileges }) {
- const { t } = useTranslation()
-
- switch (privileges) {
- case 'readAndWrite':
- return t('can_edit')
-
- case 'readOnly':
- return t('read_only')
-
- case 'review':
- return t('can_review')
-
- default:
- return null
- }
-}
-MemberPrivileges.propTypes = {
- privileges: PropTypes.string.isRequired,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/owner-info.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/owner-info.jsx
deleted file mode 100644
index e8dea7e6cd..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/owner-info.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useProjectContext } from '@/shared/context/project-context'
-import { useTranslation } from 'react-i18next'
-import Icon from '@/shared/components/icon'
-import OLRow from '@/features/ui/components/ol/ol-row'
-import OLCol from '@/features/ui/components/ol/ol-col'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import MaterialIcon from '@/shared/components/material-icon'
-
-export default function OwnerInfo() {
- const { t } = useTranslation()
- const { owner } = useProjectContext()
-
- return (
-
-
-
-
}
- bs5={
}
- />
-
{owner?.email}
-
-
-
- {t('owner')}
-
-
- )
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx
deleted file mode 100644
index 3ccd72d091..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx
+++ /dev/null
@@ -1,419 +0,0 @@
-import { useEffect, useMemo, useState, useRef, useCallback } from 'react'
-import PropTypes from 'prop-types'
-import { useTranslation } from 'react-i18next'
-import { matchSorter } from 'match-sorter'
-import { useCombobox } from 'downshift'
-import classnames from 'classnames'
-
-import Icon from '@/shared/components/icon'
-import MaterialIcon from '@/shared/components/material-icon'
-import Tag from '@/features/ui/components/bootstrap-5/tag'
-import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-import { Spinner } from 'react-bootstrap-5'
-
-// Unicode characters in these Unicode groups:
-// "General Punctuation — Spaces"
-// "General Punctuation — Format character" (including zero-width spaces)
-const matchAllSpaces =
- /[\u061C\u2000-\u200F\u202A-\u202E\u2060\u2066-\u2069\u2028\u2029\u202F]/g
-
-export default function SelectCollaborators({
- loading,
- options,
- placeholder,
- multipleSelectionProps,
-}) {
- const { t } = useTranslation()
- const {
- getSelectedItemProps,
- getDropdownProps,
- addSelectedItem,
- removeSelectedItem,
- selectedItems,
- } = multipleSelectionProps
-
- const [inputValue, setInputValue] = useState('')
-
- const selectedEmails = useMemo(
- () => selectedItems.map(item => item.email),
- [selectedItems]
- )
-
- const unselectedOptions = useMemo(
- () => options.filter(option => !selectedEmails.includes(option.email)),
- [options, selectedEmails]
- )
-
- const filteredOptions = useMemo(() => {
- if (inputValue === '') {
- return unselectedOptions
- }
-
- return matchSorter(unselectedOptions, inputValue, {
- keys: ['name', 'email'],
- threshold: matchSorter.rankings.CONTAINS,
- baseSort: (a, b) => {
- // Prefer server-side sorting for ties in the match ranking.
- return a.index - b.index > 0 ? 1 : -1
- },
- })
- }, [unselectedOptions, inputValue])
-
- const inputRef = useRef(null)
-
- const focusInput = useCallback(() => {
- if (inputRef.current) {
- window.setTimeout(() => {
- inputRef.current.focus()
- }, 10)
- }
- }, [inputRef])
-
- const isValidInput = useMemo(() => {
- if (inputValue.includes('@')) {
- for (const selectedItem of selectedItems) {
- if (selectedItem.email === inputValue) {
- return false
- }
- }
- }
-
- return true
- }, [inputValue, selectedItems])
-
- function stateReducer(state, actionAndChanges) {
- const { type, changes } = actionAndChanges
- // force selected item to be null so that adding, removing, then re-adding the same collaborator is recognised as a selection change
- if (type === useCombobox.stateChangeTypes.InputChange) {
- return { ...changes, selectedItem: null }
- }
- return changes
- }
-
- const {
- isOpen,
- getLabelProps,
- getMenuProps,
- getInputProps,
- getComboboxProps,
- highlightedIndex,
- getItemProps,
- reset,
- } = useCombobox({
- inputValue,
- defaultHighlightedIndex: 0,
- items: filteredOptions,
- itemToString: item => item && item.name,
- stateReducer,
- onStateChange: ({ inputValue, type, selectedItem }) => {
- switch (type) {
- // add a selected item on Enter (keypress), click or blur
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
- case useCombobox.stateChangeTypes.ItemClick:
- case useCombobox.stateChangeTypes.InputBlur:
- if (selectedItem) {
- setInputValue('')
- addSelectedItem(selectedItem)
- }
- break
- }
- },
- })
-
- const addNewItem = useCallback(
- (_email, focus = true) => {
- const email = _email.replace(matchAllSpaces, '')
-
- if (
- isValidInput &&
- email.includes('@') &&
- !selectedEmails.includes(email)
- ) {
- addSelectedItem({
- email,
- display: email,
- type: 'user',
- })
- setInputValue('')
- reset()
- if (focus) {
- focusInput()
- }
- return true
- }
- },
- [addSelectedItem, selectedEmails, isValidInput, focusInput, reset]
- )
-
- // close and reset the menu when there are no matching items
- useEffect(() => {
- if (isOpen && filteredOptions.length === 0) {
- reset()
- }
- }, [reset, isOpen, filteredOptions.length])
-
- return (
-
- {/* eslint-disable-next-line jsx-a11y/label-has-for */}
-
-
-
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
-
- {selectedItems.map((selectedItem, index) => (
-
- ))}
-
- {
- addNewItem(inputValue, false)
- },
- onChange: e => {
- setInputValue(e.target.value)
- },
- onClick: () => focusInput,
- onKeyDown: event => {
- switch (event.key) {
- case 'Enter':
- // Enter: always prevent form submission
- event.preventDefault()
- event.stopPropagation()
- break
-
- case 'Tab':
- // Tab: if the dropdown isn't open, try to create a new item using inputValue and prevent blur if successful
- if (!isOpen && addNewItem(inputValue)) {
- event.preventDefault()
- event.stopPropagation()
- }
- break
-
- case ',':
- // comma: try to create a new item using inputValue
- event.preventDefault()
- addNewItem(inputValue)
- break
- }
- },
- onPaste: event => {
- const data =
- // modern browsers
- event.clipboardData?.getData('text/plain') ??
- // IE11
- window.clipboardData?.getData('text')
-
- if (data) {
- const emails = data
- .split(/[\r\n,; ]+/)
- .filter(item => item.includes('@'))
-
- if (emails.length) {
- // pasted comma-separated email addresses
- event.preventDefault()
-
- for (const email of emails) {
- addNewItem(email)
- }
- }
- }
- },
- })
- )}
- />
-
-
-
-
- {isOpen &&
- filteredOptions.map((item, index) => (
-
- ))}
-
-
-
-
- )
-}
-SelectCollaborators.propTypes = {
- loading: PropTypes.bool.isRequired,
- options: PropTypes.array.isRequired,
- placeholder: PropTypes.string,
- multipleSelectionProps: PropTypes.shape({
- getSelectedItemProps: PropTypes.func.isRequired,
- getDropdownProps: PropTypes.func.isRequired,
- addSelectedItem: PropTypes.func.isRequired,
- removeSelectedItem: PropTypes.func.isRequired,
- selectedItems: PropTypes.array.isRequired,
- }).isRequired,
-}
-
-function Option({ selected, item, getItemProps, index }) {
- return (
-
-
-
-
- {item.display}
- >
- }
- bs5={
-
- {item.display}
-
- }
- />
-
- )
-}
-
-Option.propTypes = {
- selected: PropTypes.bool.isRequired,
- item: PropTypes.shape({
- display: PropTypes.string.isRequired,
- }),
- index: PropTypes.number.isRequired,
- getItemProps: PropTypes.func.isRequired,
-}
-
-function SelectedItem({
- removeSelectedItem,
- selectedItem,
- focusInput,
- getSelectedItemProps,
- index,
-}) {
- const { t } = useTranslation()
-
- const handleClick = useCallback(
- event => {
- event.preventDefault()
- event.stopPropagation()
- removeSelectedItem(selectedItem)
- focusInput()
- },
- [focusInput, removeSelectedItem, selectedItem]
- )
-
- return (
-
-
- {selectedItem.display}
-
-
- }
- bs5={
- }
- bs5={}
- />
- }
- closeBtnProps={{
- onClick: handleClick,
- }}
- {...getSelectedItemProps({ selectedItem, index })}
- >
- {selectedItem.display}
-
- }
- />
- )
-}
-
-SelectedItem.propTypes = {
- focusInput: PropTypes.func.isRequired,
- removeSelectedItem: PropTypes.func.isRequired,
- selectedItem: PropTypes.shape({
- display: PropTypes.string.isRequired,
- }),
- getSelectedItemProps: PropTypes.func.isRequired,
- index: PropTypes.number.isRequired,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites.jsx
deleted file mode 100644
index f27d42f404..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/send-invites.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-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'
-import OLRow from '@/features/ui/components/ol/ol-row'
-
-export default function SendInvites({
- canAddCollaborators,
- hasExceededCollaboratorLimit,
- haveAnyEditorsBeenDowngraded,
- somePendingEditorsResolved,
-}) {
- return (
-
- {hasExceededCollaboratorLimit && !haveAnyEditorsBeenDowngraded && (
-
- )}
-
- {haveAnyEditorsBeenDowngraded && (
-
- )}
-
- {!canAddCollaborators &&
- !hasExceededCollaboratorLimit &&
- !haveAnyEditorsBeenDowngraded && }
-
-
- )
-}
-
-SendInvites.propTypes = {
- canAddCollaborators: PropTypes.bool,
- hasExceededCollaboratorLimit: PropTypes.bool,
- haveAnyEditorsBeenDowngraded: PropTypes.bool,
- somePendingEditorsResolved: PropTypes.bool,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-modal-body.tsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-modal-body.tsx
deleted file mode 100644
index aabb6f3628..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-modal-body.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import EditMember from './edit-member'
-import LinkSharing from './link-sharing'
-import Invite from './invite'
-import SendInvites from './send-invites'
-import ViewMember from './view-member'
-import OwnerInfo from './owner-info'
-import SendInvitesNotice from './send-invites-notice'
-import { useEditorContext } from '@/shared/context/editor-context'
-import { useProjectContext } from '@/shared/context/project-context'
-import { useMemo } from 'react'
-import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
-import getMeta from '@/utils/meta'
-
-export default function ShareModalBody() {
- const { members, invites, features } = useProjectContext()
- const { isProjectOwner } = useEditorContext()
-
- // whether the project has not reached the collaborator limit
- const canAddCollaborators = useMemo(() => {
- if (!isProjectOwner || !features) {
- return false
- }
-
- if (features.collaborators === -1) {
- // infinite collaborators
- return true
- }
-
- const editorInvites = invites.filter(
- invite => invite.privileges === 'readAndWrite'
- ).length
-
- return (
- members.filter(member => member.privileges === 'readAndWrite').length +
- editorInvites <
- (features.collaborators ?? 1)
- )
- }, [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
- }
-
- if (features.collaborators === -1) {
- return false
- }
-
- return (
- members.filter(member => member.privileges === 'readAndWrite').length >
- (features.collaborators ?? 1)
- )
- }, [features, isProjectOwner, members])
-
- const sortedMembers = useMemo(() => {
- return [
- ...members.filter(member => member.privileges === 'readAndWrite'),
- ...members.filter(member => member.pendingEditor),
- ...members.filter(
- member => !member.pendingEditor && member.privileges !== 'readAndWrite'
- ),
- ]
- }, [members])
-
- return (
- <>
- {isProjectOwner ? (
-
- ) : (
-
- )}
- {isProjectOwner && }
-
-
-
- {sortedMembers.map(member =>
- isProjectOwner ? (
-
- ) : (
-
- )
- )}
-
- {invites.map(invite => (
-
- ))}
-
- {!getMeta('ol-ExposedSettings').recaptchaDisabled?.invite && (
-
- )}
- >
- )
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal-content.tsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal-content.tsx
deleted file mode 100644
index 7a9656e61c..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal-content.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import Icon from '@/shared/components/icon'
-import { useEditorContext } from '@/shared/context/editor-context'
-import { lazy, Suspense } from 'react'
-import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
-import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer'
-import OLModal, {
- OLModalBody,
- OLModalFooter,
- OLModalHeader,
- OLModalTitle,
-} from '@/features/ui/components/ol/ol-modal'
-import OLNotification from '@/features/ui/components/ol/ol-notification'
-import OLButton from '@/features/ui/components/ol/ol-button'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-import { Spinner } from 'react-bootstrap-5'
-
-const ReadOnlyTokenLink = lazy(() =>
- import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
- // re-export as default -- lazy can only handle default exports.
- default: ReadOnlyTokenLink,
- }))
-)
-
-const ShareModalBody = lazy(() => import('./share-modal-body'))
-
-type ShareProjectModalContentProps = {
- cancel: () => void
- show: boolean
- animation: boolean
- inFlight: boolean
- error: string | undefined
-}
-
-export default function ShareProjectModalContent({
- show,
- cancel,
- animation,
- inFlight,
- error,
-}: ShareProjectModalContentProps) {
- const { t } = useTranslation()
-
- const { isRestrictedTokenMember } = useEditorContext()
-
- return (
-
-
- {t('share_project')}
-
-
-
-
-
}>
- {isRestrictedTokenMember ? (
-
- ) : (
-
- )}
-
- {error && (
-
}
- className="mb-0 mt-3"
- />
- )}
-
-
-
-
-
- {inFlight && (
- }
- bs5={
-
- }
- />
- )}
-
-
-
- {t('close')}
-
-
-
- )
-}
-
-function ErrorMessage({ error }: Pick) {
- const { t } = useTranslation()
- switch (error) {
- case 'cannot_invite_non_user':
- return <>{t('cannot_invite_non_user')}>
-
- case 'cannot_verify_user_not_robot':
- return <>{t('cannot_verify_user_not_robot')}>
-
- case 'cannot_invite_self':
- return <>{t('cannot_invite_self')}>
-
- case 'invalid_email':
- return <>{t('invalid_email')}>
-
- case 'too_many_requests':
- return <>{t('too_many_requests')}>
-
- case 'invite_expired':
- return <>{t('invite_expired')}>
-
- case 'invite_resend_limit_hit':
- return <>{t('invite_resend_limit_hit')}>
-
- default:
- return <>{t('generic_something_went_wrong')}>
- }
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx
deleted file mode 100644
index ca2756fa14..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import React, {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useState,
-} from 'react'
-import ShareProjectModalContent from './share-project-modal-content'
-import { useProjectContext } from '@/shared/context/project-context'
-import { useSplitTestContext } from '@/shared/context/split-test-context'
-import { sendMB } from '@/infrastructure/event-tracking'
-import { ProjectContextUpdateValue } from '@/shared/context/types/project-context'
-import { useEditorContext } from '@/shared/context/editor-context'
-import customLocalStorage from '@/infrastructure/local-storage'
-
-type ShareProjectContextValue = {
- updateProject: (project: ProjectContextUpdateValue) => void
- monitorRequest: >(request: () => T) => T
- inFlight: boolean
- setInFlight: React.Dispatch<
- React.SetStateAction
- >
- error: string | undefined
- setError: React.Dispatch<
- React.SetStateAction
- >
-}
-
-const SHOW_MODAL_COOLDOWN_PERIOD = 24 * 60 * 60 * 1000 // 24 hours
-
-const ShareProjectContext = createContext(
- undefined
-)
-
-export function useShareProjectContext() {
- const context = useContext(ShareProjectContext)
-
- if (!context) {
- throw new Error(
- 'useShareProjectContext is only available inside ShareProjectProvider'
- )
- }
-
- return context
-}
-
-type ShareProjectModalProps = {
- handleHide: () => void
- show: boolean
- handleOpen: () => void
- animation?: boolean
-}
-
-const ShareProjectModal = React.memo(function ShareProjectModal({
- handleHide,
- show,
- handleOpen,
- animation = true,
-}: ShareProjectModalProps) {
- const [inFlight, setInFlight] =
- useState(false)
- const [error, setError] = useState()
-
- const project = useProjectContext()
- const { isProjectOwner } = useEditorContext()
-
- const { splitTestVariants } = useSplitTestContext()
-
- // show the new share modal if project owner
- // is over collaborator limit or has pending editors (once every 24 hours)
- useEffect(() => {
- const hasExceededCollaboratorLimit = () => {
- if (!isProjectOwner || !project.features) {
- return false
- }
-
- if (project.features.collaborators === -1) {
- return false
- }
- return (
- project.members.filter(member => member.privileges === 'readAndWrite')
- .length > (project.features.collaborators ?? 1) ||
- project.members.some(member => member.pendingEditor)
- )
- }
-
- if (hasExceededCollaboratorLimit()) {
- const localStorageKey = `last-shown-share-modal.${project._id}`
- const lastShownShareModalTime =
- customLocalStorage.getItem(localStorageKey)
- if (
- !lastShownShareModalTime ||
- lastShownShareModalTime + SHOW_MODAL_COOLDOWN_PERIOD < Date.now()
- ) {
- handleOpen()
- customLocalStorage.setItem(localStorageKey, Date.now())
- }
- }
- }, [project, isProjectOwner, handleOpen])
-
- // send tracking event when the modal is opened
- useEffect(() => {
- if (show) {
- sendMB('share-modal-opened', {
- splitTestVariant: splitTestVariants['null-test-share-modal'],
- project_id: project._id,
- })
- }
- }, [splitTestVariants, project._id, show])
-
- // reset error when the modal is opened
- useEffect(() => {
- if (show) {
- setError(undefined)
- }
- }, [show])
-
- // close the modal if not in flight
- const cancel = useCallback(() => {
- if (!inFlight) {
- handleHide()
- }
- }, [handleHide, inFlight])
-
- // update `error` and `inFlight` while sending a request
- const monitorRequest = useCallback(request => {
- setError(undefined)
- setInFlight(true)
-
- const promise = request()
-
- promise.catch((error: { data?: Record }) => {
- setError(
- error.data?.errorReason ||
- error.data?.error ||
- 'generic_something_went_wrong'
- )
- })
-
- promise.finally(() => {
- setInFlight(false)
- })
-
- return promise
- }, [])
-
- // merge the new data with the old project data
- const updateProject = useCallback(
- data => Object.assign(project, data),
- [project]
- )
-
- if (!project) {
- return null
- }
-
- return (
-
-
-
- )
-})
-
-export default ShareProjectModal
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/transfer-ownership-modal.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/transfer-ownership-modal.jsx
deleted file mode 100644
index 88a14ac610..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/transfer-ownership-modal.jsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { useState } from 'react'
-import { Trans, useTranslation } from 'react-i18next'
-import PropTypes from 'prop-types'
-import Icon from '@/shared/components/icon'
-import { transferProjectOwnership } from '../../utils/api'
-import { useProjectContext } from '@/shared/context/project-context'
-import { useLocation } from '@/shared/hooks/use-location'
-import OLModal, {
- OLModalBody,
- OLModalFooter,
- OLModalHeader,
- OLModalTitle,
-} from '@/features/ui/components/ol/ol-modal'
-import OLNotification from '@/features/ui/components/ol/ol-notification'
-import OLButton from '@/features/ui/components/ol/ol-button'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import { bsVersion } from '@/features/utils/bootstrap-5'
-import { Spinner } from 'react-bootstrap-5'
-
-export default function TransferOwnershipModal({ member, cancel }) {
- const { t } = useTranslation()
-
- const [inflight, setInflight] = useState(false)
- const [error, setError] = useState(false)
- const location = useLocation()
-
- const { _id: projectId, name: projectName } = useProjectContext()
-
- function confirm() {
- setError(false)
- setInflight(true)
-
- transferProjectOwnership(projectId, member)
- .then(() => {
- location.reload()
- })
- .catch(() => {
- setError(true)
- setInflight(false)
- })
- }
-
- return (
-
-
- {t('change_project_owner')}
-
-
-
- , ]}
- shouldUnescape
- tOptions={{ interpolation: { escapeValue: true } }}
- />
-
- {t('project_ownership_transfer_confirmation_2')}
- {error && (
-
- )}
-
-
-
- {inflight && (
- }
- bs5={
-
- }
- />
- )}
-
-
- {t('cancel')}
-
-
- {t('change_owner')}
-
-
-
- )
-}
-TransferOwnershipModal.propTypes = {
- member: PropTypes.object.isRequired,
- cancel: PropTypes.func.isRequired,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-member.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-member.jsx
deleted file mode 100644
index cc173bd114..0000000000
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-member.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import PropTypes from 'prop-types'
-import MemberPrivileges from './member-privileges'
-import Icon from '@/shared/components/icon'
-import OLRow from '@/features/ui/components/ol/ol-row'
-import OLCol from '@/features/ui/components/ol/ol-col'
-import MaterialIcon from '@/shared/components/material-icon'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-
-export default function ViewMember({ member }) {
- return (
-
-
-
-
}
- bs5={
}
- />
-
{member.email}
-
-
-
-
-
-
- )
-}
-
-ViewMember.propTypes = {
- member: PropTypes.shape({
- _id: PropTypes.string.isRequired,
- email: PropTypes.string.isRequired,
- privileges: PropTypes.string.isRequired,
- }).isRequired,
-}
diff --git a/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx
index b13581894e..3ccd72d091 100644
--- a/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx
@@ -5,7 +5,7 @@ import { matchSorter } from 'match-sorter'
import { useCombobox } from 'downshift'
import classnames from 'classnames'
-import Icon from '../../../shared/components/icon'
+import Icon from '@/shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import Tag from '@/features/ui/components/bootstrap-5/tag'
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
@@ -155,11 +155,13 @@ export default function SelectCollaborators({
}, [reset, isOpen, filteredOptions.length])
return (
-
+
{/* eslint-disable-next-line jsx-a11y/label-has-for */}