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

Select project root folder

GitOrigin-RevId: 146bf9dcbfbd037c51529b80104495bd95922471
This commit is contained in:
ilkin-overleaf 2023-08-14 14:38:09 +03:00 committed by Copybot
parent acf87abb80
commit d8e2c10257
12 changed files with 159 additions and 24 deletions

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,4 +31,8 @@ export type HistoryContextValue = {
>
fetchNextBatchOfUpdates: () => (() => void) | void
resetSelection: () => void
shouldShowVisualSelection: boolean
setShouldShowVisualSelection: React.Dispatch<
React.SetStateAction<HistoryContextValue['shouldShowVisualSelection']>
>
}

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

View file

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