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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import { expect } from 'chai' import { expect } from 'chai'
import { useCompileContext } from '../../../../../frontend/js/shared/context/compile-context' import { useCompileContext } from '../../../../../frontend/js/shared/context/compile-context'
import { useFileTreeData } from '../../../../../frontend/js/shared/context/file-tree-data-context'
import { useEffect } from 'react' import { useEffect } from 'react'
const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf') 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 = () => const mockSynctex = () =>
fetchMock fetchMock
.get('express:/project/:projectId/sync/code', () => { .get('express:/project/:projectId/sync/code', () => {
@ -93,6 +102,27 @@ const mockSynctex = () =>
return { code: [{ file: 'main.tex', line: 100 }] } 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 () { describe('<PdfSynctexControls/>', function () {
beforeEach(function () { beforeEach(function () {
window.metaAttributesCache = new Map() window.metaAttributesCache = new Map()
@ -108,24 +138,10 @@ describe('<PdfSynctexControls/>', function () {
}) })
it('handles clicks on sync buttons', async 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( const { container } = renderWithEditorContext(
<> <>
<Inner /> <WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls /> <PdfSynctexControls />
</>, </>,
{ scope } { 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 () { describe('with detacher role', async function () {
beforeEach(function () { beforeEach(function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher') window.metaAttributesCache.set('ol-detachRole', 'detacher')
@ -190,7 +250,15 @@ describe('<PdfSynctexControls/>', function () {
}) })
it('send go to PDF location action', async function () { it('send go to PDF location action', async function () {
renderWithEditorContext(<PdfSynctexControls />, { scope }) renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</>,
{ scope }
)
sysendTestHelper.resetHistory() sysendTestHelper.resetHistory()
const syncToPdfButton = await screen.findByRole('button', { const syncToPdfButton = await screen.findByRole('button', {
@ -218,9 +286,14 @@ describe('<PdfSynctexControls/>', function () {
}) })
it('update inflight state', async function () { it('update inflight state', async function () {
const { container } = renderWithEditorContext(<PdfSynctexControls />, { const { container } = renderWithEditorContext(
scope, <>
}) <WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</>,
{ scope }
)
sysendTestHelper.resetHistory() sysendTestHelper.resetHistory()
const syncToPdfButton = await screen.findByRole('button', { 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 () { it('send go to code line action and update inflight state', async function () {
const { container } = renderWithEditorContext(<PdfSynctexControls />, { const { container } = renderWithEditorContext(
scope, <>
}) <WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</>,
{ scope }
)
sysendTestHelper.resetHistory() sysendTestHelper.resetHistory()
const syncToCodeButton = await screen.findByRole('button', { const syncToCodeButton = await screen.findByRole('button', {
@ -313,7 +391,14 @@ describe('<PdfSynctexControls/>', function () {
}) })
it('sends PDF exists state', async function () { it('sends PDF exists state', async function () {
renderWithEditorContext(<PdfSynctexControls />, { scope }) renderWithEditorContext(
<>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</>,
{ scope }
)
sysendTestHelper.resetHistory() sysendTestHelper.resetHistory()
await waitFor(() => { await waitFor(() => {
@ -328,7 +413,14 @@ describe('<PdfSynctexControls/>', function () {
}) })
it('reacts to go to PDF location action', async 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() sysendTestHelper.resetHistory()
await waitFor(() => { await waitFor(() => {