diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js
index 6cde785084..f15832b4c3 100644
--- a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js
+++ b/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
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.js b/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.js
index 7e56f04dff..d0f0a1aa5f 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.js
+++ b/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}
/>
>
)
@@ -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,
}
diff --git a/services/web/frontend/js/shared/context/file-tree-data-context.js b/services/web/frontend/js/shared/context/file-tree-data-context.js
index 387605c01f..dbf221b7fe 100644
--- a/services/web/frontend/js/shared/context/file-tree-data-context.js
+++ b/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 (
diff --git a/services/web/test/frontend/features/pdf-preview/components/pdf-synctex-controls.test.js b/services/web/test/frontend/features/pdf-preview/components/pdf-synctex-controls.test.js
index 376502bb1c..5cb5e6baec 100644
--- a/services/web/test/frontend/features/pdf-preview/components/pdf-synctex-controls.test.js
+++ b/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('', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
@@ -108,24 +138,10 @@ describe('', 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(
<>
-
+
+
>,
{ scope }
@@ -170,6 +186,50 @@ describe('', function () {
})
})
+ it('disables button when multiple entities are selected', async function () {
+ renderWithEditorContext(
+ <>
+
+
+
+ >,
+ { 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(
+ <>
+
+
+
+ >,
+ { 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('', function () {
})
it('send go to PDF location action', async function () {
- renderWithEditorContext(, { scope })
+ renderWithEditorContext(
+ <>
+
+
+
+ >,
+ { scope }
+ )
+
sysendTestHelper.resetHistory()
const syncToPdfButton = await screen.findByRole('button', {
@@ -218,9 +286,14 @@ describe('', function () {
})
it('update inflight state', async function () {
- const { container } = renderWithEditorContext(, {
- scope,
- })
+ const { container } = renderWithEditorContext(
+ <>
+
+
+
+ >,
+ { scope }
+ )
sysendTestHelper.resetHistory()
const syncToPdfButton = await screen.findByRole('button', {
@@ -277,9 +350,14 @@ describe('', function () {
})
it('send go to code line action and update inflight state', async function () {
- const { container } = renderWithEditorContext(, {
- scope,
- })
+ const { container } = renderWithEditorContext(
+ <>
+
+
+
+ >,
+ { scope }
+ )
sysendTestHelper.resetHistory()
const syncToCodeButton = await screen.findByRole('button', {
@@ -313,7 +391,14 @@ describe('', function () {
})
it('sends PDF exists state', async function () {
- renderWithEditorContext(, { scope })
+ renderWithEditorContext(
+ <>
+
+
+
+ >,
+ { scope }
+ )
sysendTestHelper.resetHistory()
await waitFor(() => {
@@ -328,7 +413,14 @@ describe('', function () {
})
it('reacts to go to PDF location action', async function () {
- renderWithEditorContext(, { scope })
+ renderWithEditorContext(
+ <>
+
+
+
+ >,
+ { scope }
+ )
sysendTestHelper.resetHistory()
await waitFor(() => {