[web] Re-introduce orphan detection in detached PDF (#32994)

GitOrigin-RevId: 07a58d6f7e3c6db8465c62b390e34270c2b4fd67
This commit is contained in:
Mathias Jakobsen
2026-04-27 08:05:42 +00:00
committed by Copybot
parent 5e675664c6
commit c46fba951e
8 changed files with 197 additions and 22 deletions
@@ -1503,6 +1503,7 @@
"recovering": "",
"recurly_email_update_needed": "",
"recurly_email_updated": "",
"redirect_to_editor": "",
"redirect_url": "",
"redo": "",
"reduce_costs_group_licenses": "",
@@ -1902,6 +1903,7 @@
"synctex_failed": "",
"syntax_checks": "",
"syntax_validation": "",
"tab_connecting": "",
"table": "",
"tabs_open_in_preview_mode_until_you_interact_with_them": "",
"tag_color": "",
@@ -0,0 +1,16 @@
import LoadingSpinner from '@/shared/components/loading-spinner'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
function PdfPreviewHybridToolbarConnectingInner() {
const { t } = useTranslation()
return (
<div className="toolbar-pdf-left">
<div className="toolbar-pdf-orphan">
<LoadingSpinner size="sm" loadingText={`${t('tab_connecting')}…`} />
</div>
</div>
)
}
export default memo(PdfPreviewHybridToolbarConnectingInner)
@@ -0,0 +1,29 @@
import { useTranslation } from 'react-i18next'
import { memo, useCallback } from 'react'
import { buildUrlWithDetachRole } from '@/shared/utils/url-helper'
import { useLocation } from '@/shared/hooks/use-location'
import OLButton from '@/shared/components/ol/ol-button'
function PdfPreviewHybridToolbarOrphanRefreshInner() {
const { t } = useTranslation()
const location = useLocation()
const redirect = useCallback(() => {
location.assign(buildUrlWithDetachRole(null).toString())
}, [location])
return (
<div className="toolbar-pdf-left">
<OLButton
variant="primary"
size="sm"
onClick={redirect}
className="toolbar-pdf-orphan-btn"
>
{t('redirect_to_editor')}
</OLButton>
</div>
)
}
export default memo(PdfPreviewHybridToolbarOrphanRefreshInner)
@@ -6,15 +6,36 @@ import PdfHybridDownloadButton from '@/features/pdf-preview/components/pdf-hybri
import { DetachedSynctexControl } from '@/features/pdf-preview/components/detach-synctex-control'
import SwitchToEditorButton from '@/features/pdf-preview/components/switch-to-editor-button'
import PdfHybridLogsButton from '@/features/pdf-preview/components/pdf-hybrid-logs-button'
import PdfPreviewHybridToolbarOrphanRefreshInner from './pdf-preview-hybrid-toolbar-orphan-refresh-inner'
import PdfPreviewHybridToolbarConnectingInner from './pdf-preview-hybrid-toolbar-connecting-inner'
import useDetachedOrphanDetection from '../hooks/use-detached-orphan-detection'
function PdfPreviewHybridToolbar() {
const { t } = useTranslation()
// TODO: add detached pdf logic
const orphanState = useDetachedOrphanDetection()
let ToolbarContent = null
if (orphanState === 'orphan') {
ToolbarContent = PdfPreviewHybridToolbarOrphanRefreshInner
} else if (orphanState === 'connecting') {
ToolbarContent = PdfPreviewHybridToolbarConnectingInner
} else {
ToolbarContent = PdfPreviewHybridToolbarInner
}
return (
<OLButtonToolbar
className="toolbar toolbar-pdf toolbar-pdf-hybrid"
aria-label={t('pdf')}
>
<ToolbarContent />
</OLButtonToolbar>
)
}
function PdfPreviewHybridToolbarInner() {
return (
<>
<div className="toolbar-pdf-left">
<PdfCompileButton />
<PdfHybridLogsButton />
@@ -26,7 +47,7 @@ function PdfPreviewHybridToolbar() {
<DetachedSynctexControl />
{/* TODO: should we have code check? */}
</div>
</OLButtonToolbar>
</>
)
}
@@ -0,0 +1,47 @@
import { useLayoutContext } from '@/shared/context/layout-context'
import { useEffect, useRef, useState } from 'react'
type OrphanState = 'connecting' | 'orphan' | 'not-orphan'
const ORPHAN_UI_TIMEOUT_MS = 5000
export default function useDetachedOrphanDetection(): OrphanState {
const { detachRole, detachIsLinked } = useLayoutContext()
const uiTimeoutRef = useRef<number>()
const [longTimeOrphan, setLongTimeOrphan] = useState(false)
const orphaned = !detachIsLinked && detachRole === 'detached'
useEffect(() => {
if (uiTimeoutRef.current) {
window.clearTimeout(uiTimeoutRef.current)
}
if (orphaned) {
uiTimeoutRef.current = window.setTimeout(() => {
setLongTimeOrphan(true)
}, ORPHAN_UI_TIMEOUT_MS)
} else {
setLongTimeOrphan(false)
}
return () => {
if (uiTimeoutRef.current) {
window.clearTimeout(uiTimeoutRef.current)
}
}
}, [orphaned])
if (!orphaned) {
// not detached, or detached but linked
return 'not-orphan'
}
if (longTimeOrphan) {
return 'orphan'
} else if (orphaned) {
return 'connecting'
} else {
return 'not-orphan'
}
}
@@ -107,6 +107,15 @@
justify-content: flex-end;
}
.toolbar-pdf-orphan {
color: var(--toolbar-btn-color);
}
.toolbar-pdf-orphan-btn,
.toolbar-pdf-orphan {
margin-left: var(--spacing-02);
}
.btn.pdf-toolbar-btn {
display: inline-block;
color: var(--toolbar-btn-color);
+2
View File
@@ -1999,6 +1999,7 @@
"reconnecting_in_x_secs": "Reconnecting in __seconds__ secs",
"recurly_email_update_needed": "Your billing email address is currently <0>__recurlyEmail__</0>. If needed you can update your billing address to <1>__userEmail__</1>.",
"recurly_email_updated": "Your billing email address was successfully updated",
"redirect_to_editor": "Redirect to editor",
"redirect_url": "Redirect URL",
"redirecting": "Redirecting",
"redo": "Redo",
@@ -2490,6 +2491,7 @@
"synctex_failed": "Couldn’t find the corresponding source file",
"syntax_checks": "Syntax checks",
"syntax_validation": "Code check",
"tab_connecting": "Connecting with the editor",
"table": "Table",
"table_generator": "Table Generator",
"tabs_open_in_preview_mode_until_you_interact_with_them": "Tabs open in preview mode until you interact with them",
@@ -15,16 +15,17 @@ describe('<PdfPreviewHybridToolbar/>', function () {
</EditorProviders>
)
cy.findByRole('button', { name: 'Recompile' })
cy.findByRole('button', { name: 'Recompile' }).should('exist')
})
describe('orphan mode', function () {
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('shows connecting message on load', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-detachRole', 'detached')
})
})
it('shows connecting message on load', function () {
cy.mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
@@ -32,13 +33,13 @@ describe('<PdfPreviewHybridToolbar/>', function () {
)
cy.contains('Connecting with the editor')
cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
cy.findByRole('button', { name: 'Redirect to editor' }).should(
'not.exist'
)
})
it('shows compile UI when connected', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-detachRole', 'detached')
})
cy.mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
@@ -52,15 +53,11 @@ describe('<PdfPreviewHybridToolbar/>', function () {
})
})
cy.findByRole('button', { name: 'Recompile' })
cy.findByRole('button', { name: 'Recompile' }).should('exist')
cy.contains('Connecting with the editor').should('not.exist')
})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('shows connecting message when disconnected', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-detachRole', 'detached')
})
it('shows connecting message when disconnected', function () {
cy.mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
@@ -72,6 +69,11 @@ describe('<PdfPreviewHybridToolbar/>', function () {
role: 'detacher',
event: 'connected',
})
})
cy.findByRole('button', { name: 'Recompile' }).should('exist')
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'closed',
@@ -79,14 +81,27 @@ describe('<PdfPreviewHybridToolbar/>', function () {
})
cy.contains('Connecting with the editor')
cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('shows redirect button after timeout', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-detachRole', 'detached')
})
it('shows redirect button after timeout', function () {
cy.clock()
cy.mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
)
cy.contains('Connecting with the editor')
cy.tick(6000)
cy.findByRole('button', { name: 'Redirect to editor' }).should('exist')
cy.contains('Connecting with the editor').should('not.exist')
})
it('recovers to compile UI when link is restored after timeout', function () {
cy.clock()
cy.mount(
@@ -96,8 +111,42 @@ describe('<PdfPreviewHybridToolbar/>', function () {
)
cy.tick(6000)
cy.findByRole('button', { name: 'Redirect to editor' }).should('exist')
cy.findByRole('button', { name: 'Redirect to editor' })
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'connected',
})
})
cy.findByRole('button', { name: 'Recompile' }).should('exist')
cy.findByRole('button', { name: 'Redirect to editor' }).should(
'not.exist'
)
})
})
describe('detacher role', function () {
it('never shows orphan UI', function () {
cy.clock()
cy.window().then(win => {
win.metaAttributesCache.set('ol-detachRole', 'detacher')
})
cy.mount(
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
)
cy.tick(6000)
cy.findByRole('button', { name: 'Recompile' }).should('exist')
cy.contains('Connecting with the editor').should('not.exist')
cy.findByRole('button', { name: 'Redirect to editor' }).should(
'not.exist'
)
})
})
})