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

Merge pull request #7066 from overleaf/ta-synctex-disable-multi

Disable Synctex Controls for Docs and Multiselections

GitOrigin-RevId: 9f14e68228f9aa13a4188918930fc8cdb5eefabf
ilkin-overleaf 4 лет назад
Родитель
Сommit
57e60c05ca

+ 11 - 4
services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js

@@ -86,7 +86,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
     rootDocId
   )
 
-  const { fileTreeData } = useFileTreeData()
+  const { fileTreeData, setSelectedEntities } = useFileTreeData()
 
   const [selectedEntityIds, dispatch] = useReducer(
     permissionsLevel === 'readOnly'
@@ -129,11 +129,18 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
     if (_.isEqual(selectedEntityIds, previousSelectedEntityIds)) {
       return
     }
-    const selectedEntities = Array.from(selectedEntityIds)
+    const _selectedEntities = Array.from(selectedEntityIds)
       .map(id => findInTree(fileTreeData, id))
       .filter(Boolean)
-    onSelect(selectedEntities)
-  }, [fileTreeData, selectedEntityIds, previousSelectedEntityIds, onSelect])
+    onSelect(_selectedEntities)
+    setSelectedEntities(_selectedEntities)
+  }, [
+    fileTreeData,
+    selectedEntityIds,
+    previousSelectedEntityIds,
+    onSelect,
+    setSelectedEntities,
+  ])
 
   useEffect(() => {
     // listen for `editor.openDoc` and selected that doc

+ 25 - 3
services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.js

@@ -1,5 +1,5 @@
 import classNames from 'classnames'
-import { memo, useCallback, useEffect, useState } from 'react'
+import { memo, useCallback, useEffect, useState, useMemo } from 'react'
 import PropTypes from 'prop-types'
 import { useIdeContext } from '../../../shared/context/ide-context'
 import { useProjectContext } from '../../../shared/context/project-context'
@@ -15,12 +15,14 @@ import useAbortController from '../../../shared/hooks/use-abort-controller'
 import useDetachState from '../../../shared/hooks/use-detach-state'
 import useDetachAction from '../../../shared/hooks/use-detach-action'
 import localStorage from '../../../infrastructure/local-storage'
+import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 
 function GoToCodeButton({
   position,
   syncToCode,
   syncToCodeInFlight,
   isDetachLayout,
+  hasSingleSelectedDoc,
 }) {
   const { t } = useTranslation()
   const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
@@ -48,7 +50,7 @@ function GoToCodeButton({
         bsStyle="default"
         bsSize="xs"
         onClick={() => syncToCode(position, 72)}
-        disabled={syncToCodeInFlight}
+        disabled={syncToCodeInFlight || !hasSingleSelectedDoc}
         className={buttonClasses}
         aria-label={t('go_to_pdf_location_in_code')}
       >
@@ -64,6 +66,7 @@ function GoToPdfButton({
   syncToPdf,
   syncToPdfInFlight,
   isDetachLayout,
+  hasSingleSelectedDoc,
 }) {
   const { t } = useTranslation()
   const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
@@ -91,7 +94,7 @@ function GoToPdfButton({
         bsStyle="default"
         bsSize="xs"
         onClick={() => syncToPdf(cursorPosition)}
-        disabled={syncToPdfInFlight || !cursorPosition}
+        disabled={syncToPdfInFlight || !cursorPosition || !hasSingleSelectedDoc}
         className={buttonClasses}
         aria-label={t('go_to_code_location_in_pdf')}
       >
@@ -118,6 +121,8 @@ function PdfSynctexControls() {
     setHighlights,
   } = useCompileContext()
 
+  const { selectedEntities } = useFileTreeData()
+
   const [cursorPosition, setCursorPosition] = useState(() => {
     const position = localStorage.getItem(
       `doc.position.${ide.editorManager.getCurrentDocId()}`
@@ -320,6 +325,17 @@ function PdfSynctexControls() {
     }
   }, [syncToCode])
 
+  const hasSingleSelectedDoc = useMemo(() => {
+    if (selectedEntities.length !== 1) {
+      return false
+    }
+
+    if (selectedEntities[0].type !== 'doc') {
+      return false
+    }
+    return true
+  }, [selectedEntities])
+
   if (!position) {
     return null
   }
@@ -336,6 +352,7 @@ function PdfSynctexControls() {
           syncToPdf={syncToPdf}
           syncToPdfInFlight={syncToPdfInFlight}
           isDetachLayout
+          hasSingleSelectedDoc={hasSingleSelectedDoc}
         />
       </>
     )
@@ -347,6 +364,7 @@ function PdfSynctexControls() {
           syncToCode={syncToCode}
           syncToCodeInFlight={syncToCodeInFlight}
           isDetachLayout
+          hasSingleSelectedDoc={hasSingleSelectedDoc}
         />
       </>
     )
@@ -357,12 +375,14 @@ function PdfSynctexControls() {
           cursorPosition={cursorPosition}
           syncToPdf={syncToPdf}
           syncToPdfInFlight={syncToPdfInFlight}
+          hasSingleSelectedDoc={hasSingleSelectedDoc}
         />
 
         <GoToCodeButton
           position={position}
           syncToCode={syncToCode}
           syncToCodeInFlight={syncToCodeInFlight}
+          hasSingleSelectedDoc={hasSingleSelectedDoc}
         />
       </>
     )
@@ -376,6 +396,7 @@ GoToCodeButton.propTypes = {
   position: PropTypes.object.isRequired,
   syncToCode: PropTypes.func.isRequired,
   syncToCodeInFlight: PropTypes.bool.isRequired,
+  hasSingleSelectedDoc: PropTypes.bool.isRequired,
 }
 
 GoToPdfButton.propTypes = {
@@ -383,4 +404,5 @@ GoToPdfButton.propTypes = {
   isDetachLayout: PropTypes.bool,
   syncToPdf: PropTypes.func.isRequired,
   syncToPdfInFlight: PropTypes.bool.isRequired,
+  hasSingleSelectedDoc: PropTypes.bool.isRequired,
 }

+ 7 - 0
services/web/frontend/js/shared/context/file-tree-data-context.js

@@ -4,6 +4,7 @@ import {
   useReducer,
   useContext,
   useMemo,
+  useState,
 } from 'react'
 import PropTypes from 'prop-types'
 import useScopeValue from '../hooks/use-scope-value'
@@ -144,6 +145,8 @@ export function FileTreeDataProvider({ children }) {
     initialState
   )
 
+  const [selectedEntities, setSelectedEntities] = useState([])
+
   useDeepCompareEffect(() => {
     dispatch({
       type: ACTION_TYPES.RESET,
@@ -205,6 +208,8 @@ export function FileTreeDataProvider({ children }) {
       fileCount,
       fileTreeData,
       hasFolders: fileTreeData?.folders.length > 0,
+      selectedEntities,
+      setSelectedEntities,
     }
   }, [
     dispatchCreateDoc,
@@ -215,6 +220,8 @@ export function FileTreeDataProvider({ children }) {
     dispatchRename,
     fileCount,
     fileTreeData,
+    selectedEntities,
+    setSelectedEntities,
   ])
 
   return (

+ 117 - 25
services/web/test/frontend/features/pdf-preview/components/pdf-synctex-controls.test.js

@@ -8,6 +8,7 @@ import fs from 'fs'
 import path from 'path'
 import { expect } from 'chai'
 import { useCompileContext } from '../../../../../frontend/js/shared/context/compile-context'
+import { useFileTreeData } from '../../../../../frontend/js/shared/context/file-tree-data-context'
 import { useEffect } from 'react'
 
 const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf')
@@ -84,6 +85,14 @@ const mockHighlights = [
   },
 ]
 
+const mockPosition = {
+  page: 1,
+  offset: { top: 10, left: 10 },
+  pageSize: { height: 500, width: 500 },
+}
+
+const mockSelectedEntities = [{ type: 'doc' }]
+
 const mockSynctex = () =>
   fetchMock
     .get('express:/project/:projectId/sync/code', () => {
@@ -93,6 +102,27 @@ const mockSynctex = () =>
       return { code: [{ file: 'main.tex', line: 100 }] }
     })
 
+const WithPosition = ({ mockPosition }) => {
+  const { setPosition } = useCompileContext()
+
+  // mock PDF scroll position update
+  useEffect(() => {
+    setPosition(mockPosition)
+  }, [mockPosition, setPosition])
+
+  return null
+}
+
+const WithSelectedEntities = ({ mockSelectedEntities = [] }) => {
+  const { setSelectedEntities } = useFileTreeData()
+
+  useEffect(() => {
+    setSelectedEntities(mockSelectedEntities)
+  }, [mockSelectedEntities, setSelectedEntities])
+
+  return null
+}
+
 describe('<PdfSynctexControls/>', function () {
   beforeEach(function () {
     window.metaAttributesCache = new Map()
@@ -108,24 +138,10 @@ describe('<PdfSynctexControls/>', function () {
   })
 
   it('handles clicks on sync buttons', async function () {
-    const Inner = () => {
-      const { setPosition } = useCompileContext()
-
-      // mock PDF scroll position update
-      useEffect(() => {
-        setPosition({
-          page: 1,
-          offset: { top: 10, left: 10 },
-          pageSize: { height: 500, width: 500 },
-        })
-      }, [setPosition])
-
-      return null
-    }
-
     const { container } = renderWithEditorContext(
       <>
-        <Inner />
+        <WithPosition mockPosition={mockPosition} />
+        <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
         <PdfSynctexControls />
       </>,
       { scope }
@@ -170,6 +186,50 @@ describe('<PdfSynctexControls/>', function () {
     })
   })
 
+  it('disables button when multiple entities are selected', async function () {
+    renderWithEditorContext(
+      <>
+        <WithPosition mockPosition={mockPosition} />
+        <WithSelectedEntities
+          mockSelectedEntities={[{ type: 'doc' }, { type: 'doc' }]}
+        />
+        <PdfSynctexControls />
+      </>,
+      { scope }
+    )
+
+    const syncToPdfButton = await screen.findByRole('button', {
+      name: 'Go to code location in PDF',
+    })
+    expect(syncToPdfButton.disabled).to.be.true
+
+    const syncToCodeButton = await screen.findByRole('button', {
+      name: /Go to PDF location in code/,
+    })
+    expect(syncToCodeButton.disabled).to.be.true
+  })
+
+  it('disables button when a file is selected', async function () {
+    renderWithEditorContext(
+      <>
+        <WithPosition mockPosition={mockPosition} />
+        <WithSelectedEntities mockSelectedEntities={[{ type: 'file' }]} />
+        <PdfSynctexControls />
+      </>,
+      { scope }
+    )
+
+    const syncToPdfButton = await screen.findByRole('button', {
+      name: 'Go to code location in PDF',
+    })
+    expect(syncToPdfButton.disabled).to.be.true
+
+    const syncToCodeButton = await screen.findByRole('button', {
+      name: /Go to PDF location in code/,
+    })
+    expect(syncToCodeButton.disabled).to.be.true
+  })
+
   describe('with detacher role', async function () {
     beforeEach(function () {
       window.metaAttributesCache.set('ol-detachRole', 'detacher')
@@ -190,7 +250,15 @@ describe('<PdfSynctexControls/>', function () {
     })
 
     it('send go to PDF location action', async function () {
-      renderWithEditorContext(<PdfSynctexControls />, { scope })
+      renderWithEditorContext(
+        <>
+          <WithPosition mockPosition={mockPosition} />
+          <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
+          <PdfSynctexControls />
+        </>,
+        { scope }
+      )
+
       sysendTestHelper.resetHistory()
 
       const syncToPdfButton = await screen.findByRole('button', {
@@ -218,9 +286,14 @@ describe('<PdfSynctexControls/>', function () {
     })
 
     it('update inflight state', async function () {
-      const { container } = renderWithEditorContext(<PdfSynctexControls />, {
-        scope,
-      })
+      const { container } = renderWithEditorContext(
+        <>
+          <WithPosition mockPosition={mockPosition} />
+          <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
+          <PdfSynctexControls />
+        </>,
+        { scope }
+      )
       sysendTestHelper.resetHistory()
 
       const syncToPdfButton = await screen.findByRole('button', {
@@ -277,9 +350,14 @@ describe('<PdfSynctexControls/>', function () {
     })
 
     it('send go to code line action and update inflight state', async function () {
-      const { container } = renderWithEditorContext(<PdfSynctexControls />, {
-        scope,
-      })
+      const { container } = renderWithEditorContext(
+        <>
+          <WithPosition mockPosition={mockPosition} />
+          <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
+          <PdfSynctexControls />
+        </>,
+        { scope }
+      )
       sysendTestHelper.resetHistory()
 
       const syncToCodeButton = await screen.findByRole('button', {
@@ -313,7 +391,14 @@ describe('<PdfSynctexControls/>', function () {
     })
 
     it('sends PDF exists state', async function () {
-      renderWithEditorContext(<PdfSynctexControls />, { scope })
+      renderWithEditorContext(
+        <>
+          <WithPosition mockPosition={mockPosition} />
+          <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
+          <PdfSynctexControls />
+        </>,
+        { scope }
+      )
       sysendTestHelper.resetHistory()
 
       await waitFor(() => {
@@ -328,7 +413,14 @@ describe('<PdfSynctexControls/>', function () {
     })
 
     it('reacts to go to PDF location action', async function () {
-      renderWithEditorContext(<PdfSynctexControls />, { scope })
+      renderWithEditorContext(
+        <>
+          <WithPosition mockPosition={mockPosition} />
+          <WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
+          <PdfSynctexControls />
+        </>,
+        { scope }
+      )
       sysendTestHelper.resetHistory()
 
       await waitFor(() => {