mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #18071 from overleaf/jdt-bib-events
Bibliography events for 3rd party integrations GitOrigin-RevId: d8d7f4378d75166481d5265d2e8bef72d75968c3
This commit is contained in:
parent
898acab307
commit
2c11ad84e0
8 changed files with 99 additions and 15 deletions
|
@ -18,7 +18,9 @@ const ProjectLocator = require('../Project/ProjectLocator')
|
||||||
const Settings = require('@overleaf/settings')
|
const Settings = require('@overleaf/settings')
|
||||||
const logger = require('@overleaf/logger')
|
const logger = require('@overleaf/logger')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
|
const AnalyticsManager = require('../../../../app/src/Features/Analytics/AnalyticsManager')
|
||||||
const LinkedFilesHandler = require('./LinkedFilesHandler')
|
const LinkedFilesHandler = require('./LinkedFilesHandler')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
CompileFailedError,
|
CompileFailedError,
|
||||||
UrlFetchFailedError,
|
UrlFetchFailedError,
|
||||||
|
@ -91,6 +93,11 @@ module.exports = LinkedFilesController = {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
return LinkedFilesController.handleError(err, req, res, next)
|
return LinkedFilesController.handleError(err, req, res, next)
|
||||||
}
|
}
|
||||||
|
if (name.endsWith('.bib')) {
|
||||||
|
AnalyticsManager.recordEventForUser(userId, 'linked-bib-file', {
|
||||||
|
integration: provider,
|
||||||
|
})
|
||||||
|
}
|
||||||
return res.json({ new_file_id: newFileId })
|
return res.json({ new_file_id: newFileId })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,7 +22,10 @@ export default function FileTreeCreateNewDoc() {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
finishCreatingDoc({ name })
|
finishCreatingDoc({ name })
|
||||||
eventTracking.sendMB('new-file-created', { method: 'doc' })
|
eventTracking.sendMB('new-file-created', {
|
||||||
|
method: 'doc',
|
||||||
|
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[finishCreatingDoc, name]
|
[finishCreatingDoc, name]
|
||||||
)
|
)
|
||||||
|
|
|
@ -95,7 +95,10 @@ export default function FileTreeImportFromProject() {
|
||||||
// form submission: create a linked file with this name, from this entity or output file
|
// form submission: create a linked file with this name, from this entity or output file
|
||||||
const handleSubmit: FormEventHandler = event => {
|
const handleSubmit: FormEventHandler = event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
eventTracking.sendMB('new-file-created', { method: 'project' })
|
eventTracking.sendMB('new-file-created', {
|
||||||
|
method: 'project',
|
||||||
|
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
|
||||||
|
})
|
||||||
|
|
||||||
if (isOutputFilesMode) {
|
if (isOutputFilesMode) {
|
||||||
finishCreatingLinkedFile({
|
finishCreatingLinkedFile({
|
||||||
|
|
|
@ -36,7 +36,10 @@ export default function FileTreeImportFromUrl() {
|
||||||
// form submission: create a linked file with this name, from this URL
|
// form submission: create a linked file with this name, from this URL
|
||||||
const handleSubmit = event => {
|
const handleSubmit = event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
eventTracking.sendMB('new-file-created', { method: 'url' })
|
eventTracking.sendMB('new-file-created', {
|
||||||
|
method: 'url',
|
||||||
|
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
|
||||||
|
})
|
||||||
finishCreatingLinkedFile({
|
finishCreatingLinkedFile({
|
||||||
name,
|
name,
|
||||||
provider: 'url',
|
provider: 'url',
|
||||||
|
|
|
@ -145,7 +145,13 @@ export default function FileTreeUploadDoc() {
|
||||||
})
|
})
|
||||||
// broadcast doc metadata after each successful upload
|
// broadcast doc metadata after each successful upload
|
||||||
.on('upload-success', (file, response) => {
|
.on('upload-success', (file, response) => {
|
||||||
eventTracking.sendMB('new-file-created', { method: 'upload' })
|
eventTracking.sendMB('new-file-created', {
|
||||||
|
method: 'upload',
|
||||||
|
extension:
|
||||||
|
file?.name && file?.name.split('.').length > 1
|
||||||
|
? file?.name.split('.').pop()
|
||||||
|
: '',
|
||||||
|
})
|
||||||
if (response.body.entity_type === 'doc') {
|
if (response.body.entity_type === 'doc') {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
refreshProjectMetadata(projectId, response.body.entity_id)
|
refreshProjectMetadata(projectId, response.body.entity_id)
|
||||||
|
|
|
@ -1,12 +1,30 @@
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import * as eventTracking from '../../../../infrastructure/event-tracking'
|
import * as eventTracking from '../../../../infrastructure/event-tracking'
|
||||||
|
import { useProjectContext } from '@/shared/context/project-context'
|
||||||
|
|
||||||
import { MenuItem } from 'react-bootstrap'
|
import { MenuItem } from 'react-bootstrap'
|
||||||
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
|
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
|
||||||
|
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||||
|
import { useFileTreeSelectable } from '../../contexts/file-tree-selectable'
|
||||||
|
import { findInTree } from '../../util/find-in-tree'
|
||||||
|
|
||||||
function FileTreeItemMenuItems() {
|
function FileTreeItemMenuItems() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { fileTreeData } = useFileTreeData()
|
||||||
|
const { selectedEntityIds } = useFileTreeSelectable()
|
||||||
|
|
||||||
|
// return the name of the selected file or doc if there is only one selected
|
||||||
|
const selectedFileName = useMemo(() => {
|
||||||
|
if (selectedEntityIds.size === 1) {
|
||||||
|
const [selectedEntityId] = selectedEntityIds
|
||||||
|
const selectedEntity = findInTree(fileTreeData, selectedEntityId)
|
||||||
|
return selectedEntity?.entity?.name
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [fileTreeData, selectedEntityIds])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
canRename,
|
canRename,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
@ -19,15 +37,24 @@ function FileTreeItemMenuItems() {
|
||||||
downloadPath,
|
downloadPath,
|
||||||
} = useFileTreeActionable()
|
} = useFileTreeActionable()
|
||||||
|
|
||||||
const createWithAnalytics = () => {
|
const { owner } = useProjectContext()
|
||||||
|
|
||||||
|
const downloadWithAnalytics = useCallback(() => {
|
||||||
|
// we are only interested in downloads of bib files WRT analytics, for the purposes of promoting the tpr integrations
|
||||||
|
if (selectedFileName?.endsWith('.bib')) {
|
||||||
|
eventTracking.sendMB('download-bib-file', { projectOwner: owner._id })
|
||||||
|
}
|
||||||
|
}, [selectedFileName, owner])
|
||||||
|
|
||||||
|
const createWithAnalytics = useCallback(() => {
|
||||||
eventTracking.sendMB('new-file-click', { location: 'file-menu' })
|
eventTracking.sendMB('new-file-click', { location: 'file-menu' })
|
||||||
startCreatingDocOrFile()
|
startCreatingDocOrFile()
|
||||||
}
|
}, [startCreatingDocOrFile])
|
||||||
|
|
||||||
const uploadWithAnalytics = () => {
|
const uploadWithAnalytics = useCallback(() => {
|
||||||
eventTracking.sendMB('upload-click', { location: 'file-menu' })
|
eventTracking.sendMB('upload-click', { location: 'file-menu' })
|
||||||
startUploadingDocOrFile()
|
startUploadingDocOrFile()
|
||||||
}
|
}, [startUploadingDocOrFile])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -35,7 +62,7 @@ function FileTreeItemMenuItems() {
|
||||||
<MenuItem onClick={startRenaming}>{t('rename')}</MenuItem>
|
<MenuItem onClick={startRenaming}>{t('rename')}</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
{downloadPath ? (
|
{downloadPath ? (
|
||||||
<MenuItem href={downloadPath} download>
|
<MenuItem href={downloadPath} onClick={downloadWithAnalytics} download>
|
||||||
{t('download')}
|
{t('download')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import {
|
||||||
} from '@/features/ide-react/types/file-tree'
|
} from '@/features/ide-react/types/file-tree'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
|
import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
|
||||||
|
import { sendMB } from '@/infrastructure/event-tracking'
|
||||||
|
import { FileRef } from '../../../../../types/file-ref'
|
||||||
|
|
||||||
const FileTreeOpenContext = createContext<
|
const FileTreeOpenContext = createContext<
|
||||||
| {
|
| {
|
||||||
|
@ -34,7 +36,7 @@ const FileTreeOpenContext = createContext<
|
||||||
>(undefined)
|
>(undefined)
|
||||||
|
|
||||||
export const FileTreeOpenProvider: FC = ({ children }) => {
|
export const FileTreeOpenProvider: FC = ({ children }) => {
|
||||||
const { rootDocId } = useProjectContext()
|
const { rootDocId, owner } = useProjectContext()
|
||||||
const { eventEmitter, projectJoined } = useIdeReactContext()
|
const { eventEmitter, projectJoined } = useIdeReactContext()
|
||||||
const {
|
const {
|
||||||
openDocId: openDocWithId,
|
openDocId: openDocWithId,
|
||||||
|
@ -71,8 +73,14 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
|
||||||
setOpenEntity(selected)
|
setOpenEntity(selected)
|
||||||
if (selected.type === 'doc' && fileTreeReady) {
|
if (selected.type === 'doc' && fileTreeReady) {
|
||||||
openDocWithId(selected.entity._id)
|
openDocWithId(selected.entity._id)
|
||||||
|
if (selected.entity.name.endsWith('.bib')) {
|
||||||
|
sendMB('open-bib-file', {
|
||||||
|
projectOwner: owner._id,
|
||||||
|
isSampleFile: selected.entity.name === 'sample.bib',
|
||||||
|
linkedFileProvider: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep openFile scope value in sync with the file tree
|
// Keep openFile scope value in sync with the file tree
|
||||||
const openFile =
|
const openFile =
|
||||||
selected.type === 'fileRef'
|
selected.type === 'fileRef'
|
||||||
|
@ -80,10 +88,18 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
|
||||||
: null
|
: null
|
||||||
setOpenFile(openFile)
|
setOpenFile(openFile)
|
||||||
if (openFile) {
|
if (openFile) {
|
||||||
|
if (selected?.entity?.name?.endsWith('.bib')) {
|
||||||
|
sendMB('open-bib-file', {
|
||||||
|
projectOwner: owner._id,
|
||||||
|
isSampleFile: false,
|
||||||
|
linkedFileProvider: (selected.entity as FileRef).linkedFileData
|
||||||
|
?.provider,
|
||||||
|
})
|
||||||
|
}
|
||||||
window.dispatchEvent(new CustomEvent('file-view:file-opened'))
|
window.dispatchEvent(new CustomEvent('file-view:file-opened'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[fileTreeReady, setOpenFile, openDocWithId]
|
[fileTreeReady, setOpenFile, openDocWithId, owner]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFileTreeDelete = useCallback(
|
const handleFileTreeDelete = useCallback(
|
||||||
|
|
|
@ -8,8 +8,12 @@ import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||||
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
|
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
|
||||||
import { bsVersion } from '@/features/utils/bootstrap-5'
|
import { bsVersion } from '@/features/utils/bootstrap-5'
|
||||||
|
|
||||||
function trackUpgradeClick() {
|
function trackUpgradeClick(integration: string) {
|
||||||
sendMB('settings-upgrade-click')
|
sendMB('settings-upgrade-click', { integration })
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackLinkingClick(integration: string) {
|
||||||
|
sendMB('link-integration-click', { integration, location: 'Settings' })
|
||||||
}
|
}
|
||||||
|
|
||||||
type IntegrationLinkingWidgetProps = {
|
type IntegrationLinkingWidgetProps = {
|
||||||
|
@ -73,6 +77,7 @@ export function IntegrationLinkingWidget({
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
integration={title}
|
||||||
hasFeature={hasFeature}
|
hasFeature={hasFeature}
|
||||||
linked={linked}
|
linked={linked}
|
||||||
handleUnlinkClick={handleUnlinkClick}
|
handleUnlinkClick={handleUnlinkClick}
|
||||||
|
@ -81,6 +86,7 @@ export function IntegrationLinkingWidget({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UnlinkConfirmationModal
|
<UnlinkConfirmationModal
|
||||||
|
integration={title}
|
||||||
show={showModal}
|
show={showModal}
|
||||||
title={unlinkConfirmationTitle}
|
title={unlinkConfirmationTitle}
|
||||||
content={unlinkConfirmationText}
|
content={unlinkConfirmationText}
|
||||||
|
@ -92,6 +98,7 @@ export function IntegrationLinkingWidget({
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionButtonProps = {
|
type ActionButtonProps = {
|
||||||
|
integration: string
|
||||||
hasFeature?: boolean
|
hasFeature?: boolean
|
||||||
linked?: boolean
|
linked?: boolean
|
||||||
handleUnlinkClick: () => void
|
handleUnlinkClick: () => void
|
||||||
|
@ -105,6 +112,7 @@ function ActionButton({
|
||||||
handleUnlinkClick,
|
handleUnlinkClick,
|
||||||
linkPath,
|
linkPath,
|
||||||
disabled,
|
disabled,
|
||||||
|
integration,
|
||||||
}: ActionButtonProps) {
|
}: ActionButtonProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
if (!hasFeature) {
|
if (!hasFeature) {
|
||||||
|
@ -112,7 +120,7 @@ function ActionButton({
|
||||||
<ButtonWrapper
|
<ButtonWrapper
|
||||||
variant="primary"
|
variant="primary"
|
||||||
href="/user/subscription/plans"
|
href="/user/subscription/plans"
|
||||||
onClick={trackUpgradeClick}
|
onClick={() => trackUpgradeClick(integration)}
|
||||||
bs3Props={{ bsStyle: null, className: 'btn-primary' }}
|
bs3Props={{ bsStyle: null, className: 'btn-primary' }}
|
||||||
>
|
>
|
||||||
<span className="text-capitalize">{t('upgrade')}</span>
|
<span className="text-capitalize">{t('upgrade')}</span>
|
||||||
|
@ -152,6 +160,7 @@ function ActionButton({
|
||||||
bs5: 'text-capitalize',
|
bs5: 'text-capitalize',
|
||||||
})}
|
})}
|
||||||
bs3Props={{ bsStyle: null }}
|
bs3Props={{ bsStyle: null }}
|
||||||
|
onClick={() => trackLinkingClick(integration)}
|
||||||
>
|
>
|
||||||
{t('link')}
|
{t('link')}
|
||||||
</ButtonWrapper>
|
</ButtonWrapper>
|
||||||
|
@ -164,6 +173,7 @@ function ActionButton({
|
||||||
type UnlinkConfirmModalProps = {
|
type UnlinkConfirmModalProps = {
|
||||||
show: boolean
|
show: boolean
|
||||||
title: string
|
title: string
|
||||||
|
integration: string
|
||||||
content: string
|
content: string
|
||||||
unlinkPath: string
|
unlinkPath: string
|
||||||
handleHide: () => void
|
handleHide: () => void
|
||||||
|
@ -172,6 +182,7 @@ type UnlinkConfirmModalProps = {
|
||||||
function UnlinkConfirmationModal({
|
function UnlinkConfirmationModal({
|
||||||
show,
|
show,
|
||||||
title,
|
title,
|
||||||
|
integration,
|
||||||
content,
|
content,
|
||||||
unlinkPath,
|
unlinkPath,
|
||||||
handleHide,
|
handleHide,
|
||||||
|
@ -182,6 +193,13 @@ function UnlinkConfirmationModal({
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
handleHide()
|
handleHide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
sendMB('unlink-integration-click', {
|
||||||
|
integration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleModal show={show} onHide={handleHide}>
|
<AccessibleModal show={show} onHide={handleHide}>
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
|
@ -209,6 +227,7 @@ function UnlinkConfirmationModal({
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="danger-ghost"
|
variant="danger-ghost"
|
||||||
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
|
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
|
||||||
|
onClick={handleConfirm}
|
||||||
>
|
>
|
||||||
{t('unlink')}
|
{t('unlink')}
|
||||||
</ButtonWrapper>
|
</ButtonWrapper>
|
||||||
|
|
Loading…
Reference in a new issue