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__. If needed you can update your billing address to <1>__userEmail__.", "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' + ) }) }) })