From c9e4ab8b992fa0df355de35ce3bd7693f00acd8d Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:04:49 +0300 Subject: [PATCH] Merge pull request #33166 from overleaf/ii-share-modal-copy-link [web] Share modal copy sharing link button GitOrigin-RevId: a95f879196528ec3bc998558a6838c52aac33069 --- .../web/frontend/extracted-translations.json | 2 + .../components/copy-sharing-link-button.tsx | 48 +++++++++++++ .../components/project-access.tsx | 2 +- .../share-project-modal-content.tsx | 9 ++- services/web/locales/en.json | 2 + .../components/share-project-modal.test.tsx | 70 +++++++++++++++++++ 6 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 services/web/frontend/js/features/share-project-modal/components/copy-sharing-link-button.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 3cb4acae63..9830089d88 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -387,6 +387,7 @@ "copy": "", "copy_project": "", "copy_response": "", + "copy_sharing_link": "", "copying": "", "cost_summary": "", "could_not_save_reference_to_bib_file": "", @@ -1126,6 +1127,7 @@ "link_account": "", "link_accounts": "", "link_accounts_and_add_email": "", + "link_copied": "", "link_institutional_email_get_started": "", "link_sharing": "", "link_sharing_is_off_short": "", diff --git a/services/web/frontend/js/features/share-project-modal/components/copy-sharing-link-button.tsx b/services/web/frontend/js/features/share-project-modal/components/copy-sharing-link-button.tsx new file mode 100644 index 0000000000..ef5e06df10 --- /dev/null +++ b/services/web/frontend/js/features/share-project-modal/components/copy-sharing-link-button.tsx @@ -0,0 +1,48 @@ +import { useTranslation } from 'react-i18next' +import OLButton from '@/shared/components/ol/ol-button' +import { useShareProjectContext } from '@/features/share-project-modal/components/share-project-modal' +import { useProjectContext } from '@/shared/context/project-context' +import { useUserContext } from '@/shared/context/user-context' +import { debugConsole } from '@/utils/debugging' +import getMeta from '@/utils/meta' + +export default function CopySharingLinkButton() { + const { t } = useTranslation() + const { sharingLinkData, projectAccess, setSuccessActionMessage } = + useShareProjectContext() + const { projectId } = useProjectContext() + const { isAdmin } = useUserContext() + + const isCopyBtnEnabled = + Boolean(navigator.clipboard?.writeText) && + Boolean(sharingLinkData?.token) && + (projectAccess === 'anyoneInXyzWithTheLink' || + projectAccess === 'anyoneWithTheLink') + + const handleCopyClick = () => { + if (!sharingLinkData?.token) { + return + } + + const origin = isAdmin + ? getMeta('ol-ExposedSettings').siteUrl + : window.location.origin + const link = `${origin}/project/${projectId}/share#${sharingLinkData.token}` + + navigator.clipboard + .writeText(link) + .then(() => setSuccessActionMessage(t('link_copied'))) + .catch(debugConsole.error) + } + + return ( + + {t('copy_sharing_link')} + + ) +} 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 index 148c7d7b7e..5a22a93dfd 100644 --- 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 @@ -92,7 +92,7 @@ function ProjectAccess({ return data }).then(data => { - setSharingLinkData(newAccess === 'onlyInvitedPeople' ? null : data) + setSharingLinkData(data) setProjectAccess(newAccess) setSuccessActionMessage(t('access_updated')) }) 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 ade755afea..95a60cd71a 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 @@ -14,6 +14,7 @@ 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 CopySharingLinkButton from '@/features/share-project-modal/components/copy-sharing-link-button' import ErrorMessage from '@/features/share-project-modal/components/error-message' import GiveFeedbackLink from '@/features/share-project-modal/components/give-feedback-link' import classNames from 'classnames' @@ -74,7 +75,7 @@ function ShareProjectModalContentInner({ const { t } = useTranslation() const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') const [isInvitedPeopleScreen, setIsInvitedPeopleScreen] = useState(false) - const { successActionMessage } = useShareProjectContext() + const { successActionMessage, projectAccess } = useShareProjectContext() const { isRestrictedTokenMember, isProjectOwner } = useEditorContext() return ( @@ -135,6 +136,12 @@ function ShareProjectModalContentInner({
{isSharingUpdatesEnabled ? ( <> + {!isInvitedPeopleScreen && + (projectAccess === 'onlyInvitedPeople' || + projectAccess === 'anyoneInXyzWithTheLink' || + projectAccess === 'anyoneWithTheLink') && ( + + )} {successActionMessage && (
', function () { }) }) + afterEach(function () { + window.metaAttributesCache.delete('ol-splitTestVariants') + }) + it('sets "Via sharing links (legacy)" when `publicAccessLevel` is `tokenBased`', async function () { fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {}) @@ -1060,6 +1064,72 @@ describe('', function () { await screen.findByText('Anyone in your group with the link') }) + + describe('copy link button', function () { + let clipboardWriteTextStub: sinon.SinonStub + + beforeEach(function () { + clipboardWriteTextStub = sinon.stub().resolves() + Object.defineProperty(navigator, 'clipboard', { + value: { writeText: clipboardWriteTextStub }, + configurable: true, + writable: true, + }) + }) + + afterEach(function () { + window.metaAttributesCache.delete('ol-splitTestVariants') + delete (navigator as any).clipboard + }) + + it('shows a disabled copy sharing link button when access is "Only invited people"', async function () { + fetchMock.get('express:/project/:projectId/sharing-link', 404) + + renderWithEditorContext( + , + createContextProps() + ) + + const copyButton: HTMLButtonElement = await screen.findByRole( + 'button', + { + name: /copy sharing link/i, + } + ) + expect(copyButton.disabled).to.be.true + }) + + it('enables the copy sharing link button when access is "Anyone with the link" and copies the correct URL on click', async function () { + const sharingLinkToken = 'abc123token' + fetchMock.get('express:/project/:projectId/sharing-link', { + _id: 'invite-id', + token: sharingLinkToken, + privileges: 'readAndWrite', + }) + + renderWithEditorContext( + , + createContextProps() + ) + + const copyButton: HTMLButtonElement = await screen.findByRole( + 'button', + { + name: /copy sharing link/i, + } + ) + expect(copyButton.disabled).to.be.false + + await userEvent.click(copyButton) + + expect(clipboardWriteTextStub.calledOnce).to.be.true + expect(clipboardWriteTextStub.firstCall.args[0]).to.equal( + `${window.location.origin}/project/${shareModalProjectDefaults._id}/share#${sharingLinkToken}` + ) + + await screen.findByText(/link copied/i) + }) + }) }) it('allows an email address to be selected, removed, then re-added', async function () {