Merge pull request #32264 from overleaf/mg-paste-blocked
Show toast when context menu paste fails due to blocked clipboard access GitOrigin-RevId: fcbeaa17174288ba627b3988ef5149d54a33fd4f
This commit is contained in:
@@ -1254,6 +1254,7 @@
|
||||
"open_target": "",
|
||||
"optional": "",
|
||||
"or": "",
|
||||
"or_enable_clipboard_access_in_your_browser_settings": "",
|
||||
"organization_name": "",
|
||||
"organize_tags": "",
|
||||
"other": "",
|
||||
@@ -2153,6 +2154,7 @@
|
||||
"use_a_different_email": "",
|
||||
"use_a_different_password": "",
|
||||
"use_saml_metadata_to_configure_sso_with_idp": "",
|
||||
"use_the_shortcut_key_to_paste": "",
|
||||
"use_your_own_machine": "",
|
||||
"used_latex_before": "",
|
||||
"used_latex_response_familiar": "",
|
||||
@@ -2326,6 +2328,7 @@
|
||||
"your_account_is_managed_by_your_group_admin": "",
|
||||
"your_add_on_has_been_cancelled_and_will_remain_active_until_your_billing_cycle_ends_on": "",
|
||||
"your_affiliation_is_confirmed": "",
|
||||
"your_browser_cant_access_the_clipboard_so_use_this_shortcut": "",
|
||||
"your_changes_will_save": "",
|
||||
"your_compile_timed_out": "",
|
||||
"your_current_plan": "",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Fragment, memo, ReactElement, useCallback, useState } from 'react'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||
import { OLToastContainer } from '@/shared/components/ol/ol-toast-container'
|
||||
import clipboardToastGenerators from '@/features/source-editor/components/clipboard-toasts'
|
||||
|
||||
const moduleGeneratorsImport = importOverleafModules('toastGenerators') as {
|
||||
import: { default: GlobalToastGeneratorEntry[] }
|
||||
@@ -23,7 +24,10 @@ type GlobalToastGenerator = (
|
||||
args: Record<string, any>
|
||||
) => Omit<OLToastProps, 'onDismiss'>
|
||||
|
||||
const GENERATOR_LIST: GlobalToastGeneratorEntry[] = moduleGenerators.flat()
|
||||
const GENERATOR_LIST: GlobalToastGeneratorEntry[] = [
|
||||
...moduleGenerators.flat(),
|
||||
...clipboardToastGenerators,
|
||||
]
|
||||
const GENERATOR_MAP: Map<string, GlobalToastGenerator> = new Map(
|
||||
GENERATOR_LIST.map(({ key, generator }) => [key, generator])
|
||||
)
|
||||
|
||||
@@ -105,7 +105,7 @@ export const pasteWithoutFormatting = async (
|
||||
pastePlainText(view, text)
|
||||
return true
|
||||
} catch {
|
||||
// Clipboard access denied or empty
|
||||
// Clipboard access denied
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { GlobalToastGeneratorEntry } from '@/features/ide-react/components/global-toasts'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
|
||||
const ClipboardPasteErrorToast = ({ shortcut }: { shortcut?: string }) => {
|
||||
const { t } = useTranslation()
|
||||
const resolvedShortcut = shortcut ?? (isMac ? '⌘V' : 'Ctrl+V')
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<b>{t('use_the_shortcut_key_to_paste')}</b>
|
||||
</p>
|
||||
{t('your_browser_cant_access_the_clipboard_so_use_this_shortcut')}{' '}
|
||||
<b>{resolvedShortcut}</b>
|
||||
<br />
|
||||
{t('or_enable_clipboard_access_in_your_browser_settings')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const generators: GlobalToastGeneratorEntry[] = [
|
||||
{
|
||||
key: 'clipboard:paste-error',
|
||||
generator: ({ shortcut }: { shortcut?: string }) => ({
|
||||
content: <ClipboardPasteErrorToast shortcut={shortcut} />,
|
||||
type: 'warning',
|
||||
autoHide: true,
|
||||
delay: 6000,
|
||||
isDismissible: true,
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
export default generators
|
||||
|
||||
export const showClipboardPasteErrorToast = (shortcut?: string) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('ide:show-toast', {
|
||||
detail: { key: 'clipboard:paste-error', shortcut },
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
pasteWithoutFormatting,
|
||||
pasteWithFormatting,
|
||||
} from '../commands/clipboard'
|
||||
import { showClipboardPasteErrorToast } from '../components/clipboard-toasts'
|
||||
import { isVisual } from '../extensions/visual/visual'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode'
|
||||
@@ -109,17 +110,38 @@ export const useContextMenuItems = () => {
|
||||
[view, closeMenu]
|
||||
)
|
||||
|
||||
const getShortcut = useCallback(
|
||||
(id: string) => {
|
||||
const shortcut = shortcuts[id]?.[0]
|
||||
return shortcut ? formatShortcut(shortcut) : undefined
|
||||
},
|
||||
[shortcuts]
|
||||
)
|
||||
|
||||
const inVisualMode = isVisual(view)
|
||||
|
||||
const handleCut = wrapForContextMenu('cut', () => cutSelection(view))
|
||||
const handleCopy = wrapForContextMenu('copy', () => copySelection(view))
|
||||
const handlePaste = wrapForContextMenu('paste', () =>
|
||||
inVisualMode ? pasteWithFormatting(view) : pasteWithoutFormatting(view)
|
||||
)
|
||||
const handlePaste = wrapForContextMenu('paste', async () => {
|
||||
const result = await (inVisualMode
|
||||
? pasteWithFormatting(view)
|
||||
: pasteWithoutFormatting(view))
|
||||
if (result === false) {
|
||||
showClipboardPasteErrorToast(getShortcut('paste'))
|
||||
}
|
||||
return result
|
||||
})
|
||||
const handlePasteSpecial = wrapForContextMenu(
|
||||
inVisualMode ? 'paste-without-formatting' : 'paste-with-formatting',
|
||||
() =>
|
||||
inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view)
|
||||
async () => {
|
||||
const result = await (inVisualMode
|
||||
? pasteWithoutFormatting(view)
|
||||
: pasteWithFormatting(view))
|
||||
if (result === false) {
|
||||
showClipboardPasteErrorToast(getShortcut('paste'))
|
||||
}
|
||||
return result
|
||||
}
|
||||
)
|
||||
const handleDelete = wrapForContextMenu('delete', () =>
|
||||
commands.deleteSelection(view)
|
||||
@@ -166,14 +188,6 @@ export const useContextMenuItems = () => {
|
||||
view.focus()
|
||||
}, [syncToPdf, view, changeLayout, isEditorOnly])
|
||||
|
||||
const getShortcut = useCallback(
|
||||
(id: string) => {
|
||||
const shortcut = shortcuts[id]?.[0]
|
||||
return shortcut ? formatShortcut(shortcut) : undefined
|
||||
},
|
||||
[shortcuts]
|
||||
)
|
||||
|
||||
return {
|
||||
closeMenu,
|
||||
onToggle,
|
||||
|
||||
@@ -1616,6 +1616,7 @@
|
||||
"opted_out_linking": "You’ve opted out from linking your <b>__email__</b> <b>__appName__</b> account to your institutional account.",
|
||||
"optional": "Optional",
|
||||
"or": "or",
|
||||
"or_enable_clipboard_access_in_your_browser_settings": "Or enable clipboard access in your browser settings.",
|
||||
"organization": "Organization",
|
||||
"organization_does_not_support_sso": "Your organization doesn’t support SSO yet, but you can still register using your email address.",
|
||||
"organization_name": "Organization name",
|
||||
@@ -2705,6 +2706,7 @@
|
||||
"use_a_different_email": "Use a <0>different email</0>.",
|
||||
"use_a_different_password": "Please use a different password",
|
||||
"use_saml_metadata_to_configure_sso_with_idp": "Use the Overleaf SAML metadata to configure SSO with your Identity Provider.",
|
||||
"use_the_shortcut_key_to_paste": "Use the shortcut key to paste",
|
||||
"use_your_own_machine": "Use your own machine, with your own setup",
|
||||
"used_latex_before": "How familiar are you with LaTeX?",
|
||||
"used_latex_response_familiar": "I’ve heard of it, but never used it",
|
||||
@@ -2901,6 +2903,7 @@
|
||||
"your_account_is_suspended": "Your account is suspended",
|
||||
"your_add_on_has_been_cancelled_and_will_remain_active_until_your_billing_cycle_ends_on": "Your add-on has been cancelled and will remain active until your billing cycle ends on __nextBillingDate__",
|
||||
"your_affiliation_is_confirmed": "Your <0>__institutionName__</0> affiliation is confirmed.",
|
||||
"your_browser_cant_access_the_clipboard_so_use_this_shortcut": "Your browser can’t access the clipboard, so use this shortcut:",
|
||||
"your_changes_will_save": "Your changes will save when we get the connection back.",
|
||||
"your_compile_timed_out": "Your compile timed out",
|
||||
"your_current_plan": "Your current plan",
|
||||
|
||||
+57
@@ -13,6 +13,7 @@ import { DetachCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import { FileTreeDataContext } from '@/shared/context/file-tree-data-context'
|
||||
import PackageVersions from '../../../../../app/src/infrastructure/PackageVersions'
|
||||
import { mockProject } from '../helpers/mock-project'
|
||||
import { GlobalToasts } from '@/features/ide-react/components/global-toasts'
|
||||
|
||||
const createPermissionsProvider = (
|
||||
permissions: Partial<Permissions>
|
||||
@@ -717,6 +718,62 @@ describe('editor context menu', { scrollBehavior: false }, function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('when clipboard access is blocked', function () {
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
const blocked = new DOMException('Not allowed', 'NotAllowedError')
|
||||
cy.stub(win.navigator.clipboard, 'read').rejects(blocked)
|
||||
cy.stub(win.navigator.clipboard, 'readText').rejects(blocked)
|
||||
})
|
||||
})
|
||||
|
||||
it('should show a toast when clicking Paste', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<GlobalToasts />
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(10).rightclick()
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('menuitem', { name: pasteLabelMatcher }).click()
|
||||
})
|
||||
|
||||
cy.get('.global-toasts').should(
|
||||
'contain.text',
|
||||
'Use the shortcut key to paste'
|
||||
)
|
||||
})
|
||||
|
||||
it('should show a toast when clicking Paste with formatting', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<GlobalToasts />
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(10).rightclick()
|
||||
cy.findByRole('menu').within(() => {
|
||||
cy.findByRole('menuitem', { name: /paste with formatting/i }).click()
|
||||
})
|
||||
|
||||
cy.get('.global-toasts').should(
|
||||
'contain.text',
|
||||
'Use the shortcut key to paste'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when pasting an image', function () {
|
||||
it('should open figure modal on pasting image', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
Reference in New Issue
Block a user