Merge pull request #33166 from overleaf/ii-share-modal-copy-link

[web] Share modal copy sharing link button

GitOrigin-RevId: a95f879196528ec3bc998558a6838c52aac33069
This commit is contained in:
ilkin-overleaf
2026-06-10 11:57:42 +00:00
committed by Copybot
parent 820d5a45ec
commit c9e4ab8b99
6 changed files with 131 additions and 2 deletions
@@ -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": "",
@@ -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 (
<OLButton
variant="secondary"
leadingIcon={isCopyBtnEnabled ? 'link' : 'link_off'}
disabled={!isCopyBtnEnabled}
onClick={handleCopyClick}
>
{t('copy_sharing_link')}
</OLButton>
)
}
@@ -92,7 +92,7 @@ function ProjectAccess({
return data
}).then(data => {
setSharingLinkData(newAccess === 'onlyInvitedPeople' ? null : data)
setSharingLinkData(data)
setProjectAccess(newAccess)
setSuccessActionMessage(t('access_updated'))
})
@@ -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({
<div className="d-flex flex-grow-1 flex-wrap gap-2">
{isSharingUpdatesEnabled ? (
<>
{!isInvitedPeopleScreen &&
(projectAccess === 'onlyInvitedPeople' ||
projectAccess === 'anyoneInXyzWithTheLink' ||
projectAccess === 'anyoneWithTheLink') && (
<CopySharingLinkButton />
)}
{successActionMessage && (
<div className="ms-auto px-3 align-self-center">
<div
+2
View File
@@ -517,6 +517,7 @@
"copy": "Copy",
"copy_project": "Copy project",
"copy_response": "Copy response",
"copy_sharing_link": "Copy sharing link",
"copying": "Copying",
"cost_summary": "Cost summary",
"could_not_connect_to_collaboration_server": "Could not connect to collaboration server",
@@ -1464,6 +1465,7 @@
"link_account": "Link Account",
"link_accounts": "Link accounts",
"link_accounts_and_add_email": "Link accounts and add email",
"link_copied": "Link copied",
"link_institutional_email_get_started": "Link an institutional email address to your account to get started.",
"link_sharing": "Link sharing",
"link_sharing_is_off_short": "Link sharing is off",
@@ -1005,6 +1005,10 @@ describe('<ShareProjectModal/>', 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('<ShareProjectModal/>', 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(
<ShareProjectModal {...modalProps} />,
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(
<ShareProjectModal {...modalProps} />,
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 () {