diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index ce76bfda87..8aaa42c297 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -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": "",
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar-connecting-inner.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar-connecting-inner.tsx
new file mode 100644
index 0000000000..f12d176eb9
--- /dev/null
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar-connecting-inner.tsx
@@ -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 (
+
+ )
+}
+
+export default memo(PdfPreviewHybridToolbarConnectingInner)
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar-orphan-refresh-inner.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar-orphan-refresh-inner.tsx
new file mode 100644
index 0000000000..56c8489fb0
--- /dev/null
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar-orphan-refresh-inner.tsx
@@ -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 (
+
+
+ {t('redirect_to_editor')}
+
+
+ )
+}
+
+export default memo(PdfPreviewHybridToolbarOrphanRefreshInner)
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar.tsx
index 90bd68035c..dc7d3343e8 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar.tsx
@@ -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 (
+
+
+ )
+}
+
+function PdfPreviewHybridToolbarInner() {
+ return (
+ <>
@@ -26,7 +47,7 @@ function PdfPreviewHybridToolbar() {
{/* TODO: should we have code check? */}
-
+ >
)
}
diff --git a/services/web/frontend/js/features/pdf-preview/hooks/use-detached-orphan-detection.ts b/services/web/frontend/js/features/pdf-preview/hooks/use-detached-orphan-detection.ts
new file mode 100644
index 0000000000..ee3d1a2559
--- /dev/null
+++ b/services/web/frontend/js/features/pdf-preview/hooks/use-detached-orphan-detection.ts
@@ -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()
+ 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'
+ }
+}
diff --git a/services/web/frontend/stylesheets/pages/editor/pdf.scss b/services/web/frontend/stylesheets/pages/editor/pdf.scss
index 85cbf7904b..f1a5e122a3 100644
--- a/services/web/frontend/stylesheets/pages/editor/pdf.scss
+++ b/services/web/frontend/stylesheets/pages/editor/pdf.scss
@@ -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);
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 2c6dce43a1..5d9e214c1b 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -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",
diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx
index 777d12f7d9..e453231961 100644
--- a/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx
+++ b/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx
@@ -15,16 +15,17 @@ describe('', function () {
)
- 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(
@@ -32,13 +33,13 @@ describe('', 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(
@@ -52,15 +53,11 @@ describe('', 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(
@@ -72,6 +69,11 @@ describe('', 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('', 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(
+
+
+
+ )
+
+ 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('', 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(
+
+
+
+ )
+
+ 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'
+ )
})
})
})