Merge pull request #14207 from overleaf/jdt-editor-events

editor events

GitOrigin-RevId: 8d74576d4f8117ecca47402afcc9cee229dd0dca
This commit is contained in:
Jimmy Domagala-Tang 2023-08-22 09:33:19 -04:00 committed by Copybot
parent 7e5a476a95
commit b2e74464a2
22 changed files with 194 additions and 28 deletions

View file

@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import EditorCloneProjectModalWrapper from '../../clone-project-modal/components/editor-clone-project-modal-wrapper'
import LeftMenuButton from './left-menu-button'
import { useLocation } from '../../../shared/hooks/use-location'
import * as eventTracking from '../../../infrastructure/event-tracking'
type ProjectCopyResponse = {
project_id: string
@ -20,10 +21,15 @@ export default function ActionsCopyProject() {
[location]
)
const handleShowModal = useCallback(() => {
eventTracking.sendMB('left-menu-copy')
setShowModal(true)
}, [])
return (
<>
<LeftMenuButton
onClick={() => setShowModal(true)}
onClick={handleShowModal}
icon={{
type: 'copy',
fw: true,

View file

@ -1,20 +1,26 @@
import { useState } from 'react'
import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '../../../shared/components/tooltip'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import WordCountModal from '../../word-count-modal/components/word-count-modal'
import LeftMenuButton from './left-menu-button'
import * as eventTracking from '../../../infrastructure/event-tracking'
export default function ActionsWordCount() {
const [showModal, setShowModal] = useState(false)
const { pdfUrl } = useCompileContext()
const { t } = useTranslation()
const handleShowModal = useCallback(() => {
eventTracking.sendMB('left-menu-count')
setShowModal(true)
}, [])
return (
<>
{pdfUrl ? (
<LeftMenuButton
onClick={() => setShowModal(true)}
onClick={handleShowModal}
icon={{
type: 'eye',
fw: true,

View file

@ -1,15 +1,30 @@
import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useProjectContext } from '../../../shared/context/project-context'
import Icon from '../../../shared/components/icon'
import Tooltip from '../../../shared/components/tooltip'
import * as eventTracking from '../../../infrastructure/event-tracking'
export default function DownloadPDF() {
const { t } = useTranslation()
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
const { _id: projectId } = useProjectContext()
function sendDownloadEvent() {
eventTracking.sendMB('download-pdf-button-click', {
projectId,
location: 'left-menu',
})
}
if (pdfUrl) {
return (
<a href={pdfDownloadUrl || pdfUrl} target="_blank" rel="noreferrer">
<a
href={pdfDownloadUrl || pdfUrl}
target="_blank"
rel="noreferrer"
onClick={sendDownloadEvent}
>
<Icon type="file-pdf-o" modifier="2x" />
<br />
PDF

View file

@ -1,16 +1,25 @@
import { useTranslation } from 'react-i18next'
import { useProjectContext } from '../../../shared/context/project-context'
import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking'
export default function DownloadSource() {
const { t } = useTranslation()
const { _id: projectId } = useProjectContext()
function sendDownloadEvent() {
eventTracking.sendMB('download-zip-button-click', {
projectId,
location: 'left-menu',
})
}
return (
<a
href={`/project/${projectId}/download/zip`}
target="_blank"
rel="noreferrer"
onClick={sendDownloadEvent}
>
<Icon type="file-archive-o" modifier="2x" />
<br />

View file

@ -1,4 +1,6 @@
import { useTranslation } from 'react-i18next'
import { useCallback } from 'react'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useContactUsModal } from '../../../shared/hooks/use-contact-us-modal'
import LeftMenuButton from './left-menu-button'
@ -6,10 +8,15 @@ export default function HelpContactUs() {
const { modal, showModal } = useContactUsModal()
const { t } = useTranslation()
const showModalWithAnalytics = useCallback(() => {
eventTracking.sendMB('left-menu-contact')
showModal()
}, [showModal])
return (
<>
<LeftMenuButton
onClick={showModal}
onClick={showModalWithAnalytics}
icon={{
type: 'question',
fw: true,

View file

@ -1,5 +1,6 @@
import { useState } from 'react'
import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useProjectContext } from '../../../shared/context/project-context'
import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal'
import useScopeValue from '../../../shared/hooks/use-scope-value'
@ -12,10 +13,15 @@ export default function HelpShowHotkeys() {
const { features } = useProjectContext()
const isMac = /Mac/.test(window.navigator?.platform)
const showModalWithAnalytics = useCallback(() => {
eventTracking.sendMB('left-menu-hotkeys')
setShowModal(true)
}, [])
return (
<>
<LeftMenuButton
onClick={() => setShowModal(true)}
onClick={showModalWithAnalytics}
icon={{
type: 'keyboard-o',
fw: true,

View file

@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking'
function BackToProjectsButton() {
const { t } = useTranslation()
@ -12,7 +13,13 @@ function BackToProjectsButton() {
overlayProps={{ placement: 'right' }}
>
<div className="toolbar-item">
<a className="btn btn-full-height" href="/project">
<a
className="btn btn-full-height"
href="/project"
onClick={() => {
eventTracking.sendMB('navigation-clicked-home')
}}
>
<Icon
type="home"
fw

View file

@ -5,6 +5,7 @@ import { useEditorContext } from '../../../shared/context/editor-context'
import { useChatContext } from '../../chat/context/chat-context'
import { useLayoutContext } from '../../../shared/context/layout-context'
import { useProjectContext } from '../../../shared/context/project-context'
import * as eventTracking from '../../../infrastructure/event-tracking'
const projectContextPropTypes = {
name: PropTypes.string.isRequired,
@ -38,6 +39,10 @@ const chatContextPropTypes = {
unreadMessageCount: PropTypes.number.isRequired,
}
function isOpentoString(open) {
return open ? 'open' : 'close'
}
const EditorNavigationToolbarRoot = React.memo(
function EditorNavigationToolbarRoot({
onlineUsersArray,
@ -74,29 +79,38 @@ const EditorNavigationToolbarRoot = React.memo(
if (!chatIsOpen) {
markMessagesAsRead()
}
eventTracking.sendMB('navigation-clicked-chat', {
action: isOpentoString(!chatIsOpen),
})
setChatIsOpen(value => !value)
}, [chatIsOpen, setChatIsOpen, markMessagesAsRead])
const toggleReviewPanelOpen = useCallback(
event => {
event.preventDefault()
eventTracking.sendMB('navigation-clicked-review', {
action: isOpentoString(!reviewPanelOpen),
})
setReviewPanelOpen(value => !value)
},
[setReviewPanelOpen]
[reviewPanelOpen, setReviewPanelOpen]
)
const toggleHistoryOpen = useCallback(() => {
const action = view === 'history' ? 'close' : 'open'
eventTracking.sendMB('navigation-clicked-history', { action })
setView(view === 'history' ? 'editor' : 'history')
}, [view, setView])
const openShareModal = useCallback(() => {
eventTracking.sendMB('navigation-clicked-share')
openShareProjectModal()
}, [openShareProjectModal])
const onShowLeftMenuClick = useCallback(
() => setLeftMenuShown(value => !value),
[setLeftMenuShown]
)
const onShowLeftMenuClick = useCallback(() => {
eventTracking.sendMB('navigation-clicked-menu')
setLeftMenuShown(value => !value)
}, [setLeftMenuShown])
const goToUser = useCallback(
user => {

View file

@ -149,6 +149,9 @@ function LayoutDropdownButton() {
)}
<ControlledDropdown
id="layout-dropdown"
onMainButtonClick={() => {
eventTracking.sendMB('navigation-clicked-layout')
}}
className="toolbar-item layout-dropdown"
pullRight
>

View file

@ -3,12 +3,14 @@ import { Button } from 'react-bootstrap'
import PropTypes from 'prop-types'
import Icon from '../../../../shared/components/icon'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
import * as eventTracking from '../../../../infrastructure/event-tracking'
export default function FileTreeModalCreateFileMode({ mode, icon, label }) {
const { newFileCreateMode, startCreatingFile } = useFileTreeActionable()
const handleClick = () => {
startCreatingFile(mode)
eventTracking.sendMB('file-modal-click', { method: mode })
}
return (

View file

@ -3,6 +3,7 @@ import FileTreeCreateNameInput from '../file-tree-create-name-input'
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
import ErrorMessage from '../error-message'
export default function FileTreeCreateNewDoc() {
@ -21,6 +22,7 @@ export default function FileTreeCreateNewDoc() {
event.preventDefault()
finishCreatingDoc({ name })
eventTracking.sendMB('new-file-created', { method: 'doc' })
},
[finishCreatingDoc, name]
)

View file

@ -12,6 +12,7 @@ import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form'
import { useProjectContext } from '../../../../../shared/context/project-context'
import ErrorMessage from '../error-message'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
export default function FileTreeImportFromProject() {
const { t } = useTranslation()
@ -85,6 +86,7 @@ export default function FileTreeImportFromProject() {
// form submission: create a linked file with this name, from this entity or output file
const handleSubmit = event => {
event.preventDefault()
eventTracking.sendMB('new-file-created', { method: 'project' })
if (isOutputFilesMode) {
finishCreatingLinkedFile({

View file

@ -6,6 +6,7 @@ import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form'
import ErrorMessage from '../error-message'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
export default function FileTreeImportFromUrl() {
const { t } = useTranslation()
@ -35,7 +36,7 @@ export default function FileTreeImportFromUrl() {
// form submission: create a linked file with this name, from this URL
const handleSubmit = event => {
event.preventDefault()
eventTracking.sendMB('new-file-created', { method: 'url' })
finishCreatingLinkedFile({
name,
provider: 'url',

View file

@ -7,6 +7,7 @@ import XHRUpload from '@uppy/xhr-upload'
import { Dashboard, useUppy } from '@uppy/react'
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
import { useProjectContext } from '../../../../../shared/context/project-context'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
@ -94,6 +95,7 @@ export default function FileTreeUploadDoc() {
})
// broadcast doc metadata after each successful upload
.on('upload-success', (file, response) => {
eventTracking.sendMB('new-file-created', { method: 'upload' })
if (response.body.entity_type === 'doc') {
window.setTimeout(() => {
refreshProjectMetadata(projectId, response.body.entity_id)

View file

@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import { MenuItem } from 'react-bootstrap'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
@ -18,6 +19,16 @@ function FileTreeItemMenuItems() {
downloadPath,
} = useFileTreeActionable()
const createWithAnalytics = () => {
eventTracking.sendMB('new-file-click', { location: 'file-menu' })
startCreatingDocOrFile()
}
const uploadWithAnalytics = () => {
eventTracking.sendMB('upload-click', { location: 'file-menu' })
startUploadingDocOrFile()
}
return (
<>
{canRename ? (
@ -34,9 +45,9 @@ function FileTreeItemMenuItems() {
{canCreate ? (
<>
<MenuItem divider />
<MenuItem onClick={startCreatingDocOrFile}>{t('new_file')}</MenuItem>
<MenuItem onClick={createWithAnalytics}>{t('new_file')}</MenuItem>
<MenuItem onClick={startCreatingFolder}>{t('new_folder')}</MenuItem>
<MenuItem onClick={startUploadingDocOrFile}>{t('upload')}</MenuItem>
<MenuItem onClick={uploadWithAnalytics}>{t('upload')}</MenuItem>
</>
) : null}
</>

View file

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { Button } from 'react-bootstrap'
import Tooltip from '../../../shared/components/tooltip'
@ -34,6 +35,16 @@ function FileTreeToolbarLeft() {
startUploadingDocOrFile,
} = useFileTreeActionable()
const createWithAnalytics = () => {
eventTracking.sendMB('new-file-click', { location: 'toolbar' })
startCreatingDocOrFile()
}
const uploadWithAnalytics = () => {
eventTracking.sendMB('upload-click', { location: 'toolbar' })
startUploadingDocOrFile()
}
if (!canCreate) return null
return (
@ -43,7 +54,7 @@ function FileTreeToolbarLeft() {
description={t('new_file')}
overlayProps={{ placement: 'bottom' }}
>
<Button onClick={startCreatingDocOrFile} bsStyle={null}>
<Button onClick={createWithAnalytics} bsStyle={null}>
<Icon type="file" fw accessibilityLabel={t('new_file')} />
</Button>
</Tooltip>
@ -61,7 +72,7 @@ function FileTreeToolbarLeft() {
description={t('upload')}
overlayProps={{ placement: 'bottom' }}
>
<Button onClick={startUploadingDocOrFile}>
<Button onClick={uploadWithAnalytics}>
<Icon type="upload" fw accessibilityLabel={t('upload')} />
</Button>
</Tooltip>

View file

@ -1,4 +1,5 @@
import { Trans } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
export default function HotkeysModalBottomText() {
return (
@ -8,6 +9,7 @@ export default function HotkeysModalBottomText() {
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a
onClick={() => eventTracking.sendMB('left-menu-hotkeys-template')}
href="/articles/overleaf-keyboard-shortcuts/qykqfvmxdnjf"
target="_blank"
/>,

View file

@ -5,9 +5,18 @@ import { useDetachCompileContext as useCompileContext } from '../../../shared/co
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
import SplitMenu from '../../../shared/components/split-menu'
import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking'
const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
function sendEventAndSet(value, setter, settingName) {
eventTracking.sendMB('recompile-setting-changed', {
setting: settingName,
settingVal: value,
})
setter(value)
}
function PdfCompileButton() {
const {
animateCompileDropdownArrow,
@ -30,6 +39,13 @@ function PdfCompileButton() {
const { t } = useTranslation()
const fromScratchWithEvent = () => {
eventTracking.sendMB('recompile-setting-changed', {
setting: 'from-scratch',
})
recompileFromScratch()
}
const compileButtonLabel = compiling ? `${t('compiling')}` : t('recompile')
const tooltipElement = (
<>
@ -76,36 +92,52 @@ function PdfCompileButton() {
>
<SplitMenu.Item header>{t('auto_compile')}</SplitMenu.Item>
<SplitMenu.Item onSelect={() => setAutoCompile(true)}>
<SplitMenu.Item
onSelect={() => sendEventAndSet(true, setAutoCompile, 'auto-compile')}
>
<Icon type={autoCompile ? 'check' : ''} fw />
{t('on')}
</SplitMenu.Item>
<SplitMenu.Item onSelect={() => setAutoCompile(false)}>
<SplitMenu.Item
onSelect={() => sendEventAndSet(false, setAutoCompile, 'auto-compile')}
>
<Icon type={!autoCompile ? 'check' : ''} fw />
{t('off')}
</SplitMenu.Item>
<SplitMenu.Item header>{t('compile_mode')}</SplitMenu.Item>
<SplitMenu.Item onSelect={() => setDraft(false)}>
<SplitMenu.Item
onSelect={() => sendEventAndSet(false, setDraft, 'compile-mode')}
>
<Icon type={!draft ? 'check' : ''} fw />
{t('normal')}
</SplitMenu.Item>
<SplitMenu.Item onSelect={() => setDraft(true)}>
<SplitMenu.Item
onSelect={() => sendEventAndSet(true, setDraft, 'compile-mode')}
>
<Icon type={draft ? 'check' : ''} fw />
{t('fast')} <span className="subdued">[draft]</span>
</SplitMenu.Item>
<SplitMenu.Item header>Syntax Checks</SplitMenu.Item>
<SplitMenu.Item onSelect={() => setStopOnValidationError(true)}>
<SplitMenu.Item
onSelect={() =>
sendEventAndSet(true, setStopOnValidationError, 'syntax-check')
}
>
<Icon type={stopOnValidationError ? 'check' : ''} fw />
{t('stop_on_validation_error')}
</SplitMenu.Item>
<SplitMenu.Item onSelect={() => setStopOnValidationError(false)}>
<SplitMenu.Item
onSelect={() =>
sendEventAndSet(false, setStopOnValidationError, 'syntax-check')
}
>
<Icon type={!stopOnValidationError ? 'check' : ''} fw />
{t('ignore_validation_errors')}
</SplitMenu.Item>
@ -133,7 +165,7 @@ function PdfCompileButton() {
</SplitMenu.Item>
<SplitMenu.Item
onSelect={() => recompileFromScratch()}
onSelect={fromScratchWithEvent}
disabled={compiling}
aria-disabled={compiling}
>

View file

@ -17,7 +17,10 @@ function PdfHybridDownloadButton() {
: t('please_compile_pdf_before_download')
function handleOnClick() {
eventTracking.sendMB('download-pdf-button-click', { projectId })
eventTracking.sendMB('download-pdf-button-click', {
projectId,
location: 'pdf-preview',
})
}
return (

View file

@ -4,6 +4,7 @@ import { Button, Label } from 'react-bootstrap'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import * as eventTracking from '../../../infrastructure/event-tracking'
function PdfHybridLogsButton() {
const { error, logEntries, toggleLogs, showLogs, stoppedOnFirstError } =
@ -12,8 +13,12 @@ function PdfHybridLogsButton() {
const { t } = useTranslation()
const handleClick = useCallback(() => {
// only send analytics on open
if (!showLogs) {
eventTracking.sendMB('logs-click')
}
toggleLogs()
}, [toggleLogs])
}, [toggleLogs, showLogs])
const errorCount = Number(logEntries?.errors?.length)
const warningCount = Number(logEntries?.warnings?.length)

View file

@ -10,6 +10,7 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
import PdfPreviewErrorBoundaryFallback from './pdf-preview-error-boundary-fallback'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { captureException } from '../../../infrastructure/error-reporter'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { getPdfCachingMetrics } from '../util/metrics'
function PdfJsViewer({ url, pdfFile }) {
@ -205,6 +206,11 @@ function PdfJsViewer({ url, pdfFile }) {
)
if (clickPosition) {
eventTracking.sendMB('jump-to-location', {
direction: 'pdf-location-in-code',
method: 'double-click',
})
window.dispatchEvent(
new CustomEvent('synctex:sync-to-position', {
detail: clickPosition,

View file

@ -18,6 +18,7 @@ import useDetachAction from '../../../shared/hooks/use-detach-action'
import localStorage from '../../../infrastructure/local-storage'
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
import useScopeEventListener from '../../../shared/hooks/use-scope-event-listener'
import * as eventTracking from '../../../infrastructure/event-tracking'
function GoToCodeButton({
position,
@ -38,6 +39,14 @@ function GoToCodeButton({
buttonIcon = <Icon type="arrow-left" className="synctex-control-icon" />
}
const syncToCodeWithButton = () => {
eventTracking.sendMB('jump-to-location', {
direction: 'pdf-location-in-code',
method: 'arrow',
})
syncToCode(position, 72)
}
return (
<Tooltip
id="sync-to-code"
@ -47,7 +56,7 @@ function GoToCodeButton({
<Button
bsStyle={null}
bsSize="xs"
onClick={() => syncToCode(position, 72)}
onClick={syncToCodeWithButton}
disabled={syncToCodeInFlight}
className={buttonClasses}
aria-label={t('go_to_pdf_location_in_code')}
@ -228,6 +237,11 @@ function PdfSynctexControls() {
column: cursorPosition.column,
}).toString()
eventTracking.sendMB('jump-to-location', {
direction: 'code-location-in-pdf',
method: 'arrow',
})
goToPdfLocation(params)
},
[getCurrentFilePath, goToPdfLocation]