mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 06:03:45 +01:00
[web] add events for out of sync modals (#36132)
GitOrigin-RevId: f9673defab0d06d8e814bf2d43cbb7e19eb249a7
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { memo, useState } from 'react'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { useLocation } from '@/shared/hooks/use-location'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalBody,
|
||||
@@ -22,6 +23,12 @@ function OutOfSyncModal({ editorContent, show, onHide }: OutOfSyncModalProps) {
|
||||
const [editorContentShown, setEditorContentShown] = useState(false)
|
||||
const editorContentRows = (editorContent.match(/\n/g)?.length || 0) + 1
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
sendMB('out-of-sync-modal-shown')
|
||||
}
|
||||
}, [show])
|
||||
|
||||
// Reload the page to avoid staying in an inconsistent state.
|
||||
// https://github.com/overleaf/issues/issues/3694
|
||||
function done() {
|
||||
|
||||
+17
-2
@@ -1,5 +1,5 @@
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import {
|
||||
OLModal,
|
||||
@@ -15,6 +15,7 @@ import { debugConsole } from '@/utils/debugging'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import { downloadFileContent } from '@/utils/download-file'
|
||||
import { useLocation } from '@/shared/hooks/use-location'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
|
||||
export type UnableToSyncModalProps = {
|
||||
baseContent: string
|
||||
@@ -62,6 +63,12 @@ function UnableToSyncModal({
|
||||
const [saveError, setSaveError] = useState(false)
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
sendMB('unable-to-sync-modal-shown')
|
||||
}
|
||||
}, [show])
|
||||
|
||||
const onHide = useCallback(() => {
|
||||
onHideProp()
|
||||
if (reloadAfterClose) {
|
||||
@@ -69,11 +76,18 @@ function UnableToSyncModal({
|
||||
}
|
||||
}, [onHideProp, reloadAfterClose, location])
|
||||
|
||||
const handleDiscardChanges = useCallback(() => {
|
||||
sendMB('unable-to-sync-modal-click', { action: 'discard-changes' })
|
||||
onHide()
|
||||
}, [onHide])
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
sendMB('unable-to-sync-modal-click', { action: 'download' })
|
||||
downloadFileContent(targetContent, docName ?? 'document.txt')
|
||||
}, [targetContent, docName])
|
||||
|
||||
const handleSaveAsNewFile = useCallback(async () => {
|
||||
sendMB('unable-to-sync-modal-click', { action: 'save-new-file' })
|
||||
const projectId = getMeta('ol-project_id')
|
||||
const filename = buildOfflineFilename(docName)
|
||||
|
||||
@@ -98,6 +112,7 @@ function UnableToSyncModal({
|
||||
onHide()
|
||||
} catch (error) {
|
||||
debugConsole.error('Failed to save offline changes as new file', error)
|
||||
sendMB('unable-to-sync-modal-error-shown')
|
||||
setSaveError(true)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
@@ -142,7 +157,7 @@ function UnableToSyncModal({
|
||||
)}
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="danger-ghost" onClick={onHide}>
|
||||
<OLButton variant="danger-ghost" onClick={handleDiscardChanges}>
|
||||
{t('discard_changes')}
|
||||
</OLButton>
|
||||
<OLButton
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import OutOfSyncModal from '@/features/ide-react/components/modals/out-of-sync-modal'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import { location } from '@/shared/components/location'
|
||||
|
||||
describe('<OutOfSyncModal />', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
let reloadStub: sinon.SinonStub
|
||||
let onHide: sinon.SinonStub
|
||||
|
||||
const renderModal = (props: { show?: boolean } = {}) =>
|
||||
render(
|
||||
<OutOfSyncModal
|
||||
editorContent="offline content"
|
||||
show
|
||||
onHide={onHide}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
beforeEach(function () {
|
||||
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
|
||||
reloadStub = sinon.stub(location, 'reload')
|
||||
onHide = sinon.stub()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sendMBSpy.restore()
|
||||
reloadStub.restore()
|
||||
})
|
||||
|
||||
it('emits out-of-sync-modal-shown when the modal is shown', function () {
|
||||
renderModal()
|
||||
|
||||
expect(sendMBSpy).to.have.been.calledOnceWith('out-of-sync-modal-shown')
|
||||
})
|
||||
|
||||
it('does not emit out-of-sync-modal-shown while hidden', function () {
|
||||
renderModal({ show: false })
|
||||
|
||||
expect(sendMBSpy).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('does not emit a further event when reloading the editor', function () {
|
||||
renderModal()
|
||||
sendMBSpy.resetHistory()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Reload editor' }))
|
||||
|
||||
expect(onHide).to.have.been.calledOnce
|
||||
expect(reloadStub).to.have.been.calledOnce
|
||||
expect(sendMBSpy).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import UnableToSyncModal from '@/features/ide-react/components/modals/unable-to-sync-modal'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
|
||||
const PROJECT_ID = 'project-123'
|
||||
const ROOT_FOLDER_ID = 'root-folder-id'
|
||||
const UPLOAD_URL = `express:/project/${PROJECT_ID}/upload`
|
||||
|
||||
describe('<UnableToSyncModal />', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
let onHide: sinon.SinonStub
|
||||
|
||||
const renderModal = (props: { show?: boolean } = {}) =>
|
||||
render(
|
||||
<UnableToSyncModal
|
||||
baseContent="server content"
|
||||
targetContent="offline content"
|
||||
docName="main.tex"
|
||||
rootFolderId={ROOT_FOLDER_ID}
|
||||
show
|
||||
onHide={onHide}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const clickSaveAsNewFile = () =>
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save as new file' }))
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-project_id', PROJECT_ID)
|
||||
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
|
||||
onHide = sinon.stub()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sendMBSpy.restore()
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('emits unable-to-sync-modal-shown when the modal is shown', function () {
|
||||
renderModal()
|
||||
|
||||
expect(sendMBSpy).to.have.been.calledOnceWith('unable-to-sync-modal-shown')
|
||||
})
|
||||
|
||||
it('does not emit unable-to-sync-modal-shown while hidden', function () {
|
||||
renderModal({ show: false })
|
||||
|
||||
expect(sendMBSpy).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('emits a discard-changes click and hides the modal', function () {
|
||||
renderModal()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Discard changes' }))
|
||||
|
||||
expect(sendMBSpy).to.have.been.calledWithMatch(
|
||||
'unable-to-sync-modal-click',
|
||||
{ action: 'discard-changes' }
|
||||
)
|
||||
expect(onHide).to.have.been.calledOnce
|
||||
})
|
||||
|
||||
it('emits a save-new-file click and hides the modal when the upload succeeds', async function () {
|
||||
fetchMock.post(UPLOAD_URL, { status: 200, body: {} })
|
||||
|
||||
renderModal()
|
||||
clickSaveAsNewFile()
|
||||
|
||||
await waitFor(() => expect(onHide).to.have.been.calledOnce)
|
||||
expect(sendMBSpy).to.have.been.calledWithMatch(
|
||||
'unable-to-sync-modal-click',
|
||||
{ action: 'save-new-file' }
|
||||
)
|
||||
})
|
||||
|
||||
it('emits unable-to-sync-modal-error-shown when the upload fails', async function () {
|
||||
fetchMock.post(UPLOAD_URL, 500)
|
||||
|
||||
renderModal()
|
||||
clickSaveAsNewFile()
|
||||
|
||||
await screen.findByRole('link', { name: 'download the file' })
|
||||
expect(sendMBSpy).to.have.been.calledWith(
|
||||
'unable-to-sync-modal-error-shown'
|
||||
)
|
||||
expect(onHide).not.to.have.been.called
|
||||
})
|
||||
|
||||
describe('downloading', function () {
|
||||
let anchorClickStub: sinon.SinonStub
|
||||
|
||||
beforeEach(function () {
|
||||
// jsdom implements neither object URLs nor anchor-triggered downloads.
|
||||
window.URL.createObjectURL = sinon.stub().returns('blob:fake')
|
||||
window.URL.revokeObjectURL = sinon.stub()
|
||||
anchorClickStub = sinon.stub(HTMLAnchorElement.prototype, 'click')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
anchorClickStub.restore()
|
||||
delete (window.URL as Partial<typeof window.URL>).createObjectURL
|
||||
delete (window.URL as Partial<typeof window.URL>).revokeObjectURL
|
||||
})
|
||||
|
||||
it('emits a download click from the upload error notification', async function () {
|
||||
fetchMock.post(UPLOAD_URL, 500)
|
||||
|
||||
renderModal()
|
||||
clickSaveAsNewFile()
|
||||
|
||||
const downloadLink = await screen.findByRole('link', {
|
||||
name: 'download the file',
|
||||
})
|
||||
fireEvent.click(downloadLink)
|
||||
|
||||
expect(sendMBSpy).to.have.been.calledWithMatch(
|
||||
'unable-to-sync-modal-click',
|
||||
{ action: 'download' }
|
||||
)
|
||||
expect(anchorClickStub).to.have.been.calledOnce
|
||||
|
||||
// Let the deferred revokeObjectURL run before the shims are removed.
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user