Merge pull request #14197 from overleaf/ii-filetree-empty-space-click

[web] Select project root folder

GitOrigin-RevId: 78272a6af16879e9c71c9167ba066c258b0ff7b7
This commit is contained in:
Alf Eaton 2023-08-11 09:08:13 +01:00 committed by Copybot
parent 06c3de586a
commit 3ae32f8332
12 changed files with 130 additions and 16 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}
</>
)

View file

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

View file

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

View file

@ -90,8 +90,6 @@
ul.file-tree-list {
margin: 0;
overflow-x: hidden;
height: 100%;
flex-grow: 1;
position: relative;
overflow-y: auto;

View file

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

View file

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