Просмотр исходного кода

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

GitOrigin-RevId: 07a58d6f7e3c6db8465c62b390e34270c2b4fd67
Mathias Jakobsen 3 месяцев назад
Родитель
Сommit
c46fba951e

+ 2 - 0
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": "",

+ 16 - 0
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 (
+    <div className="toolbar-pdf-left">
+      <div className="toolbar-pdf-orphan">
+        <LoadingSpinner size="sm" loadingText={`${t('tab_connecting')}…`} />
+      </div>
+    </div>
+  )
+}
+
+export default memo(PdfPreviewHybridToolbarConnectingInner)

+ 29 - 0
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 (
+    <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)

+ 23 - 2
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 (
     <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>
+    </>
   )
 }
 

+ 47 - 0
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<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'
+  }
+}

+ 9 - 0
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);

+ 2 - 0
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",

+ 68 - 19
services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx

@@ -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,15 +81,58 @@ 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(
+        <EditorProviders>
+          <PdfPreviewHybridToolbar />
+        </EditorProviders>
+      )
+
+      cy.tick(6000)
+      cy.findByRole('button', { name: 'Redirect to editor' }).should('exist')
+
+      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>
@@ -97,7 +142,11 @@ describe('<PdfPreviewHybridToolbar/>', function () {
 
       cy.tick(6000)
 
-      cy.findByRole('button', { name: 'Redirect to editor' })
+      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'
+      )
     })
   })
 })