mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-02 21:59:00 +02:00
Merge pull request #31035 from overleaf/mg-context-menu-analytics
Add analytics event for context menu, comment, track changes, and jump to location GitOrigin-RevId: 8412cc3c8039cd1582ccee20b162b4bef4467dea
This commit is contained in:
@@ -10,6 +10,7 @@ import { Placement } from 'react-bootstrap/types'
|
||||
import useSynctex from '../hooks/use-synctex'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
|
||||
const GoToCodeButton = memo(function GoToCodeButton({
|
||||
syncToCode,
|
||||
@@ -35,6 +36,10 @@ const GoToCodeButton = memo(function GoToCodeButton({
|
||||
}
|
||||
|
||||
const syncToCodeWithButton = useCallback(() => {
|
||||
sendMB('jump-to-location', {
|
||||
method: 'arrow',
|
||||
direction: 'pdf-location-in-code',
|
||||
})
|
||||
syncToCode({ visualOffset: 72 })
|
||||
}, [syncToCode])
|
||||
|
||||
@@ -85,6 +90,14 @@ const GoToPdfButton = memo(function GoToPdfButton({
|
||||
'detach-synctex-control': !!isDetachLayout,
|
||||
})
|
||||
|
||||
const handleSyncToPdf = useCallback(() => {
|
||||
sendMB('jump-to-location', {
|
||||
method: 'arrow',
|
||||
direction: 'code-location-in-pdf',
|
||||
})
|
||||
syncToPdf()
|
||||
}, [syncToPdf])
|
||||
|
||||
let buttonIcon = null
|
||||
if (syncToPdfInFlight) {
|
||||
buttonIcon = <OLSpinner size="sm" />
|
||||
@@ -104,7 +117,7 @@ const GoToPdfButton = memo(function GoToPdfButton({
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={syncToPdf}
|
||||
onClick={handleSyncToPdf}
|
||||
disabled={syncToPdfInFlight || !canSyncToPdf}
|
||||
className={buttonClasses}
|
||||
aria-label={t('go_to_code_location_in_pdf')}
|
||||
|
||||
@@ -28,7 +28,7 @@ function ReviewModeSwitcher() {
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const { write, trackedWrite } = usePermissionsContext()
|
||||
const { features } = useProjectContext()
|
||||
const { setShowUpgradeModal } = useEditorContext()
|
||||
const { setUpgradeTrackChangesModal } = useEditorContext()
|
||||
const showViewOption = permissionsLevel === 'readOnly'
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
@@ -73,7 +73,10 @@ function ReviewModeSwitcher() {
|
||||
return
|
||||
}
|
||||
if (!features.trackChanges) {
|
||||
setShowUpgradeModal(true)
|
||||
setUpgradeTrackChangesModal({
|
||||
show: true,
|
||||
location: 'review-switcher',
|
||||
})
|
||||
} else {
|
||||
sendMB('editing-mode-change', {
|
||||
role: permissionsLevel,
|
||||
|
||||
@@ -36,6 +36,7 @@ import classNames from 'classnames'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import useReviewPanelLayout from '../hooks/use-review-panel-layout'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
|
||||
const EDIT_MODE_SWITCH_WIDGET_HEIGHT = 40
|
||||
const CM_LINE_RIGHT_PADDING = 8
|
||||
@@ -110,6 +111,13 @@ const ReviewTooltipMenuContent = memo<{ onAddComment: () => void }>(
|
||||
>()
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const handleAddCommentClick = useCallback(() => {
|
||||
sendMB('add-comment', {
|
||||
location: 'tooltip',
|
||||
})
|
||||
onAddComment()
|
||||
}, [onAddComment])
|
||||
|
||||
const changesInSelection = useMemo(() => {
|
||||
return (ranges?.changes ?? []).filter(({ op }) => {
|
||||
const opFrom = op.p
|
||||
@@ -219,7 +227,7 @@ const ReviewTooltipMenuContent = memo<{ onAddComment: () => void }>(
|
||||
>
|
||||
<button
|
||||
className="review-tooltip-menu-button review-tooltip-add-comment-button"
|
||||
onClick={onAddComment}
|
||||
onClick={handleAddCommentClick}
|
||||
>
|
||||
<MaterialIcon type="chat" />
|
||||
{t('add_comment')}
|
||||
|
||||
+38
-10
@@ -4,7 +4,8 @@ import { useUserContext } from '@/shared/context/user-context'
|
||||
import teaserVideo from '../images/teaser-track-changes.mp4'
|
||||
import teaserImage from '../images/teaser-track-changes.gif'
|
||||
import { startFreeTrial, upgradePlan } from '@/main/account-upgrade'
|
||||
import { memo } from 'react'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import { memo, useEffect } from 'react'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalBody,
|
||||
@@ -22,14 +23,34 @@ function UpgradeTrackChangesModal() {
|
||||
const { t } = useTranslation()
|
||||
const { project } = useProjectContext()
|
||||
const user = useUserContext()
|
||||
const { showUpgradeModal, setShowUpgradeModal } = useEditorContext()
|
||||
const {
|
||||
upgradeTrackChangesModal: { show, location = 'unknown' },
|
||||
setUpgradeTrackChangesModal,
|
||||
} = useEditorContext()
|
||||
|
||||
if (!showUpgradeModal) {
|
||||
const handleClose = () => {
|
||||
sendMB('paywall-dismiss', {
|
||||
'paywall-type': 'track-changes',
|
||||
location,
|
||||
})
|
||||
setUpgradeTrackChangesModal({ show: false })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
sendMB('paywall-prompt', {
|
||||
'paywall-type': 'track-changes',
|
||||
location,
|
||||
})
|
||||
}
|
||||
}, [show, location])
|
||||
|
||||
if (!show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<OLModal show={showUpgradeModal} onHide={() => setShowUpgradeModal(false)}>
|
||||
<OLModal show={show} onHide={handleClose}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('upgrade_to_review')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
@@ -67,14 +88,24 @@ function UpgradeTrackChangesModal() {
|
||||
user.allowedFreeTrial ? (
|
||||
<OLButton
|
||||
variant="premium"
|
||||
onClick={() => startFreeTrial('track-changes')}
|
||||
onClick={() =>
|
||||
startFreeTrial('track-changes', undefined, {
|
||||
location,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('try_it_for_free')}
|
||||
</OLButton>
|
||||
) : (
|
||||
<OLButton
|
||||
variant="premium"
|
||||
onClick={() => upgradePlan('project-sharing')}
|
||||
onClick={() => {
|
||||
sendMB('paywall-click', {
|
||||
'paywall-type': 'track-changes',
|
||||
location,
|
||||
})
|
||||
upgradePlan('project-sharing')
|
||||
}}
|
||||
>
|
||||
{t('upgrade')}
|
||||
</OLButton>
|
||||
@@ -92,10 +123,7 @@ function UpgradeTrackChangesModal() {
|
||||
)}
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
onClick={() => setShowUpgradeModal(false)}
|
||||
>
|
||||
<OLButton variant="secondary" onClick={handleClose}>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
|
||||
+7
-1
@@ -6,9 +6,15 @@ import {
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
|
||||
import SplitTestBadge from '@/shared/components/split-test-badge'
|
||||
import { sendContextMenuEvent } from '../utils/context-menu-analytics'
|
||||
|
||||
const FEEDBACK_FORM_URL = 'https://forms.gle/BsbNQeSwGKEwXpxTA'
|
||||
const handleClick = () => {
|
||||
|
||||
function handleClick() {
|
||||
sendContextMenuEvent('menu-click', {
|
||||
location: 'editor-context-menu',
|
||||
item: 'give-feedback',
|
||||
})
|
||||
window.open(FEEDBACK_FORM_URL, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import { useContextMenuItems } from '../hooks/use-context-menu-items'
|
||||
import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
|
||||
import { EditorContextMenuFeedback } from './editor-context-menu-feedback'
|
||||
import { sendContextMenuEvent } from '../utils/context-menu-analytics'
|
||||
|
||||
const EditorContextMenu: FC = () => {
|
||||
const state = useCodeMirrorStateContext()
|
||||
@@ -40,6 +41,9 @@ const EditorContextMenuContent: FC = memo(function EditorContextMenuContent() {
|
||||
const menuRef = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
sendContextMenuEvent('menu-expand', {
|
||||
location: 'editor-context-menu',
|
||||
})
|
||||
menuRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
|
||||
+3
-1
@@ -17,6 +17,8 @@ import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
|
||||
const addCommentFromToolbar = () => commands.addComment('toolbar')
|
||||
|
||||
export const ToolbarItems: FC<{
|
||||
state: EditorState
|
||||
overflowed?: Set<string>
|
||||
@@ -135,7 +137,7 @@ export const ToolbarItems: FC<{
|
||||
id="toolbar-add-comment"
|
||||
label={t('add_comment')}
|
||||
disabled={state.selection.main.empty}
|
||||
command={commands.addComment}
|
||||
command={addCommentFromToolbar}
|
||||
icon="add_comment"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
deleteToVisualLineStart,
|
||||
} from './visual-line-selection'
|
||||
import { emitShortcutEvent } from '@/features/source-editor/extensions/toolbar/utils/analytics'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
|
||||
const toggleReviewPanel = () => {
|
||||
window.dispatchEvent(new Event('ui.toggle-review-panel'))
|
||||
@@ -30,6 +31,9 @@ const toggleReviewPanel = () => {
|
||||
}
|
||||
|
||||
const addNewCommentFromKbdShortcut = () => {
|
||||
sendMB('add-comment', {
|
||||
location: 'shortcut',
|
||||
})
|
||||
window.dispatchEvent(new Event('add-new-review-comment'))
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
openSearchPanel,
|
||||
searchPanelOpen,
|
||||
} from '@codemirror/search'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import { toggleRanges, wrapRanges } from '../../commands/ranges'
|
||||
import {
|
||||
ancestorListType,
|
||||
@@ -163,7 +164,10 @@ export const toggleSearch: Command = view => {
|
||||
return true
|
||||
}
|
||||
|
||||
export const addComment = () => {
|
||||
export const addComment = (location: string) => {
|
||||
sendMB('add-comment', {
|
||||
location,
|
||||
})
|
||||
window.dispatchEvent(new Event('add-new-review-comment'))
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ import {
|
||||
import { isVisual } from '../extensions/visual/visual'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode'
|
||||
import {
|
||||
sendContextMenuEvent,
|
||||
ContextMenuItemSegmentation,
|
||||
} from '../utils/context-menu-analytics'
|
||||
|
||||
export const useContextMenuItems = () => {
|
||||
const view = useCodeMirrorViewContext()
|
||||
@@ -45,7 +49,7 @@ export const useContextMenuItems = () => {
|
||||
const { shortcuts } = useCommandRegistry()
|
||||
const { features } = useProjectContext()
|
||||
const requestedPdfSyncRef = useRef(false)
|
||||
const { setShowUpgradeModal } = useEditorContext()
|
||||
const { setUpgradeTrackChangesModal } = useEditorContext()
|
||||
const trackingChangesMode = useTrackingChangesMode()
|
||||
const isReview = trackingChangesMode === 'review'
|
||||
|
||||
@@ -87,40 +91,58 @@ export const useContextMenuItems = () => {
|
||||
pdfUrl && pdfViewer !== 'native' && !visualPreviewEnabled && canSyncToPdf
|
||||
|
||||
const wrapForContextMenu = useCallback(
|
||||
(command: () => Promise<boolean> | boolean) => async () => {
|
||||
const result = await command()
|
||||
if (result !== false) {
|
||||
view.focus()
|
||||
closeMenu()
|
||||
}
|
||||
},
|
||||
(
|
||||
item: ContextMenuItemSegmentation,
|
||||
command: () => Promise<boolean> | boolean
|
||||
) =>
|
||||
async () => {
|
||||
sendContextMenuEvent('menu-click', {
|
||||
location: 'editor-context-menu',
|
||||
item,
|
||||
})
|
||||
const result = await command()
|
||||
if (result !== false) {
|
||||
view.focus()
|
||||
closeMenu()
|
||||
}
|
||||
},
|
||||
[view, closeMenu]
|
||||
)
|
||||
|
||||
const inVisualMode = isVisual(view)
|
||||
|
||||
const handleCut = wrapForContextMenu(() => cutSelection(view))
|
||||
const handleCopy = wrapForContextMenu(() => copySelection(view))
|
||||
const handlePaste = wrapForContextMenu(() =>
|
||||
const handleCut = wrapForContextMenu('cut', () => cutSelection(view))
|
||||
const handleCopy = wrapForContextMenu('copy', () => copySelection(view))
|
||||
const handlePaste = wrapForContextMenu('paste', () =>
|
||||
inVisualMode ? pasteWithFormatting(view) : pasteWithoutFormatting(view)
|
||||
)
|
||||
const handlePasteSpecial = wrapForContextMenu(() =>
|
||||
inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view)
|
||||
const handlePasteSpecial = wrapForContextMenu(
|
||||
inVisualMode ? 'paste-without-formatting' : 'paste-with-formatting',
|
||||
() =>
|
||||
inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view)
|
||||
)
|
||||
const handleDelete = wrapForContextMenu('delete', () =>
|
||||
commands.deleteSelection(view)
|
||||
)
|
||||
const handleDelete = wrapForContextMenu(() => commands.deleteSelection(view))
|
||||
|
||||
const handleToggleTrackChanges = wrapForContextMenu(() => {
|
||||
// Matching the logic in review toggle to ensure consistency for server pro
|
||||
if (!features.trackChanges && !isReview) {
|
||||
setShowUpgradeModal(true)
|
||||
const handleToggleTrackChanges = wrapForContextMenu(
|
||||
wantTrackChanges ? 'back-to-editing' : 'suggest-edits',
|
||||
() => {
|
||||
// Matching the logic in review toggle to ensure consistency for server pro
|
||||
if (!features.trackChanges && !isReview) {
|
||||
setUpgradeTrackChangesModal({
|
||||
show: true,
|
||||
location: 'editor-context-menu',
|
||||
})
|
||||
return true
|
||||
}
|
||||
window.dispatchEvent(new Event('toggle-track-changes'))
|
||||
return true
|
||||
}
|
||||
window.dispatchEvent(new Event('toggle-track-changes'))
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
const handleComment = wrapForContextMenu(() => {
|
||||
commands.addComment()
|
||||
const handleComment = wrapForContextMenu('comment', () => {
|
||||
commands.addComment('editor-context-menu')
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -130,6 +152,15 @@ export const useContextMenuItems = () => {
|
||||
if (isEditorOnly) {
|
||||
changeLayout('sideBySide')
|
||||
}
|
||||
|
||||
sendContextMenuEvent('menu-click', {
|
||||
location: 'editor-context-menu',
|
||||
item: 'jump-to-location-in-pdf',
|
||||
})
|
||||
sendContextMenuEvent('jump-to-location', {
|
||||
method: 'editor-context-menu',
|
||||
direction: 'code-location-in-pdf',
|
||||
})
|
||||
requestedPdfSyncRef.current = true
|
||||
syncToPdf()
|
||||
view.focus()
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ export const useToolbarMenuBarEditorCommands = () => {
|
||||
id: 'comment',
|
||||
label: t('comment'),
|
||||
handler: () => {
|
||||
commands.addComment()
|
||||
commands.addComment('toolbar')
|
||||
},
|
||||
disabled: !comment || state.selection.main.empty,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
|
||||
export type ContextMenuItemSegmentation =
|
||||
| 'cut'
|
||||
| 'copy'
|
||||
| 'paste'
|
||||
| 'paste-without-formatting'
|
||||
| 'paste-with-formatting'
|
||||
| 'give-feedback'
|
||||
| 'delete'
|
||||
| 'jump-to-location-in-pdf'
|
||||
| 'suggest-edits'
|
||||
| 'back-to-editing'
|
||||
| 'comment'
|
||||
|
||||
export type ContextMenuAnalyticsEvents = {
|
||||
'menu-expand': {
|
||||
location: 'editor-context-menu'
|
||||
}
|
||||
'menu-click': {
|
||||
location: 'editor-context-menu'
|
||||
item: ContextMenuItemSegmentation
|
||||
}
|
||||
'jump-to-location': {
|
||||
method: 'editor-context-menu'
|
||||
direction: 'code-location-in-pdf'
|
||||
}
|
||||
'add-comment': {
|
||||
location: 'editor-context-menu'
|
||||
}
|
||||
'paywall-prompt': {
|
||||
'paywall-type': 'track-changes'
|
||||
location: 'editor-context-menu'
|
||||
}
|
||||
}
|
||||
|
||||
export const sendContextMenuEvent = <
|
||||
T extends keyof ContextMenuAnalyticsEvents,
|
||||
>(
|
||||
eventName: T,
|
||||
segmentation: ContextMenuAnalyticsEvents[T]
|
||||
) => {
|
||||
sendMB(eventName, segmentation)
|
||||
}
|
||||
@@ -20,6 +20,11 @@ import { WritefullAPI } from './types/writefull-instance'
|
||||
import { Cobranding } from '../../../../types/cobranding'
|
||||
import { SymbolWithCharacter } from '../../../../modules/symbol-palette/frontend/js/data/symbols'
|
||||
|
||||
type UpgradeTrackChangesModal = {
|
||||
show: boolean
|
||||
location?: string
|
||||
}
|
||||
|
||||
export const EditorContext = createContext<
|
||||
| {
|
||||
cobranding?: Cobranding
|
||||
@@ -35,8 +40,10 @@ export const EditorContext = createContext<
|
||||
premiumSuggestionResetDate: Date
|
||||
writefullInstance: WritefullAPI | null
|
||||
setWritefullInstance: (instance: WritefullAPI) => void
|
||||
showUpgradeModal: boolean
|
||||
setShowUpgradeModal: Dispatch<SetStateAction<boolean>>
|
||||
upgradeTrackChangesModal: UpgradeTrackChangesModal
|
||||
setUpgradeTrackChangesModal: Dispatch<
|
||||
SetStateAction<UpgradeTrackChangesModal>
|
||||
>
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
@@ -88,7 +95,8 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
: new Date()
|
||||
})
|
||||
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false)
|
||||
const [showUpgradeModal, setShowUpgradeModal] =
|
||||
useState<UpgradeTrackChangesModal>({ show: false })
|
||||
|
||||
const isPendingEditor = useMemo(
|
||||
() =>
|
||||
@@ -170,8 +178,8 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
setPremiumSuggestionResetDate,
|
||||
writefullInstance,
|
||||
setWritefullInstance,
|
||||
showUpgradeModal,
|
||||
setShowUpgradeModal,
|
||||
upgradeTrackChangesModal: showUpgradeModal,
|
||||
setUpgradeTrackChangesModal: setShowUpgradeModal,
|
||||
}),
|
||||
[
|
||||
cobranding,
|
||||
|
||||
@@ -292,10 +292,10 @@ export function makeEditorProvider({
|
||||
setPremiumSuggestionResetDate: () => {},
|
||||
writefullInstance: null,
|
||||
setWritefullInstance: () => {},
|
||||
showUpgradeModal: false,
|
||||
setShowUpgradeModal: () => {},
|
||||
cobranding,
|
||||
isRestrictedTokenMember,
|
||||
upgradeTrackChangesModal: { show: false },
|
||||
setUpgradeTrackChangesModal: () => {},
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user