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

Disable Synctex Controls for Docs and Multiselections

GitOrigin-RevId: 9f14e68228f9aa13a4188918930fc8cdb5eefabf
This commit is contained in:
ilkin-overleaf 2022-03-21 16:47:01 +02:00 committed by Copybot
parent be0774be8f
commit 57e60c05ca
4 changed files with 160 additions and 32 deletions

View file

@ -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

View file

@ -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,
}

View file

@ -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 (

View file

@ -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(() => {