mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #14291 from overleaf/ii-filetree-empty-space-click-2
Select project root folder GitOrigin-RevId: 146bf9dcbfbd037c51529b80104495bd95922471
This commit is contained in:
parent
acf87abb80
commit
d8e2c10257
12 changed files with 159 additions and 24 deletions
|
@ -15,6 +15,7 @@ function FileTreeContext({
|
|||
reindexReferences,
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
setShouldShowVisualSelection,
|
||||
onSelect,
|
||||
children,
|
||||
}) {
|
||||
|
@ -23,9 +24,13 @@ function FileTreeContext({
|
|||
refProviders={refProviders}
|
||||
setRefProviderEnabled={setRefProviderEnabled}
|
||||
setStartedFreeTrial={setStartedFreeTrial}
|
||||
setShouldShowVisualSelection={setShouldShowVisualSelection}
|
||||
reindexReferences={reindexReferences}
|
||||
>
|
||||
<FileTreeSelectableProvider onSelect={onSelect}>
|
||||
<FileTreeSelectableProvider
|
||||
onSelect={onSelect}
|
||||
setShouldShowVisualSelection={setShouldShowVisualSelection}
|
||||
>
|
||||
<FileTreeActionableProvider>
|
||||
<FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
|
||||
</FileTreeActionableProvider>
|
||||
|
@ -39,6 +44,7 @@ FileTreeContext.propTypes = {
|
|||
refProviders: PropTypes.object.isRequired,
|
||||
setRefProviderEnabled: PropTypes.func.isRequired,
|
||||
setStartedFreeTrial: PropTypes.func.isRequired,
|
||||
setShouldShowVisualSelection: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
|
|
|
@ -8,9 +8,16 @@ import Icon from '../../../shared/components/icon'
|
|||
import iconTypeFromName from '../util/icon-type-from-name'
|
||||
import classnames from 'classnames'
|
||||
|
||||
function FileTreeDoc({ name, id, isFile, isLinkedFile }) {
|
||||
function FileTreeDoc({
|
||||
name,
|
||||
id,
|
||||
isFile,
|
||||
isLinkedFile,
|
||||
shouldShowVisualSelection,
|
||||
}) {
|
||||
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
|
||||
id,
|
||||
shouldShowVisualSelection,
|
||||
isFile
|
||||
)
|
||||
|
||||
|
@ -38,6 +45,7 @@ FileTreeDoc.propTypes = {
|
|||
id: PropTypes.string.isRequired,
|
||||
isFile: PropTypes.bool,
|
||||
isLinkedFile: PropTypes.bool,
|
||||
shouldShowVisualSelection: PropTypes.bool,
|
||||
}
|
||||
|
||||
export const FileTreeIcon = ({ isLinkedFile, name }) => {
|
||||
|
|
|
@ -12,6 +12,7 @@ function FileTreeFolderList({
|
|||
classes = {},
|
||||
dropRef = null,
|
||||
children,
|
||||
shouldShowVisualSelection,
|
||||
}) {
|
||||
files = files.map(file => ({ ...file, isFile: true }))
|
||||
const docsAndFiles = [...docs, ...files]
|
||||
|
@ -32,6 +33,7 @@ function FileTreeFolderList({
|
|||
folders={folder.folders}
|
||||
docs={folder.docs}
|
||||
files={folder.fileRefs}
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
@ -43,6 +45,7 @@ function FileTreeFolderList({
|
|||
id={doc._id}
|
||||
isFile={doc.isFile}
|
||||
isLinkedFile={doc.linkedFileData && !!doc.linkedFileData.provider}
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
@ -60,6 +63,7 @@ FileTreeFolderList.propTypes = {
|
|||
}),
|
||||
dropRef: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
shouldShowVisualSelection: PropTypes.bool,
|
||||
}
|
||||
|
||||
function compareFunction(one, two) {
|
||||
|
|
|
@ -14,10 +14,20 @@ import FileTreeItemInner from './file-tree-item/file-tree-item-inner'
|
|||
import FileTreeFolderList from './file-tree-folder-list'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
|
||||
function FileTreeFolder({ name, id, folders, docs, files }) {
|
||||
function FileTreeFolder({
|
||||
name,
|
||||
id,
|
||||
folders,
|
||||
docs,
|
||||
files,
|
||||
shouldShowVisualSelection,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { isSelected, props: selectableEntityProps } = useSelectableEntity(id)
|
||||
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
|
||||
id,
|
||||
shouldShowVisualSelection
|
||||
)
|
||||
|
||||
const { selectedEntityParentIds } = useFileTreeSelectable(id)
|
||||
|
||||
|
@ -87,6 +97,7 @@ function FileTreeFolder({ name, id, folders, docs, files }) {
|
|||
docs={docs}
|
||||
files={files}
|
||||
dropRef={dropRefList}
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -99,6 +110,7 @@ FileTreeFolder.propTypes = {
|
|||
folders: PropTypes.array.isRequired,
|
||||
docs: PropTypes.array.isRequired,
|
||||
files: PropTypes.array.isRequired,
|
||||
shouldShowVisualSelection: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default FileTreeFolder
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
|
@ -31,6 +31,12 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
|||
const { _id: projectId } = useProjectContext(projectContextPropTypes)
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const isReady = projectId && fileTreeData
|
||||
const [shouldShowVisualSelection, setShouldShowVisualSelection] =
|
||||
useState(true)
|
||||
|
||||
const handleFileTreeClick = () => {
|
||||
setShouldShowVisualSelection(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady) onInit()
|
||||
|
@ -42,14 +48,22 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
|||
refProviders={refProviders}
|
||||
setRefProviderEnabled={setRefProviderEnabled}
|
||||
setStartedFreeTrial={setStartedFreeTrial}
|
||||
setShouldShowVisualSelection={setShouldShowVisualSelection}
|
||||
reindexReferences={reindexReferences}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{isConnected ? null : <div className="disconnected-overlay" />}
|
||||
<FileTreeToolbar />
|
||||
<FileTreeContextMenu />
|
||||
<div className="file-tree-inner">
|
||||
<FileTreeRootFolder />
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className="file-tree-inner"
|
||||
onClick={handleFileTreeClick}
|
||||
data-testid="file-tree-inner"
|
||||
>
|
||||
<FileTreeRootFolder
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
/>
|
||||
</div>
|
||||
<FileTreeModalDelete />
|
||||
<FileTreeModalCreateFile />
|
||||
|
@ -59,7 +73,7 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
|||
)
|
||||
})
|
||||
|
||||
function FileTreeRootFolder() {
|
||||
function FileTreeRootFolder({ shouldShowVisualSelection }) {
|
||||
useFileTreeSocketListener()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
|
||||
|
@ -75,6 +89,7 @@ function FileTreeRootFolder() {
|
|||
classes={{ root: 'file-tree-list' }}
|
||||
dropRef={dropRef}
|
||||
isOver={isOver}
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
>
|
||||
<li className="bottom-buffer" />
|
||||
</FileTreeFolderList>
|
||||
|
@ -82,6 +97,10 @@ function FileTreeRootFolder() {
|
|||
)
|
||||
}
|
||||
|
||||
FileTreeRootFolder.propTypes = {
|
||||
shouldShowVisualSelection: PropTypes.bool,
|
||||
}
|
||||
|
||||
FileTreeRoot.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onInit: PropTypes.func.isRequired,
|
||||
|
|
|
@ -75,7 +75,11 @@ function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) {
|
|||
}
|
||||
}
|
||||
|
||||
export function FileTreeSelectableProvider({ onSelect, children }) {
|
||||
export function FileTreeSelectableProvider({
|
||||
onSelect,
|
||||
setShouldShowVisualSelection,
|
||||
children,
|
||||
}) {
|
||||
const { _id: projectId, rootDocId } = useProjectContext(
|
||||
projectContextPropTypes
|
||||
)
|
||||
|
@ -150,6 +154,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
|
|||
|
||||
dispatch({ type: ACTION_TYPES.SELECT, id: found.entity._id })
|
||||
}
|
||||
|
||||
window.addEventListener('editor.openDoc', handleOpenDoc)
|
||||
return () => window.removeEventListener('editor.openDoc', handleOpenDoc)
|
||||
}, [fileTreeData])
|
||||
|
@ -176,6 +181,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
|
|||
select,
|
||||
unselect,
|
||||
selectOrMultiSelectEntity,
|
||||
setShouldShowVisualSelection,
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -187,6 +193,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
|
|||
|
||||
FileTreeSelectableProvider.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
setShouldShowVisualSelection: PropTypes.func.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
|
@ -202,20 +209,34 @@ const editorContextPropTypes = {
|
|||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}
|
||||
|
||||
export function useSelectableEntity(id, isFile) {
|
||||
export function useSelectableEntity(
|
||||
id,
|
||||
shouldShowVisualSelection = true,
|
||||
isFile
|
||||
) {
|
||||
const { view, setView } = useLayoutContext(layoutContextPropTypes)
|
||||
const { selectedEntityIds, selectOrMultiSelectEntity } = useContext(
|
||||
FileTreeSelectableContext
|
||||
)
|
||||
const {
|
||||
selectedEntityIds,
|
||||
selectOrMultiSelectEntity,
|
||||
setShouldShowVisualSelection,
|
||||
} = useContext(FileTreeSelectableContext)
|
||||
|
||||
const isSelected = selectedEntityIds.has(id)
|
||||
|
||||
const handleEvent = useCallback(
|
||||
ev => {
|
||||
ev.stopPropagation()
|
||||
setShouldShowVisualSelection(true)
|
||||
selectOrMultiSelectEntity(id, ev.ctrlKey || ev.metaKey)
|
||||
setView(isFile ? 'file' : 'editor')
|
||||
},
|
||||
[id, selectOrMultiSelectEntity, setView, isFile]
|
||||
[
|
||||
id,
|
||||
setShouldShowVisualSelection,
|
||||
selectOrMultiSelectEntity,
|
||||
setView,
|
||||
isFile,
|
||||
]
|
||||
)
|
||||
|
||||
const handleClick = useCallback(
|
||||
|
@ -244,7 +265,8 @@ export function useSelectableEntity(id, isFile) {
|
|||
[id, handleEvent, selectedEntityIds]
|
||||
)
|
||||
|
||||
const isVisuallySelected = isSelected && view !== 'pdf'
|
||||
const isVisuallySelected =
|
||||
shouldShowVisualSelection && isSelected && view !== 'pdf'
|
||||
const props = useMemo(
|
||||
() => ({
|
||||
className: classNames({ selected: isVisuallySelected }),
|
||||
|
|
|
@ -9,7 +9,7 @@ type HistoryFileTreeDocProps = {
|
|||
file: FileDiff
|
||||
name: string
|
||||
selected: boolean
|
||||
onClick: (file: FileDiff) => void
|
||||
onClick: (file: FileDiff, event: React.MouseEvent<HTMLLIElement>) => void
|
||||
onKeyDown: (file: FileDiff, event: React.KeyboardEvent<HTMLLIElement>) => void
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ function HistoryFileTreeDoc({
|
|||
<li
|
||||
role="treeitem"
|
||||
className={classNames({ selected })}
|
||||
onClick={() => onClick(file)}
|
||||
onClick={e => onClick(file, e)}
|
||||
onKeyDown={e => onKeyDown(file, e)}
|
||||
aria-selected={selected}
|
||||
aria-label={name}
|
||||
|
|
|
@ -21,10 +21,21 @@ function HistoryFileTreeFolderList({
|
|||
rootClassName,
|
||||
children,
|
||||
}: HistoryFileTreeFolderListProps) {
|
||||
const { selection, setSelection } = useHistoryContext()
|
||||
const {
|
||||
selection,
|
||||
setSelection,
|
||||
shouldShowVisualSelection,
|
||||
setShouldShowVisualSelection,
|
||||
} = useHistoryContext()
|
||||
|
||||
const handleTopLevelClick = () => {
|
||||
setShouldShowVisualSelection(false)
|
||||
}
|
||||
|
||||
const handleEvent = useCallback(
|
||||
(file: FileDiff) => {
|
||||
(file: FileDiff, event: React.UIEvent) => {
|
||||
event.stopPropagation()
|
||||
setShouldShowVisualSelection(true)
|
||||
setSelection(prevSelection => {
|
||||
if (file.pathname !== prevSelection.selectedFile?.pathname) {
|
||||
return {
|
||||
|
@ -37,12 +48,12 @@ function HistoryFileTreeFolderList({
|
|||
return prevSelection
|
||||
})
|
||||
},
|
||||
[setSelection]
|
||||
[setSelection, setShouldShowVisualSelection]
|
||||
)
|
||||
|
||||
const handleClick = useCallback(
|
||||
(file: FileDiff) => {
|
||||
handleEvent(file)
|
||||
(file: FileDiff, event: React.MouseEvent<HTMLLIElement>) => {
|
||||
handleEvent(file, event)
|
||||
},
|
||||
[handleEvent]
|
||||
)
|
||||
|
@ -50,14 +61,19 @@ function HistoryFileTreeFolderList({
|
|||
const handleKeyDown = useCallback(
|
||||
(file: FileDiff, event: React.KeyboardEvent<HTMLLIElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
handleEvent(file)
|
||||
handleEvent(file, event)
|
||||
}
|
||||
},
|
||||
[handleEvent]
|
||||
)
|
||||
|
||||
return (
|
||||
<ul className={classNames('list-unstyled', rootClassName)} role="tree">
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<ul
|
||||
className={classNames('list-unstyled', rootClassName)}
|
||||
role="tree"
|
||||
onClick={handleTopLevelClick}
|
||||
>
|
||||
{folders.map(folder => (
|
||||
<HistoryFileTreeFolder
|
||||
key={folder.name}
|
||||
|
@ -72,6 +88,7 @@ function HistoryFileTreeFolderList({
|
|||
name={doc.name}
|
||||
file={doc}
|
||||
selected={
|
||||
shouldShowVisualSelection &&
|
||||
!!selection.selectedFile &&
|
||||
fileFinalPathname(selection.selectedFile) === doc.pathname
|
||||
}
|
||||
|
|
|
@ -88,6 +88,8 @@ function useHistory() {
|
|||
const currentUserIsOwner = projectOwnerId === userId
|
||||
|
||||
const [selection, setSelection] = useState<Selection>(selectionInitialState)
|
||||
const [shouldShowVisualSelection, setShouldShowVisualSelection] =
|
||||
useState(true)
|
||||
|
||||
const [updatesInfo, setUpdatesInfo] = useState<
|
||||
HistoryContextValue['updatesInfo']
|
||||
|
@ -330,6 +332,8 @@ function useHistory() {
|
|||
setSelection,
|
||||
fetchNextBatchOfUpdates,
|
||||
resetSelection,
|
||||
shouldShowVisualSelection,
|
||||
setShouldShowVisualSelection,
|
||||
}),
|
||||
[
|
||||
loadingFileDiffs,
|
||||
|
@ -346,6 +350,8 @@ function useHistory() {
|
|||
setSelection,
|
||||
fetchNextBatchOfUpdates,
|
||||
resetSelection,
|
||||
shouldShowVisualSelection,
|
||||
setShouldShowVisualSelection,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -31,4 +31,8 @@ export type HistoryContextValue = {
|
|||
>
|
||||
fetchNextBatchOfUpdates: () => (() => void) | void
|
||||
resetSelection: () => void
|
||||
shouldShowVisualSelection: boolean
|
||||
setShouldShowVisualSelection: React.Dispatch<
|
||||
React.SetStateAction<HistoryContextValue['shouldShowVisualSelection']>
|
||||
>
|
||||
}
|
||||
|
|
|
@ -305,4 +305,38 @@ describe('<FileTreeRoot/>', function () {
|
|||
// multiple items selected: no menu button is visible
|
||||
expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(0)
|
||||
})
|
||||
|
||||
it('deselects files when clicked outside the list but inside wrapping container', function () {
|
||||
const rootFolder = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [{ _id: '456def', name: 'main.tex' }],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
]
|
||||
renderWithEditorContext(
|
||||
<FileTreeRoot
|
||||
refProviders={{}}
|
||||
reindexReferences={() => null}
|
||||
setRefProviderEnabled={() => null}
|
||||
setStartedFreeTrial={() => null}
|
||||
onSelect={onSelect}
|
||||
onInit={onInit}
|
||||
isConnected
|
||||
/>,
|
||||
{
|
||||
rootFolder,
|
||||
projectId: '123abc',
|
||||
rootDocId: '456def',
|
||||
features: {},
|
||||
permissionsLevel: 'owner',
|
||||
}
|
||||
)
|
||||
|
||||
screen.getByRole('treeitem', { selected: true })
|
||||
fireEvent.click(screen.getByTestId('file-tree-inner'))
|
||||
expect(screen.queryByRole('treeitem', { selected: true })).to.be.null
|
||||
})
|
||||
})
|
||||
|
|
|
@ -24,6 +24,7 @@ export default (children, options = {}) => {
|
|||
setStartedFreeTrial: () => {
|
||||
console.log('started free trial')
|
||||
},
|
||||
setShouldShowVisualSelection: () => {},
|
||||
onSelect: () => {},
|
||||
...contextProps,
|
||||
}
|
||||
|
@ -32,6 +33,7 @@ export default (children, options = {}) => {
|
|||
reindexReferences,
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
setShouldShowVisualSelection,
|
||||
onSelect,
|
||||
...editorContextProps
|
||||
} = contextProps
|
||||
|
@ -41,6 +43,7 @@ export default (children, options = {}) => {
|
|||
reindexReferences={reindexReferences}
|
||||
setRefProviderEnabled={setRefProviderEnabled}
|
||||
setStartedFreeTrial={setStartedFreeTrial}
|
||||
setShouldShowVisualSelection={setShouldShowVisualSelection}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{children}
|
||||
|
|
Loading…
Reference in a new issue