mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
be0774be8f
commit
57e60c05ca
4 changed files with 160 additions and 32 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
Loading…
Reference in a new issue