mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #14197 from overleaf/ii-filetree-empty-space-click
[web] Select project root folder GitOrigin-RevId: 78272a6af16879e9c71c9167ba066c258b0ff7b7
This commit is contained in:
parent
06c3de586a
commit
3ae32f8332
12 changed files with 130 additions and 16 deletions
|
@ -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,17 @@ 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 = e => {
|
||||
if (e.target.classList.contains('bottom-buffer')) {
|
||||
setShouldShowVisualSelection(false)
|
||||
return
|
||||
}
|
||||
|
||||
setShouldShowVisualSelection(e.target !== e.currentTarget)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady) onInit()
|
||||
|
@ -48,8 +59,15 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
|||
{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 +77,7 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
|||
)
|
||||
})
|
||||
|
||||
function FileTreeRootFolder() {
|
||||
function FileTreeRootFolder({ shouldShowVisualSelection }) {
|
||||
useFileTreeSocketListener()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
|
||||
|
@ -75,6 +93,7 @@ function FileTreeRootFolder() {
|
|||
classes={{ root: 'file-tree-list' }}
|
||||
dropRef={dropRef}
|
||||
isOver={isOver}
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
>
|
||||
<li className="bottom-buffer" />
|
||||
</FileTreeFolderList>
|
||||
|
@ -82,6 +101,10 @@ function FileTreeRootFolder() {
|
|||
)
|
||||
}
|
||||
|
||||
FileTreeRootFolder.propTypes = {
|
||||
shouldShowVisualSelection: PropTypes.bool,
|
||||
}
|
||||
|
||||
FileTreeRoot.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onInit: PropTypes.func.isRequired,
|
||||
|
|
|
@ -150,6 +150,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])
|
||||
|
@ -202,7 +203,11 @@ 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
|
||||
|
@ -244,7 +249,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 }),
|
||||
|
|
|
@ -11,6 +11,7 @@ import { fileFinalPathname } from '../../utils/file-diff'
|
|||
type HistoryFileTreeFolderListProps = {
|
||||
folders: HistoryFileTree[]
|
||||
docs: HistoryDoc[]
|
||||
shouldShowVisualSelection: boolean
|
||||
rootClassName?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
@ -19,6 +20,7 @@ function HistoryFileTreeFolderList({
|
|||
folders,
|
||||
docs,
|
||||
rootClassName,
|
||||
shouldShowVisualSelection,
|
||||
children,
|
||||
}: HistoryFileTreeFolderListProps) {
|
||||
const { selection, setSelection } = useHistoryContext()
|
||||
|
@ -64,6 +66,7 @@ function HistoryFileTreeFolderList({
|
|||
name={folder.name}
|
||||
folders={folder.folders}
|
||||
docs={folder.docs ?? []}
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
/>
|
||||
))}
|
||||
{docs.map(doc => (
|
||||
|
@ -72,6 +75,7 @@ function HistoryFileTreeFolderList({
|
|||
name={doc.name}
|
||||
file={doc}
|
||||
selected={
|
||||
shouldShowVisualSelection &&
|
||||
!!selection.selectedFile &&
|
||||
fileFinalPathname(selection.selectedFile) === doc.pathname
|
||||
}
|
||||
|
|
|
@ -11,12 +11,14 @@ type HistoryFileTreeFolderProps = {
|
|||
name: string
|
||||
folders: HistoryFileTree[]
|
||||
docs: HistoryDoc[]
|
||||
shouldShowVisualSelection: boolean
|
||||
}
|
||||
|
||||
function HistoryFileTreeFolder({
|
||||
name,
|
||||
folders,
|
||||
docs,
|
||||
shouldShowVisualSelection,
|
||||
}: HistoryFileTreeFolderProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
@ -58,7 +60,11 @@ function HistoryFileTreeFolder({
|
|||
<HistoryFileTreeItem name={name} icons={icons} />
|
||||
</li>
|
||||
{expanded ? (
|
||||
<HistoryFileTreeFolderList folders={folders} docs={docs} />
|
||||
<HistoryFileTreeFolderList
|
||||
folders={folders}
|
||||
docs={docs}
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo } from 'react'
|
||||
import { useMemo, useEffect, useState } from 'react'
|
||||
import { orderBy, reduce } from 'lodash'
|
||||
import { useHistoryContext } from '../context/history-context'
|
||||
import {
|
||||
|
@ -6,9 +6,12 @@ import {
|
|||
reducePathsToTree,
|
||||
} from '../utils/file-tree'
|
||||
import HistoryFileTreeFolderList from './file-tree/history-file-tree-folder-list'
|
||||
import { fileTreeContainer } from './history-root'
|
||||
|
||||
export default function HistoryFileTree() {
|
||||
const { selection } = useHistoryContext()
|
||||
const [shouldShowVisualSelection, setShouldShowVisualSelection] =
|
||||
useState(true)
|
||||
|
||||
const fileTree = useMemo(
|
||||
() => reduce(selection.files, reducePathsToTree, []),
|
||||
|
@ -25,11 +28,29 @@ export default function HistoryFileTree() {
|
|||
[sortedFileTree]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const listener = function (e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).classList.contains('bottom-buffer')) {
|
||||
setShouldShowVisualSelection(false)
|
||||
return
|
||||
}
|
||||
|
||||
setShouldShowVisualSelection(e.target !== e.currentTarget)
|
||||
}
|
||||
|
||||
fileTreeContainer?.addEventListener('click', listener)
|
||||
|
||||
return () => {
|
||||
fileTreeContainer?.removeEventListener('click', listener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<HistoryFileTreeFolderList
|
||||
folders={mappedFileTree.folders}
|
||||
docs={mappedFileTree.docs ?? []}
|
||||
rootClassName="history-file-tree-list"
|
||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
||||
>
|
||||
<li className="bottom-buffer" />
|
||||
</HistoryFileTreeFolderList>
|
||||
|
|
|
@ -8,7 +8,7 @@ import LoadingSpinner from '../../../shared/components/loading-spinner'
|
|||
import { ErrorBoundaryFallback } from '../../../shared/components/error-boundary-fallback'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
|
||||
const fileTreeContainer = document.getElementById('history-file-tree')
|
||||
export const fileTreeContainer = document.getElementById('history-file-tree')
|
||||
|
||||
function Main() {
|
||||
const { view } = useLayoutContext()
|
||||
|
|
|
@ -90,8 +90,6 @@
|
|||
ul.file-tree-list {
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
|
|
|
@ -384,8 +384,6 @@ history-root {
|
|||
ul.history-file-tree-list {
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue