From 816f8c45eb7d57913f71f595b25b08dad1805cca Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:08:01 +0300 Subject: [PATCH] Merge pull request #32924 from overleaf/ii-share-modal-basic-layout [web] Share modal redesign base layout GitOrigin-RevId: 0f011d03762c6669a0fd3b1fc2af32c9026c7ea4 --- .../Features/Project/ProjectController.mjs | 1 + .../web/frontend/extracted-translations.json | 15 + ...alSymbolsRoundedUnfilledPartialSlice.woff2 | Bin 8876 -> 9952 bytes .../material-symbols/unfilled-symbols.mjs | 7 + .../components/error-message.tsx | 33 +++ .../components/invited-people.tsx | 7 + .../components/link-sharing.tsx | 131 ++++---- .../components/project-access.tsx | 280 ++++++++++++++++++ .../components/remove-sharing-links-modal.tsx | 57 ++++ .../components/send-invites.tsx | 14 +- .../components/share-modal-body.tsx | 97 ++++-- .../share-project-modal-content.tsx | 107 ++++--- .../components/share-project-modal-row.tsx | 11 + .../components/share-project-modal.tsx | 41 ++- .../dropdown/dropdown-list-item.tsx | 8 +- .../stylesheets/components/dropdown-menu.scss | 8 + .../stylesheets/pages/editor/share.scss | 37 ++- services/web/locales/en.json | 15 + 18 files changed, 747 insertions(+), 122 deletions(-) create mode 100644 services/web/frontend/js/features/share-project-modal/components/error-message.tsx create mode 100644 services/web/frontend/js/features/share-project-modal/components/invited-people.tsx create mode 100644 services/web/frontend/js/features/share-project-modal/components/project-access.tsx create mode 100644 services/web/frontend/js/features/share-project-modal/components/remove-sharing-links-modal.tsx create mode 100644 services/web/frontend/js/features/share-project-modal/components/share-project-modal-row.tsx diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index 50b3500556..75bb479fa1 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -481,6 +481,7 @@ const _ProjectController = { 'editor-tabs', 'overleaf-code', 'export-docx', + 'sharing-updates', ].filter(Boolean) const getUserValues = async userId => diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 5d02d14091..457c09d5fc 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -51,6 +51,7 @@ "access_denied": "", "access_edit_your_projects": "", "access_levels_changed": "", + "access_updated": "", "account_associated_with_x": "", "account_billed_manually": "", "account_has_been_link_to_group_account": "", @@ -150,8 +151,10 @@ "anonymous": "", "anyone": "", "anyone_in_x": "", + "anyone_in_x_with_the_link": "", "anyone_with_link_can_edit": "", "anyone_with_link_can_view": "", + "anyone_with_the_link": "", "app_on_x": "", "appearance": "", "apply": "", @@ -1097,6 +1100,7 @@ "make_owner": "", "make_primary": "", "make_private": "", + "manage_access": "", "manage_beta_program_membership": "", "manage_email_preferences": "", "manage_files_from_your_dropbox_folder": "", @@ -1245,6 +1249,7 @@ "not_captured": "", "not_managed": "", "not_now": "", + "not_permitted_by_your_organization": "", "notification": "", "notification_personal_and_group_subscriptions": "", "notification_project_invite_accepted_message": "", @@ -1268,6 +1273,7 @@ "only_group_admin_or_managers_can_delete_your_account_7": "", "only_group_admin_or_managers_can_delete_your_account_9": "", "only_importer_can_refresh": "", + "only_invited_people": "", "only_project_owner_can_link_github": "", "open_action_menu": "", "open_advanced_reference_search": "", @@ -1426,6 +1432,7 @@ "processing_uppercase": "", "professional": "", "progress_bar_percentage": "", + "project_access": "", "project_actions": "", "project_approaching_file_limit": "", "project_categories_tags": "", @@ -1539,6 +1546,8 @@ "remove_link": "", "remove_manager": "", "remove_or_replace_figure": "", + "remove_original_links": "", + "remove_original_sharing_links": "", "remove_secondary_email_addresses": "", "remove_secondary_email_addresses_except_saml_email": "", "remove_sso_login_option": "", @@ -1722,6 +1731,7 @@ "share": "", "share_feedback": "", "share_project": "", + "share_project_name": "", "shared_with_you": "", "sharelatex_beta_program": "", "sharing_permissions": "", @@ -1970,6 +1980,8 @@ "this_action_cannot_be_undone": "", "this_action_will_also_disable_domain_capture": "", "this_address_will_be_shown_on_the_invoice": "", + "this_change_will_permanently_remove_your_original_links_you_can_still_share_new_link": "", + "this_change_will_permanently_remove_your_original_links_you_need_to_share_the_new_link": "", "this_could_be_because_we_cant_support_some_elements_of_the_table": "", "this_field_is_required": "", "this_grants_access_to_features_2": "", @@ -2233,6 +2245,7 @@ "verify_email_address_before_enabling_managed_users": "", "verify_your_email_address": "", "verifying_domain": "", + "via_sharing_links_legacy": "", "view": "", "view_all": "", "view_all_plans": "", @@ -2315,6 +2328,8 @@ "x_changes_in": "", "x_changes_in_plural": "", "x_libraries_accessed_in_this_project": "", + "x_people_invited": "", + "x_people_invited_plural": "", "x_price_for_first_month": "", "x_price_for_first_year": "", "x_price_for_y_months": "", diff --git a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 index 4ca1d4db78389ee0cedb14df734362e291892670..ae0890e9ebf192023cff6b0a08e658067a3bf5b7 100644 GIT binary patch literal 9952 zcmV<6CLh^%Pew8T0RR9104Cr74gdfE094Qb049Y10RR9100000000000000000000 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 literal 8876 zcmV;dB2(RWPew8T0RR9103xga4gdfE08X#~03uKT0RR9100000000000000000000 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 { + 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')} + } +} + +export default ErrorMessage diff --git a/services/web/frontend/js/features/share-project-modal/components/invited-people.tsx b/services/web/frontend/js/features/share-project-modal/components/invited-people.tsx new file mode 100644 index 0000000000..9d1308f3b6 --- /dev/null +++ b/services/web/frontend/js/features/share-project-modal/components/invited-people.tsx @@ -0,0 +1,7 @@ +type InvitedPeopleProps = object + +function InvitedPeople(_props: InvitedPeopleProps) { + return null +} + +export default InvitedPeople diff --git a/services/web/frontend/js/features/share-project-modal/components/link-sharing.tsx b/services/web/frontend/js/features/share-project-modal/components/link-sharing.tsx index a5596cd418..3ec1e70a25 100644 --- a/services/web/frontend/js/features/share-project-modal/components/link-sharing.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/link-sharing.tsx @@ -16,6 +16,7 @@ import OLCol from '@/shared/components/ol/ol-col' import OLButton from '@/shared/components/ol/ol-button' import OLTooltip from '@/shared/components/ol/ol-tooltip' import MaterialIcon from '@/shared/components/material-icon' +import { useFeatureFlag } from '@/shared/context/split-test-context' type Tokens = { readAndWrite: string @@ -28,12 +29,13 @@ type Tokens = { type AccessLevel = 'private' | 'tokenBased' | 'readAndWrite' | 'readOnly' export default function LinkSharing() { + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') const [inflight, setInflight] = useState(false) const [showLinks, setShowLinks] = useState(true) const linkSharingEnabled = getMeta('ol-capabilities')?.includes('link-sharing') - const { monitorRequest } = useShareProjectContext() + const { monitorRequest, projectAccess } = useShareProjectContext() const { projectId, project } = useProjectContext() const { publicAccessLevel } = project || {} @@ -64,6 +66,13 @@ export default function LinkSharing() { return null } + if (isSharingUpdatesEnabled) { + if (projectAccess === 'linkSharing') { + return + } + return null + } + switch (publicAccessLevel) { // Private (with token-access available) case 'private': @@ -140,6 +149,20 @@ function PrivateSharing({ ) } +function useProjectTokens() { + const { 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 tokens +} + function TokenBasedSharing({ setAccessLevel, inflight, @@ -152,17 +175,7 @@ function TokenBasedSharing({ showLinks: boolean }) { const { t } = useTranslation() - const { 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]) + const tokens = useProjectTokens() return ( @@ -191,24 +204,8 @@ function TokenBasedSharing({ {showLinks && ( -
- {t('anyone_with_link_can_edit')} - -
-
- {t('anyone_with_link_can_view')} - -
+ +
)}
@@ -249,32 +246,26 @@ function LegacySharing({ ) } -export function ReadOnlyTokenLink() { - const { t } = useTranslation() - const { 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]) +export function ReadAndWriteTokenLinks() { + const tokens = useProjectTokens() return ( -
- {t('anyone_with_link_can_view')} - -
+ + +
+
+ ) +} + +export function ReadOnlyTokenLink() { + const tokens = useProjectTokens() + + return ( + + + ) @@ -336,3 +327,39 @@ function LinkSharingInfo() { ) } + +function AccessTokenEditDisplayArea({ tokens }: { tokens: Tokens | null }) { + const { t } = useTranslation() + + return ( +
+ + {t('anyone_with_link_can_edit')} + + +
+ ) +} + +function AccessTokenViewDisplayArea({ tokens }: { tokens: Tokens | null }) { + const { t } = useTranslation() + + return ( +
+ + {t('anyone_with_link_can_view')} + + +
+ ) +} diff --git a/services/web/frontend/js/features/share-project-modal/components/project-access.tsx b/services/web/frontend/js/features/share-project-modal/components/project-access.tsx new file mode 100644 index 0000000000..4be98572ab --- /dev/null +++ b/services/web/frontend/js/features/share-project-modal/components/project-access.tsx @@ -0,0 +1,280 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import ShareProjectModalRow from '@/features/share-project-modal/components/share-project-modal-row' +import MaterialIcon from '@/shared/components/material-icon' +import OLButton from '@/shared/components/ol/ol-button' +import OLTooltip from '@/shared/components/ol/ol-tooltip' +import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item' +import { + Dropdown, + DropdownDivider, + DropdownItem, + DropdownMenu, + DropdownToggle, +} from '@/shared/components/dropdown/dropdown-menu' +import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item' +import LinkSharing from '@/features/share-project-modal/components/link-sharing' +import { useEditorContext } from '@/shared/context/editor-context' +import { PermissionsLevel } from '@/features/ide-react/types/permissions' +import MemberPrivileges from '@/features/share-project-modal/components/member-privileges' +import RemoveSharingLinksModal from '@/features/share-project-modal/components/remove-sharing-links-modal' +import { + ProjectAccessType, + useShareProjectContext, +} from '@/features/share-project-modal/components/share-project-modal' + +type ProjectAccessProps = { + setIsInvitedPeopleScreen: React.Dispatch> + invitedPeopleCount: number +} + +export type PendingAccessType = Exclude + +function ProjectAccess({ + setIsInvitedPeopleScreen, + invitedPeopleCount, +}: ProjectAccessProps) { + const { t } = useTranslation() + const [pendingAccess, setPendingAccess] = useState( + null + ) + // TODO set initial state + const [privileges, setPrivileges] = + useState>('readOnly') + const { isProjectOwner } = useEditorContext() + // TODO set company name + const companyName = 'XYZ' + + const { + monitorRequest, + setSuccessActionMessage, + projectAccess, + setProjectAccess, + } = useShareProjectContext() + + const handleAccessChange = (newAccess: PendingAccessType) => { + setPendingAccess(null) + + monitorRequest( + () => + // TODO: replace with real API call + new Promise(resolve => setTimeout(resolve, 1000)) + ).then(() => { + setProjectAccess(newAccess) + setSuccessActionMessage(t('access_updated')) + }) + } + + const onAccessSelect = (eventKey: ProjectAccessType) => { + if (projectAccess === 'linkSharing' && eventKey !== 'linkSharing') { + // Legacy link sharing: show confirmation first + setPendingAccess(eventKey as PendingAccessType) + } else { + // Non-legacy: fire request directly + handleAccessChange(eventKey as PendingAccessType) + } + } + + const onPrivilegesChange = (eventKey: Exclude) => { + monitorRequest( + () => + // TODO: replace with real API call + new Promise(resolve => setTimeout(resolve, 1000)) + ).then(() => { + setPrivileges(eventKey) + setSuccessActionMessage(t('access_updated')) + }) + } + + const getProjectAccessDropdownToggleText = () => { + switch (projectAccess) { + case 'linkSharing': + return t('via_sharing_links_legacy') + case 'onlyInvitedPeople': + return t('only_invited_people') + case 'anyoneInXyzWithTheLink': + return t('anyone_in_x_with_the_link', { companyName }) + case 'anyoneWithTheLink': + return t('anyone_with_the_link') + default: + return '' + } + } + + return ( + <> +

{t('project_access')}

+ +
+ +
+ {t('x_people_invited', { count: invitedPeopleCount })} +
+
+ setIsInvitedPeopleScreen(true)} + > + {t('manage_access')} + +
+ {projectAccess && ( + +
+ {projectAccess === 'linkSharing' && } + {projectAccess === 'onlyInvitedPeople' && ( + + )} + {projectAccess === 'anyoneInXyzWithTheLink' && ( + + )} + {projectAccess === 'anyoneWithTheLink' && ( + + )} + + + {getProjectAccessDropdownToggleText()} + + + + {projectAccess === 'linkSharing' && ( + <> + + } + trailingIcon={ + projectAccess === 'linkSharing' ? 'check' : undefined + } + active={projectAccess === 'linkSharing'} + > + {t('via_sharing_links_legacy')} + + + + + )} + + } + trailingIcon={ + projectAccess === 'onlyInvitedPeople' + ? 'check' + : undefined + } + active={projectAccess === 'onlyInvitedPeople'} + > + {t('only_invited_people')} + + + + } + trailingIcon={ + projectAccess === 'anyoneInXyzWithTheLink' + ? 'check' + : undefined + } + active={projectAccess === 'anyoneInXyzWithTheLink'} + > + {t('anyone_in_x_with_the_link', { companyName })} + + + + } + trailingIcon={ + projectAccess === 'anyoneWithTheLink' + ? 'check' + : undefined + } + active={projectAccess === 'anyoneWithTheLink'} + > + {t('anyone_with_the_link')} + + + + + + + + + + {pendingAccess && ( + setPendingAccess(null)} + onConfirm={() => handleAccessChange(pendingAccess)} + /> + )} +
+ {projectAccess !== 'linkSharing' && ( + + + + + + + } + active={privileges === 'readAndWrite'} + trailingIcon={ + privileges === 'readAndWrite' ? 'check' : undefined + } + > + {t('editor')} + + } + active={privileges === 'review'} + trailingIcon={privileges === 'review' ? 'check' : undefined} + > + {t('reviewer')} + + } + active={privileges === 'readOnly'} + trailingIcon={privileges === 'readOnly' ? 'check' : undefined} + > + {t('viewer')} + + + + )} +
+ )} + + {isProjectOwner && } + + ) +} + +export default ProjectAccess diff --git a/services/web/frontend/js/features/share-project-modal/components/remove-sharing-links-modal.tsx b/services/web/frontend/js/features/share-project-modal/components/remove-sharing-links-modal.tsx new file mode 100644 index 0000000000..0eb1489a14 --- /dev/null +++ b/services/web/frontend/js/features/share-project-modal/components/remove-sharing-links-modal.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from 'react-i18next' +import { + OLModal, + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/shared/components/ol/ol-modal' +import OLButton from '@/shared/components/ol/ol-button' +import { PendingAccessType } from '@/features/share-project-modal/components/project-access' + +type RemoveSharingLinksModalProps = { + pendingAccess: PendingAccessType + onCancel: () => void + onConfirm: () => void +} + +function RemoveSharingLinksModal({ + pendingAccess, + onCancel, + onConfirm, +}: RemoveSharingLinksModalProps) { + const { t } = useTranslation() + + let confirmationModalBodyText = null + if (pendingAccess === 'onlyInvitedPeople') { + confirmationModalBodyText = t( + 'this_change_will_permanently_remove_your_original_links_you_can_still_share_new_link' + ) + } else if ( + pendingAccess === 'anyoneInXyzWithTheLink' || + pendingAccess === 'anyoneWithTheLink' + ) { + confirmationModalBodyText = t( + 'this_change_will_permanently_remove_your_original_links_you_need_to_share_the_new_link' + ) + } + + return ( + + + {t('remove_original_sharing_links')} + + {confirmationModalBodyText} + + + {t('cancel')} + + + {t('remove_original_links')} + + + + ) +} + +export default RemoveSharingLinksModal diff --git a/services/web/frontend/js/features/share-project-modal/components/send-invites.tsx b/services/web/frontend/js/features/share-project-modal/components/send-invites.tsx index 41e831af2d..089544ddf0 100644 --- a/services/web/frontend/js/features/share-project-modal/components/send-invites.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/send-invites.tsx @@ -3,6 +3,8 @@ import AddCollaboratorsUpgrade from './add-collaborators-upgrade' import CollaboratorsLimitUpgrade from './collaborators-limit-upgrade' import AccessLevelsChanged from './access-levels-changed' import OLRow from '@/shared/components/ol/ol-row' +import classnames from 'classnames' +import { useFeatureFlag } from '@/shared/context/split-test-context' export default function SendInvites({ canAddCollaborators, @@ -15,8 +17,18 @@ export default function SendInvites({ haveAnyEditorsBeenDowngraded: boolean somePendingEditorsResolved: boolean }) { + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') + + if (isSharingUpdatesEnabled) { + return null + } + return ( - + {hasExceededCollaboratorLimit && !haveAnyEditorsBeenDowngraded && ( )} diff --git a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx index 4f74d1d5b5..9a70aa1f22 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx @@ -10,11 +10,27 @@ import { useProjectContext } from '@/shared/context/project-context' import { useMemo } from 'react' import RecaptchaConditions from '@/shared/components/recaptcha-conditions' import getMeta from '@/utils/meta' +import { useFeatureFlag } from '@/shared/context/split-test-context' +import OLNotification from '@/shared/components/ol/ol-notification' +import ErrorMessage from '@/features/share-project-modal/components/error-message' +import ProjectAccess from '@/features/share-project-modal/components/project-access' +import InvitedPeople from '@/features/share-project-modal/components/invited-people' -export default function ShareModalBody() { +type ShareModalBodyProps = { + isInvitedPeopleScreen: boolean + setIsInvitedPeopleScreen: React.Dispatch> + error?: string +} + +export default function ShareModalBody({ + isInvitedPeopleScreen, + setIsInvitedPeopleScreen, + error, +}: ShareModalBodyProps) { const { project, features } = useProjectContext() const { members, invites } = project || {} const { isProjectOwner } = useEditorContext() + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') // whether the project has not reached the collaborator limit const canAddCollaborators = useMemo(() => { @@ -108,37 +124,62 @@ export default function ShareModalBody() { ) : ( )} - {isProjectOwner && } - + {isSharingUpdatesEnabled ? ( + <> + {error && ( + } + /> + )} + {isInvitedPeopleScreen || !isProjectOwner ? ( + + ) : ( + + )} + + ) : ( + <> + {isProjectOwner && } - {sortedMembers.map(member => - isProjectOwner ? ( - - ) : ( - - ) + + + {sortedMembers.map(member => + isProjectOwner ? ( + + ) : ( + + ) + )} + + {(invites || []).map(invite => ( + + ))} + )} - {(invites || []).map(invite => ( - - ))} - {!getMeta('ol-ExposedSettings').recaptchaDisabled?.invite && ( )} diff --git a/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx b/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx index 7ef3f601b2..a45b21f3c4 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' import { useEditorContext } from '@/shared/context/editor-context' -import { lazy, Suspense } from 'react' +import { lazy, Suspense, useState } from 'react' import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner' import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer' import { @@ -13,6 +13,11 @@ import { import OLNotification from '@/shared/components/ol/ol-notification' import OLButton from '@/shared/components/ol/ol-button' import OLSpinner from '@/shared/components/ol/ol-spinner' +import MaterialIcon from '@/shared/components/material-icon' +import ErrorMessage from '@/features/share-project-modal/components/error-message' +import classNames from 'classnames' +import { useFeatureFlag } from '@/shared/context/split-test-context' +import { useShareProjectContext } from '@/features/share-project-modal/components/share-project-modal' const ReadOnlyTokenLink = lazy(() => import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({ @@ -29,6 +34,7 @@ type ShareProjectModalContentProps = { animation: boolean inFlight: boolean error: string | undefined + projectName?: string } export default function ShareProjectModalContent({ @@ -37,27 +43,59 @@ export default function ShareProjectModalContent({ animation, inFlight, error, + projectName, }: ShareProjectModalContentProps) { const { t } = useTranslation() + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') + const [isInvitedPeopleScreen, setIsInvitedPeopleScreen] = useState(false) + const { successActionMessage } = useShareProjectContext() const { isRestrictedTokenMember } = useEditorContext() return ( - {t('share_project')} +
+ {isSharingUpdatesEnabled && isInvitedPeopleScreen ? ( + setIsInvitedPeopleScreen(false)} + leadingIcon="arrow_back_ios_new" + > + {t('back')} + + ) : ( + + {isSharingUpdatesEnabled && projectName + ? t('share_project_name', { projectName }) + : t('share_project')} + + )} +
- -
+ +
}> {isRestrictedTokenMember ? ( ) : ( - + )} - {error && ( + {!isSharingUpdatesEnabled && error && ( } @@ -68,12 +106,36 @@ export default function ShareProjectModalContent({ -
{inFlight && }
+
+ {isSharingUpdatesEnabled ? ( + <> + {successActionMessage && ( +
+
+ + {successActionMessage} +
+
+ )} + + ) : ( + inFlight && + )} +
{t('close')} @@ -82,32 +144,3 @@ export default function ShareProjectModalContent({ ) } - -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/share-project-modal-row.tsx b/services/web/frontend/js/features/share-project-modal/components/share-project-modal-row.tsx new file mode 100644 index 0000000000..ff1ed50ead --- /dev/null +++ b/services/web/frontend/js/features/share-project-modal/components/share-project-modal-row.tsx @@ -0,0 +1,11 @@ +const ShareProjectModalRow: React.FC = ({ + children, +}) => { + return ( +
+ {children} +
+ ) +} + +export default ShareProjectModalRow diff --git a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx index 6dabc9f9d8..cade5e8077 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx @@ -12,7 +12,13 @@ import { sendMB } from '@/infrastructure/event-tracking' import { useEditorContext } from '@/shared/context/editor-context' import customLocalStorage from '@/infrastructure/local-storage' -type ShareProjectContextValue = { +export type ProjectAccessType = + | 'linkSharing' + | 'onlyInvitedPeople' + | 'anyoneInXyzWithTheLink' + | 'anyoneWithTheLink' + +export type ShareProjectContextValue = { monitorRequest: >(request: () => T) => T inFlight: boolean setInFlight: React.Dispatch< @@ -22,6 +28,14 @@ type ShareProjectContextValue = { setError: React.Dispatch< React.SetStateAction > + successActionMessage: string | undefined + setSuccessActionMessage: React.Dispatch< + React.SetStateAction + > + projectAccess: ProjectAccessType | undefined + setProjectAccess: React.Dispatch< + React.SetStateAction + > } const SHOW_MODAL_COOLDOWN_PERIOD = 24 * 60 * 60 * 1000 // 24 hours @@ -58,9 +72,28 @@ const ShareProjectModal = React.memo(function ShareProjectModal({ const [inFlight, setInFlight] = useState(false) const [error, setError] = useState() + const [projectAccess, setProjectAccess] = useState< + ProjectAccessType | undefined + >() + const [successActionMessage, setSuccessActionMessage] = useState< + string | undefined + >() const { project, projectId } = useProjectContext() const { isProjectOwner } = useEditorContext() + const { publicAccessLevel } = project || {} + + // TODO: handle initial state for projectAccess + useEffect(() => { + if (!projectAccess) { + if (publicAccessLevel === 'tokenBased') { + // consider a legacy link sharing is enabled + setProjectAccess('linkSharing') + } else { + setProjectAccess('onlyInvitedPeople') + } + } + }, [projectAccess, publicAccessLevel]) const { splitTestVariants } = useSplitTestContext() @@ -120,6 +153,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({ const cancel = useCallback(() => { if (!inFlight) { handleHide() + setSuccessActionMessage(undefined) } }, [handleHide, inFlight]) @@ -157,6 +191,10 @@ const ShareProjectModal = React.memo(function ShareProjectModal({ setInFlight, error, setError, + successActionMessage, + setSuccessActionMessage, + projectAccess, + setProjectAccess, }} > ) diff --git a/services/web/frontend/js/shared/components/dropdown/dropdown-list-item.tsx b/services/web/frontend/js/shared/components/dropdown/dropdown-list-item.tsx index 3b492faca3..f5ab85202a 100644 --- a/services/web/frontend/js/shared/components/dropdown/dropdown-list-item.tsx +++ b/services/web/frontend/js/shared/components/dropdown/dropdown-list-item.tsx @@ -1,9 +1,15 @@ import { ReactNode } from 'react' export default function DropdownListItem({ + className, children, }: { + className?: string children: ReactNode }) { - return
  • {children}
  • + return ( +
  • + {children} +
  • + ) } diff --git a/services/web/frontend/stylesheets/components/dropdown-menu.scss b/services/web/frontend/stylesheets/components/dropdown-menu.scss index 6c53042d21..aa2161375a 100644 --- a/services/web/frontend/stylesheets/components/dropdown-menu.scss +++ b/services/web/frontend/stylesheets/components/dropdown-menu.scss @@ -294,3 +294,11 @@ $dropdown-item-min-height: 36px; .dropdown-item-wide { min-width: 264px; } + +.dropdown-toggle { + &.no-default-caret { + &::after { + content: none !important; + } + } +} diff --git a/services/web/frontend/stylesheets/pages/editor/share.scss b/services/web/frontend/stylesheets/pages/editor/share.scss index abd0fb7359..f9acbb6a68 100644 --- a/services/web/frontend/stylesheets/pages/editor/share.scss +++ b/services/web/frontend/stylesheets/pages/editor/share.scss @@ -1,5 +1,5 @@ .modal-body-share { - h3 { + &:not(.modal-redesign) h3 { border-bottom: 1px solid var(--neutral-30); padding-bottom: calc(var(--line-height-03) / 4); margin: 0; @@ -159,6 +159,29 @@ } } } + + &.modal-redesign { + .public-access-level { + margin-top: 0; + padding-top: 0; + padding-bottom: 0; + + .access-token-display-area { + margin-top: 0; + } + } + + .access-token-wrapper-title { + color: var(--content-primary) !important; + font-weight: 400; + } + + .access-token { + code { + font-weight: 500; + } + } + } } .copy-button:focus-within { @@ -199,12 +222,22 @@ .add-collaborator-controls { display: flex; - align-items: center; justify-content: flex-end; gap: var(--spacing-03); .select-wrapper { max-width: 130px; } + + &:not(.add-collaborator-controls-legacy) .dropdown-menu { + left: auto; + right: 0; + } + } + + .form-control-lg { + min-height: initial; + padding-top: calc(#{$input-padding-y-lg} - #{$input-border-width}); + padding-bottom: calc(#{$input-padding-y-lg} - #{$input-border-width}); } } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index c84fb06ade..f03b253775 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -56,6 +56,7 @@ "access_denied": "Access Denied", "access_edit_your_projects": "Access and edit your projects", "access_levels_changed": "Access levels changed", + "access_updated": "Access updated.", "account": "Account", "account_already_managed": "Your account is already managed.", "account_associated_with_x": "Your account is associated with __companyName__.", @@ -197,8 +198,10 @@ "anonymous": "Anonymous", "anyone": "Anyone", "anyone_in_x": "Anyone in __groupName__", + "anyone_in_x_with_the_link": "Anyone in __companyName__ with the link", "anyone_with_link_can_edit": "Anyone with this link can edit this project", "anyone_with_link_can_view": "Anyone with this link can view this project", + "anyone_with_the_link": "Anyone with the link", "app_on_x": "__appName__ on __social__", "appearance": "Appearance", "apply": "Apply ", @@ -1461,6 +1464,7 @@ "make_owner": "Make owner", "make_primary": "Make primary", "make_private": "Make private", + "manage_access": "Manage access", "manage_beta_program_membership": "Manage beta program membership", "manage_email_preferences": "Manage email preferences", "manage_files_from_your_dropbox_folder": "Manage files from your Dropbox folder", @@ -1646,6 +1650,7 @@ "not_found_error_from_the_supplied_url": "The link to open this content on Overleaf pointed to a file that could not be found. If this keeps happening for links on a particular site, please report this to them.", "not_managed": "Not managed", "not_now": "Not now", + "not_permitted_by_your_organization": "Not permitted by your organization", "not_registered": "Not registered", "note_features_under_development": "<0>Please note that features in this program are still being tested and actively developed. This means that they might <0>change, be <0>removed or <0>become part of a premium plan", "notification": "Notification", @@ -1687,6 +1692,7 @@ "only_group_admin_or_managers_can_delete_your_account_7": "Only your group admin or group managers will be able to delete your account or change your account into an unmanaged account.", "only_group_admin_or_managers_can_delete_your_account_9": "Once you have become a managed user, <0>you yourself cannot change it back to an unmanaged account. <1>Learn more about managed Overleaf accounts.", "only_importer_can_refresh": "Only the person who originally imported this __provider__ file can refresh it.", + "only_invited_people": "Only invited people", "only_project_owner_can_link_github": "Only the project owner can link this project to a GitHub repository. Please contact the project owner if you would like to set this up.", "open_action_menu": "Open __name__ action menu", "open_advanced_reference_search": "Open advanced reference search", @@ -1914,6 +1920,7 @@ "professional": "Professional", "progress_bar_percentage": "Progress bar from 0 to 100%", "project": "project", + "project_access": "Project access", "project_actions": "Project actions", "project_approaching_file_limit": "This project is approaching the file limit", "project_categories_tags": "Project categories and tags", @@ -2048,6 +2055,8 @@ "remove_link": "Remove link", "remove_manager": "Remove manager", "remove_or_replace_figure": "Remove or replace figure", + "remove_original_links": "Remove original links", + "remove_original_sharing_links": "Remove original sharing links?", "remove_secondary_email_addresses": "Remove any secondary email addresses associated with your account. <0>Remove them in account settings.", "remove_secondary_email_addresses_except_saml_email": "Remove all email addresses except <0>__institutionEmail__ from your Overleaf account. <1>Remove them in account settings.", "remove_sso_login_option": "Remove the SSO login option for your users.", @@ -2272,6 +2281,7 @@ "share": "Share", "share_feedback": "Share feedback", "share_project": "Share Project", + "share_project_name": "Share __projectName__", "shared_with_you": "Shared with you", "sharelatex_beta_program": "__appName__ beta program", "sharing_permissions": "Sharing permissions", @@ -2587,6 +2597,8 @@ "this_action_cannot_be_undone": "This action cannot be undone.", "this_action_will_also_disable_domain_capture": "This action will also disable domain capture.", "this_address_will_be_shown_on_the_invoice": "This address will be shown on the invoice", + "this_change_will_permanently_remove_your_original_links_you_can_still_share_new_link": "This change will permanently remove your original sharing links. You can still share a new link to the project when you choose a relevant sharing option.", + "this_change_will_permanently_remove_your_original_links_you_need_to_share_the_new_link": "This change will permanently remove your original sharing links. You’ll need to share the new link instead.", "this_could_be_because_we_cant_support_some_elements_of_the_table": "This could be because we can’t yet support some elements of the table in the table preview. Or there may be an error in the table’s LaTeX code.", "this_field_is_required": "This field is required", "this_grants_access_to_features_2": "This grants you access to <0>__appName__ <0>__featureType__ features.", @@ -2881,6 +2893,7 @@ "verify_email_address_before_enabling_managed_users": "You need to verify your email address before enabling managed users.", "verify_your_email_address": "Verify your email address", "verifying_domain": "Verifying domain", + "via_sharing_links_legacy": "Via sharing links (legacy)", "via_support": "via support", "view": "View", "view_all": "View all", @@ -2975,6 +2988,8 @@ "x_changes_in": "__count__ change in", "x_changes_in_plural": "__count__ changes in", "x_libraries_accessed_in_this_project": "__provider__ libraries accessed in this project", + "x_people_invited": "__count__ person invited", + "x_people_invited_plural": "__count__ people invited", "x_price_for_first_month": "<0>__price__ for your first month", "x_price_for_first_year": "<0>__price__ for your first year", "x_price_for_y_months": "<0>__price__ for your first __discountMonths__ months",