Răsfoiți Sursa

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

[web] Select project root folder

GitOrigin-RevId: 78272a6af16879e9c71c9167ba066c258b0ff7b7
Alf Eaton 3 ani în urmă
părinte
comite
3ae32f8332

+ 9 - 1
services/web/frontend/js/features/file-tree/components/file-tree-doc.js

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

+ 4 - 0
services/web/frontend/js/features/file-tree/components/file-tree-folder-list.js

@@ -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 - 2
services/web/frontend/js/features/file-tree/components/file-tree-folder.js

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

+ 27 - 4
services/web/frontend/js/features/file-tree/components/file-tree-root.js

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

+ 8 - 2
services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js

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

+ 4 - 0
services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx

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

+ 7 - 1
services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx

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

+ 22 - 1
services/web/frontend/js/features/history/components/history-file-tree.tsx

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

+ 1 - 1
services/web/frontend/js/features/history/components/history-root.tsx

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

+ 0 - 2
services/web/frontend/stylesheets/app/editor/file-tree.less

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

+ 0 - 2
services/web/frontend/stylesheets/app/editor/history-react.less

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

+ 34 - 0
services/web/test/frontend/features/file-tree/components/file-tree-root.test.js

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