Parcourir la source

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

[web] Share modal copy sharing link button

GitOrigin-RevId: a95f879196528ec3bc998558a6838c52aac33069
ilkin-overleaf il y a 2 mois
Parent
commit
c9e4ab8b99

+ 2 - 0
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": "",

+ 48 - 0
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 (
+    <OLButton
+      variant="secondary"
+      leadingIcon={isCopyBtnEnabled ? 'link' : 'link_off'}
+      disabled={!isCopyBtnEnabled}
+      onClick={handleCopyClick}
+    >
+      {t('copy_sharing_link')}
+    </OLButton>
+  )
+}

+ 1 - 1
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'))
     })

+ 8 - 1
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({
         <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 - 0
services/web/locales/en.json

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

+ 70 - 0
services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx

@@ -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 () {