mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 16:23:39 -05:00
Merge pull request #14339 from overleaf/ii-filetree-empty-space-click-3
[web] Select project root folder improvements GitOrigin-RevId: 48b80f26adf239215bf04d3db95a61ef35b5cf77
This commit is contained in:
parent
aa2c200200
commit
b05f8ad7e7
13 changed files with 164 additions and 156 deletions
|
@ -15,7 +15,6 @@ function FileTreeContext({
|
||||||
reindexReferences,
|
reindexReferences,
|
||||||
setRefProviderEnabled,
|
setRefProviderEnabled,
|
||||||
setStartedFreeTrial,
|
setStartedFreeTrial,
|
||||||
setShouldShowVisualSelection,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
children,
|
children,
|
||||||
}) {
|
}) {
|
||||||
|
@ -24,13 +23,9 @@ function FileTreeContext({
|
||||||
refProviders={refProviders}
|
refProviders={refProviders}
|
||||||
setRefProviderEnabled={setRefProviderEnabled}
|
setRefProviderEnabled={setRefProviderEnabled}
|
||||||
setStartedFreeTrial={setStartedFreeTrial}
|
setStartedFreeTrial={setStartedFreeTrial}
|
||||||
setShouldShowVisualSelection={setShouldShowVisualSelection}
|
|
||||||
reindexReferences={reindexReferences}
|
reindexReferences={reindexReferences}
|
||||||
>
|
>
|
||||||
<FileTreeSelectableProvider
|
<FileTreeSelectableProvider onSelect={onSelect}>
|
||||||
onSelect={onSelect}
|
|
||||||
setShouldShowVisualSelection={setShouldShowVisualSelection}
|
|
||||||
>
|
|
||||||
<FileTreeActionableProvider>
|
<FileTreeActionableProvider>
|
||||||
<FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
|
<FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
|
||||||
</FileTreeActionableProvider>
|
</FileTreeActionableProvider>
|
||||||
|
@ -44,7 +39,6 @@ FileTreeContext.propTypes = {
|
||||||
refProviders: PropTypes.object.isRequired,
|
refProviders: PropTypes.object.isRequired,
|
||||||
setRefProviderEnabled: PropTypes.func.isRequired,
|
setRefProviderEnabled: PropTypes.func.isRequired,
|
||||||
setStartedFreeTrial: PropTypes.func.isRequired,
|
setStartedFreeTrial: PropTypes.func.isRequired,
|
||||||
setShouldShowVisualSelection: PropTypes.func.isRequired,
|
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
|
|
@ -8,16 +8,9 @@ import Icon from '../../../shared/components/icon'
|
||||||
import iconTypeFromName from '../util/icon-type-from-name'
|
import iconTypeFromName from '../util/icon-type-from-name'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
function FileTreeDoc({
|
function FileTreeDoc({ name, id, isFile, isLinkedFile }) {
|
||||||
name,
|
|
||||||
id,
|
|
||||||
isFile,
|
|
||||||
isLinkedFile,
|
|
||||||
shouldShowVisualSelection,
|
|
||||||
}) {
|
|
||||||
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
|
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
|
||||||
id,
|
id,
|
||||||
shouldShowVisualSelection,
|
|
||||||
isFile
|
isFile
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,7 +38,6 @@ FileTreeDoc.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
isFile: PropTypes.bool,
|
isFile: PropTypes.bool,
|
||||||
isLinkedFile: PropTypes.bool,
|
isLinkedFile: PropTypes.bool,
|
||||||
shouldShowVisualSelection: PropTypes.bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileTreeIcon = ({ isLinkedFile, name }) => {
|
export const FileTreeIcon = ({ isLinkedFile, name }) => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ function FileTreeFolderList({
|
||||||
classes = {},
|
classes = {},
|
||||||
dropRef = null,
|
dropRef = null,
|
||||||
children,
|
children,
|
||||||
shouldShowVisualSelection,
|
dataTestId,
|
||||||
}) {
|
}) {
|
||||||
files = files.map(file => ({ ...file, isFile: true }))
|
files = files.map(file => ({ ...file, isFile: true }))
|
||||||
const docsAndFiles = [...docs, ...files]
|
const docsAndFiles = [...docs, ...files]
|
||||||
|
@ -23,6 +23,7 @@ function FileTreeFolderList({
|
||||||
role="tree"
|
role="tree"
|
||||||
ref={dropRef}
|
ref={dropRef}
|
||||||
dnd-container="true"
|
dnd-container="true"
|
||||||
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
{folders.sort(compareFunction).map(folder => {
|
{folders.sort(compareFunction).map(folder => {
|
||||||
return (
|
return (
|
||||||
|
@ -33,7 +34,6 @@ function FileTreeFolderList({
|
||||||
folders={folder.folders}
|
folders={folder.folders}
|
||||||
docs={folder.docs}
|
docs={folder.docs}
|
||||||
files={folder.fileRefs}
|
files={folder.fileRefs}
|
||||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -45,7 +45,6 @@ function FileTreeFolderList({
|
||||||
id={doc._id}
|
id={doc._id}
|
||||||
isFile={doc.isFile}
|
isFile={doc.isFile}
|
||||||
isLinkedFile={doc.linkedFileData && !!doc.linkedFileData.provider}
|
isLinkedFile={doc.linkedFileData && !!doc.linkedFileData.provider}
|
||||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -63,7 +62,7 @@ FileTreeFolderList.propTypes = {
|
||||||
}),
|
}),
|
||||||
dropRef: PropTypes.func,
|
dropRef: PropTypes.func,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
shouldShowVisualSelection: PropTypes.bool,
|
dataTestId: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareFunction(one, two) {
|
function compareFunction(one, two) {
|
||||||
|
|
|
@ -14,20 +14,10 @@ import FileTreeItemInner from './file-tree-item/file-tree-item-inner'
|
||||||
import FileTreeFolderList from './file-tree-folder-list'
|
import FileTreeFolderList from './file-tree-folder-list'
|
||||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||||
|
|
||||||
function FileTreeFolder({
|
function FileTreeFolder({ name, id, folders, docs, files }) {
|
||||||
name,
|
|
||||||
id,
|
|
||||||
folders,
|
|
||||||
docs,
|
|
||||||
files,
|
|
||||||
shouldShowVisualSelection,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
|
const { isSelected, props: selectableEntityProps } = useSelectableEntity(id)
|
||||||
id,
|
|
||||||
shouldShowVisualSelection
|
|
||||||
)
|
|
||||||
|
|
||||||
const { selectedEntityParentIds } = useFileTreeSelectable(id)
|
const { selectedEntityParentIds } = useFileTreeSelectable(id)
|
||||||
|
|
||||||
|
@ -97,7 +87,6 @@ function FileTreeFolder({
|
||||||
docs={docs}
|
docs={docs}
|
||||||
files={files}
|
files={files}
|
||||||
dropRef={dropRefList}
|
dropRef={dropRefList}
|
||||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
@ -110,7 +99,6 @@ FileTreeFolder.propTypes = {
|
||||||
folders: PropTypes.array.isRequired,
|
folders: PropTypes.array.isRequired,
|
||||||
docs: PropTypes.array.isRequired,
|
docs: PropTypes.array.isRequired,
|
||||||
files: PropTypes.array.isRequired,
|
files: PropTypes.array.isRequired,
|
||||||
shouldShowVisualSelection: PropTypes.bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileTreeFolder
|
export default FileTreeFolder
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
|
||||||
|
|
||||||
|
type FileTreeInnerProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileTreeInner({ children }: FileTreeInnerProps) {
|
||||||
|
const { setIsRootFolderSelected } = useFileTreeSelectable()
|
||||||
|
|
||||||
|
const handleFileTreeClick = () => {
|
||||||
|
setIsRootFolderSelected(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// 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"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileTreeInner
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||||
|
@ -18,6 +18,7 @@ import { useDroppable } from '../contexts/file-tree-draggable'
|
||||||
|
|
||||||
import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
|
import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
|
||||||
import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
|
import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
|
||||||
|
import FileTreeInner from './file-tree-inner'
|
||||||
|
|
||||||
const FileTreeRoot = React.memo(function FileTreeRoot({
|
const FileTreeRoot = React.memo(function FileTreeRoot({
|
||||||
refProviders,
|
refProviders,
|
||||||
|
@ -31,12 +32,6 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
||||||
const { _id: projectId } = useProjectContext(projectContextPropTypes)
|
const { _id: projectId } = useProjectContext(projectContextPropTypes)
|
||||||
const { fileTreeData } = useFileTreeData()
|
const { fileTreeData } = useFileTreeData()
|
||||||
const isReady = projectId && fileTreeData
|
const isReady = projectId && fileTreeData
|
||||||
const [shouldShowVisualSelection, setShouldShowVisualSelection] =
|
|
||||||
useState(true)
|
|
||||||
|
|
||||||
const handleFileTreeClick = () => {
|
|
||||||
setShouldShowVisualSelection(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady) onInit()
|
if (isReady) onInit()
|
||||||
|
@ -48,23 +43,15 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
||||||
refProviders={refProviders}
|
refProviders={refProviders}
|
||||||
setRefProviderEnabled={setRefProviderEnabled}
|
setRefProviderEnabled={setRefProviderEnabled}
|
||||||
setStartedFreeTrial={setStartedFreeTrial}
|
setStartedFreeTrial={setStartedFreeTrial}
|
||||||
setShouldShowVisualSelection={setShouldShowVisualSelection}
|
|
||||||
reindexReferences={reindexReferences}
|
reindexReferences={reindexReferences}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
>
|
>
|
||||||
{isConnected ? null : <div className="disconnected-overlay" />}
|
{isConnected ? null : <div className="disconnected-overlay" />}
|
||||||
<FileTreeToolbar />
|
<FileTreeToolbar />
|
||||||
<FileTreeContextMenu />
|
<FileTreeContextMenu />
|
||||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
<FileTreeInner>
|
||||||
<div
|
<FileTreeRootFolder />
|
||||||
className="file-tree-inner"
|
</FileTreeInner>
|
||||||
onClick={handleFileTreeClick}
|
|
||||||
data-testid="file-tree-inner"
|
|
||||||
>
|
|
||||||
<FileTreeRootFolder
|
|
||||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FileTreeModalDelete />
|
<FileTreeModalDelete />
|
||||||
<FileTreeModalCreateFile />
|
<FileTreeModalCreateFile />
|
||||||
<FileTreeModalCreateFolder />
|
<FileTreeModalCreateFolder />
|
||||||
|
@ -73,7 +60,7 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function FileTreeRootFolder({ shouldShowVisualSelection }) {
|
function FileTreeRootFolder() {
|
||||||
useFileTreeSocketListener()
|
useFileTreeSocketListener()
|
||||||
const { fileTreeData } = useFileTreeData()
|
const { fileTreeData } = useFileTreeData()
|
||||||
|
|
||||||
|
@ -89,7 +76,7 @@ function FileTreeRootFolder({ shouldShowVisualSelection }) {
|
||||||
classes={{ root: 'file-tree-list' }}
|
classes={{ root: 'file-tree-list' }}
|
||||||
dropRef={dropRef}
|
dropRef={dropRef}
|
||||||
isOver={isOver}
|
isOver={isOver}
|
||||||
shouldShowVisualSelection={shouldShowVisualSelection}
|
dataTestId="file-tree-list-root"
|
||||||
>
|
>
|
||||||
<li className="bottom-buffer" />
|
<li className="bottom-buffer" />
|
||||||
</FileTreeFolderList>
|
</FileTreeFolderList>
|
||||||
|
@ -97,10 +84,6 @@ function FileTreeRootFolder({ shouldShowVisualSelection }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FileTreeRootFolder.propTypes = {
|
|
||||||
shouldShowVisualSelection: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
FileTreeRoot.propTypes = {
|
FileTreeRoot.propTypes = {
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
onInit: PropTypes.func.isRequired,
|
onInit: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -131,7 +131,7 @@ export function FileTreeActionableProvider({ children }) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeData()
|
const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeData()
|
||||||
const { selectedEntityIds } = useFileTreeSelectable()
|
const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable()
|
||||||
|
|
||||||
const [droppedFiles, setDroppedFiles] = useState(null)
|
const [droppedFiles, setDroppedFiles] = useState(null)
|
||||||
|
|
||||||
|
@ -263,10 +263,13 @@ export function FileTreeActionableProvider({ children }) {
|
||||||
dispatch({ type: ACTION_TYPES.START_CREATE_FOLDER })
|
dispatch({ type: ACTION_TYPES.START_CREATE_FOLDER })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const parentFolderId = useMemo(
|
const parentFolderId = useMemo(() => {
|
||||||
() => getSelectedParentFolderId(fileTreeData, selectedEntityIds),
|
return getSelectedParentFolderId(
|
||||||
[fileTreeData, selectedEntityIds]
|
fileTreeData,
|
||||||
|
selectedEntityIds,
|
||||||
|
isRootFolderSelected
|
||||||
)
|
)
|
||||||
|
}, [fileTreeData, selectedEntityIds, isRootFolderSelected])
|
||||||
|
|
||||||
const finishCreatingEntity = useCallback(
|
const finishCreatingEntity = useCallback(
|
||||||
entity => {
|
entity => {
|
||||||
|
@ -369,8 +372,8 @@ export function FileTreeActionableProvider({ children }) {
|
||||||
}, [fileTreeData, projectId, selectedEntityIds])
|
}, [fileTreeData, projectId, selectedEntityIds])
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
canDelete: selectedEntityIds.size > 0,
|
canDelete: selectedEntityIds.size > 0 && !isRootFolderSelected,
|
||||||
canRename: selectedEntityIds.size === 1,
|
canRename: selectedEntityIds.size === 1 && !isRootFolderSelected,
|
||||||
canCreate: selectedEntityIds.size < 2,
|
canCreate: selectedEntityIds.size < 2,
|
||||||
...state,
|
...state,
|
||||||
parentFolderId,
|
parentFolderId,
|
||||||
|
@ -427,7 +430,15 @@ export function useFileTreeActionable() {
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedParentFolderId(fileTreeData, selectedEntityIds) {
|
function getSelectedParentFolderId(
|
||||||
|
fileTreeData,
|
||||||
|
selectedEntityIds,
|
||||||
|
isRootFolderSelected
|
||||||
|
) {
|
||||||
|
if (isRootFolderSelected) {
|
||||||
|
return fileTreeData._id
|
||||||
|
}
|
||||||
|
|
||||||
// we expect only one entity to be selected in that case, so we pick the first
|
// we expect only one entity to be selected in that case, so we pick the first
|
||||||
const selectedEntityId = Array.from(selectedEntityIds)[0]
|
const selectedEntityId = Array.from(selectedEntityIds)[0]
|
||||||
if (!selectedEntityId) {
|
if (!selectedEntityId) {
|
||||||
|
|
|
@ -75,11 +75,7 @@ function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileTreeSelectableProvider({
|
export function FileTreeSelectableProvider({ onSelect, children }) {
|
||||||
onSelect,
|
|
||||||
setShouldShowVisualSelection,
|
|
||||||
children,
|
|
||||||
}) {
|
|
||||||
const { _id: projectId, rootDocId } = useProjectContext(
|
const { _id: projectId, rootDocId } = useProjectContext(
|
||||||
projectContextPropTypes
|
projectContextPropTypes
|
||||||
)
|
)
|
||||||
|
@ -92,6 +88,8 @@ export function FileTreeSelectableProvider({
|
||||||
|
|
||||||
const { fileTreeData, setSelectedEntities } = useFileTreeData()
|
const { fileTreeData, setSelectedEntities } = useFileTreeData()
|
||||||
|
|
||||||
|
const [isRootFolderSelected, setIsRootFolderSelected] = useState(false)
|
||||||
|
|
||||||
const [selectedEntityIds, dispatch] = useReducer(
|
const [selectedEntityIds, dispatch] = useReducer(
|
||||||
permissionsLevel === 'readOnly'
|
permissionsLevel === 'readOnly'
|
||||||
? fileTreeSelectableReadOnlyReducer
|
? fileTreeSelectableReadOnlyReducer
|
||||||
|
@ -181,7 +179,8 @@ export function FileTreeSelectableProvider({
|
||||||
select,
|
select,
|
||||||
unselect,
|
unselect,
|
||||||
selectOrMultiSelectEntity,
|
selectOrMultiSelectEntity,
|
||||||
setShouldShowVisualSelection,
|
isRootFolderSelected,
|
||||||
|
setIsRootFolderSelected,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -193,7 +192,6 @@ export function FileTreeSelectableProvider({
|
||||||
|
|
||||||
FileTreeSelectableProvider.propTypes = {
|
FileTreeSelectableProvider.propTypes = {
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
setShouldShowVisualSelection: PropTypes.func.isRequired,
|
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node,
|
PropTypes.node,
|
||||||
|
@ -209,16 +207,13 @@ const editorContextPropTypes = {
|
||||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSelectableEntity(
|
export function useSelectableEntity(id, isFile) {
|
||||||
id,
|
|
||||||
shouldShowVisualSelection = true,
|
|
||||||
isFile
|
|
||||||
) {
|
|
||||||
const { view, setView } = useLayoutContext(layoutContextPropTypes)
|
const { view, setView } = useLayoutContext(layoutContextPropTypes)
|
||||||
const {
|
const {
|
||||||
selectedEntityIds,
|
selectedEntityIds,
|
||||||
selectOrMultiSelectEntity,
|
selectOrMultiSelectEntity,
|
||||||
setShouldShowVisualSelection,
|
isRootFolderSelected,
|
||||||
|
setIsRootFolderSelected,
|
||||||
} = useContext(FileTreeSelectableContext)
|
} = useContext(FileTreeSelectableContext)
|
||||||
|
|
||||||
const isSelected = selectedEntityIds.has(id)
|
const isSelected = selectedEntityIds.has(id)
|
||||||
|
@ -226,17 +221,11 @@ export function useSelectableEntity(
|
||||||
const handleEvent = useCallback(
|
const handleEvent = useCallback(
|
||||||
ev => {
|
ev => {
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
setShouldShowVisualSelection(true)
|
setIsRootFolderSelected(false)
|
||||||
selectOrMultiSelectEntity(id, ev.ctrlKey || ev.metaKey)
|
selectOrMultiSelectEntity(id, ev.ctrlKey || ev.metaKey)
|
||||||
setView(isFile ? 'file' : 'editor')
|
setView(isFile ? 'file' : 'editor')
|
||||||
},
|
},
|
||||||
[
|
[id, setIsRootFolderSelected, selectOrMultiSelectEntity, setView, isFile]
|
||||||
id,
|
|
||||||
setShouldShowVisualSelection,
|
|
||||||
selectOrMultiSelectEntity,
|
|
||||||
setView,
|
|
||||||
isFile,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
|
@ -266,7 +255,7 @@ export function useSelectableEntity(
|
||||||
)
|
)
|
||||||
|
|
||||||
const isVisuallySelected =
|
const isVisuallySelected =
|
||||||
shouldShowVisualSelection && isSelected && view !== 'pdf'
|
!isRootFolderSelected && isSelected && view !== 'pdf'
|
||||||
const props = useMemo(
|
const props = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
className: classNames({ selected: isVisuallySelected }),
|
className: classNames({ selected: isVisuallySelected }),
|
||||||
|
|
|
@ -21,21 +21,10 @@ function HistoryFileTreeFolderList({
|
||||||
rootClassName,
|
rootClassName,
|
||||||
children,
|
children,
|
||||||
}: HistoryFileTreeFolderListProps) {
|
}: HistoryFileTreeFolderListProps) {
|
||||||
const {
|
const { selection, setSelection } = useHistoryContext()
|
||||||
selection,
|
|
||||||
setSelection,
|
|
||||||
shouldShowVisualSelection,
|
|
||||||
setShouldShowVisualSelection,
|
|
||||||
} = useHistoryContext()
|
|
||||||
|
|
||||||
const handleTopLevelClick = () => {
|
|
||||||
setShouldShowVisualSelection(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEvent = useCallback(
|
const handleEvent = useCallback(
|
||||||
(file: FileDiff, event: React.UIEvent) => {
|
(file: FileDiff) => {
|
||||||
event.stopPropagation()
|
|
||||||
setShouldShowVisualSelection(true)
|
|
||||||
setSelection(prevSelection => {
|
setSelection(prevSelection => {
|
||||||
if (file.pathname !== prevSelection.selectedFile?.pathname) {
|
if (file.pathname !== prevSelection.selectedFile?.pathname) {
|
||||||
return {
|
return {
|
||||||
|
@ -48,12 +37,12 @@ function HistoryFileTreeFolderList({
|
||||||
return prevSelection
|
return prevSelection
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[setSelection, setShouldShowVisualSelection]
|
[setSelection]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(file: FileDiff, event: React.MouseEvent<HTMLLIElement>) => {
|
(file: FileDiff) => {
|
||||||
handleEvent(file, event)
|
handleEvent(file)
|
||||||
},
|
},
|
||||||
[handleEvent]
|
[handleEvent]
|
||||||
)
|
)
|
||||||
|
@ -61,19 +50,14 @@ function HistoryFileTreeFolderList({
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(file: FileDiff, event: React.KeyboardEvent<HTMLLIElement>) => {
|
(file: FileDiff, event: React.KeyboardEvent<HTMLLIElement>) => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
handleEvent(file, event)
|
handleEvent(file)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleEvent]
|
[handleEvent]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
<ul className={classNames('list-unstyled', rootClassName)} role="tree">
|
||||||
<ul
|
|
||||||
className={classNames('list-unstyled', rootClassName)}
|
|
||||||
role="tree"
|
|
||||||
onClick={handleTopLevelClick}
|
|
||||||
>
|
|
||||||
{folders.map(folder => (
|
{folders.map(folder => (
|
||||||
<HistoryFileTreeFolder
|
<HistoryFileTreeFolder
|
||||||
key={folder.name}
|
key={folder.name}
|
||||||
|
@ -88,7 +72,6 @@ function HistoryFileTreeFolderList({
|
||||||
name={doc.name}
|
name={doc.name}
|
||||||
file={doc}
|
file={doc}
|
||||||
selected={
|
selected={
|
||||||
shouldShowVisualSelection &&
|
|
||||||
!!selection.selectedFile &&
|
!!selection.selectedFile &&
|
||||||
fileFinalPathname(selection.selectedFile) === doc.pathname
|
fileFinalPathname(selection.selectedFile) === doc.pathname
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,8 +88,6 @@ function useHistory() {
|
||||||
const currentUserIsOwner = projectOwnerId === userId
|
const currentUserIsOwner = projectOwnerId === userId
|
||||||
|
|
||||||
const [selection, setSelection] = useState<Selection>(selectionInitialState)
|
const [selection, setSelection] = useState<Selection>(selectionInitialState)
|
||||||
const [shouldShowVisualSelection, setShouldShowVisualSelection] =
|
|
||||||
useState(true)
|
|
||||||
|
|
||||||
const [updatesInfo, setUpdatesInfo] = useState<
|
const [updatesInfo, setUpdatesInfo] = useState<
|
||||||
HistoryContextValue['updatesInfo']
|
HistoryContextValue['updatesInfo']
|
||||||
|
@ -332,8 +330,6 @@ function useHistory() {
|
||||||
setSelection,
|
setSelection,
|
||||||
fetchNextBatchOfUpdates,
|
fetchNextBatchOfUpdates,
|
||||||
resetSelection,
|
resetSelection,
|
||||||
shouldShowVisualSelection,
|
|
||||||
setShouldShowVisualSelection,
|
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
loadingFileDiffs,
|
loadingFileDiffs,
|
||||||
|
@ -350,8 +346,6 @@ function useHistory() {
|
||||||
setSelection,
|
setSelection,
|
||||||
fetchNextBatchOfUpdates,
|
fetchNextBatchOfUpdates,
|
||||||
resetSelection,
|
resetSelection,
|
||||||
shouldShowVisualSelection,
|
|
||||||
setShouldShowVisualSelection,
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,4 @@ export type HistoryContextValue = {
|
||||||
>
|
>
|
||||||
fetchNextBatchOfUpdates: () => (() => void) | void
|
fetchNextBatchOfUpdates: () => (() => void) | void
|
||||||
resetSelection: () => void
|
resetSelection: () => void
|
||||||
shouldShowVisualSelection: boolean
|
|
||||||
setShouldShowVisualSelection: React.Dispatch<
|
|
||||||
React.SetStateAction<HistoryContextValue['shouldShowVisualSelection']>
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react'
|
import { screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
import fetchMock from 'fetch-mock'
|
import fetchMock from 'fetch-mock'
|
||||||
|
import MockedSocket from 'socket.io-mock'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
renderWithEditorContext,
|
renderWithEditorContext,
|
||||||
|
@ -306,13 +307,22 @@ describe('<FileTreeRoot/>', function () {
|
||||||
expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(0)
|
expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deselects files when clicked outside the list but inside wrapping container', function () {
|
describe('when deselecting files', function () {
|
||||||
|
beforeEach(function () {
|
||||||
const rootFolder = [
|
const rootFolder = [
|
||||||
{
|
{
|
||||||
_id: 'root-folder-id',
|
_id: 'root-folder-id',
|
||||||
name: 'rootFolder',
|
name: 'rootFolder',
|
||||||
docs: [{ _id: '456def', name: 'main.tex' }],
|
docs: [{ _id: '123abc', name: 'main.tex' }],
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
_id: '789ghi',
|
||||||
|
name: 'thefolder',
|
||||||
|
docs: [{ _id: '456def', name: 'sub.tex' }],
|
||||||
|
fileRefs: [],
|
||||||
folders: [],
|
folders: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
fileRefs: [],
|
fileRefs: [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -332,11 +342,57 @@ describe('<FileTreeRoot/>', function () {
|
||||||
rootDocId: '456def',
|
rootDocId: '456def',
|
||||||
features: {},
|
features: {},
|
||||||
permissionsLevel: 'owner',
|
permissionsLevel: 'owner',
|
||||||
|
socket: new MockedSocket(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
screen.getByRole('treeitem', { selected: true })
|
// select the sub file
|
||||||
|
const mainDoc = screen.getByRole('treeitem', { name: 'sub.tex' })
|
||||||
|
fireEvent.click(mainDoc)
|
||||||
|
expect(mainDoc.getAttribute('aria-selected')).to.equal('true')
|
||||||
|
|
||||||
|
// click on empty area
|
||||||
fireEvent.click(screen.getByTestId('file-tree-inner'))
|
fireEvent.click(screen.getByTestId('file-tree-inner'))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes the selected indicator', function () {
|
||||||
expect(screen.queryByRole('treeitem', { selected: true })).to.be.null
|
expect(screen.queryByRole('treeitem', { selected: true })).to.be.null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('disables the "rename" and "delete" buttons', function () {
|
||||||
|
expect(screen.queryByRole('button', { name: 'Rename' })).to.be.null
|
||||||
|
expect(screen.queryByRole('button', { name: 'Delete' })).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates new file in the root folder', async function () {
|
||||||
|
fetchMock.post('express:/project/:projectId/doc', () => 200)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /new file/i }))
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /create/i }))
|
||||||
|
|
||||||
|
const socketData = {
|
||||||
|
_id: '12345',
|
||||||
|
name: 'abcdef.tex',
|
||||||
|
docs: [],
|
||||||
|
fileRefs: [],
|
||||||
|
folders: [],
|
||||||
|
}
|
||||||
|
window._ide.socket.socketClient.emit(
|
||||||
|
'reciveNewDoc',
|
||||||
|
'root-folder-id',
|
||||||
|
socketData
|
||||||
|
)
|
||||||
|
|
||||||
|
await fetchMock.flush(true)
|
||||||
|
|
||||||
|
const newItem = screen.getByRole('treeitem', { name: socketData.name })
|
||||||
|
const rootEl = screen.getByTestId('file-tree-list-root')
|
||||||
|
|
||||||
|
expect(newItem.parentNode).to.equal(rootEl)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,7 +24,6 @@ export default (children, options = {}) => {
|
||||||
setStartedFreeTrial: () => {
|
setStartedFreeTrial: () => {
|
||||||
console.log('started free trial')
|
console.log('started free trial')
|
||||||
},
|
},
|
||||||
setShouldShowVisualSelection: () => {},
|
|
||||||
onSelect: () => {},
|
onSelect: () => {},
|
||||||
...contextProps,
|
...contextProps,
|
||||||
}
|
}
|
||||||
|
@ -33,7 +32,6 @@ export default (children, options = {}) => {
|
||||||
reindexReferences,
|
reindexReferences,
|
||||||
setRefProviderEnabled,
|
setRefProviderEnabled,
|
||||||
setStartedFreeTrial,
|
setStartedFreeTrial,
|
||||||
setShouldShowVisualSelection,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
...editorContextProps
|
...editorContextProps
|
||||||
} = contextProps
|
} = contextProps
|
||||||
|
@ -43,7 +41,6 @@ export default (children, options = {}) => {
|
||||||
reindexReferences={reindexReferences}
|
reindexReferences={reindexReferences}
|
||||||
setRefProviderEnabled={setRefProviderEnabled}
|
setRefProviderEnabled={setRefProviderEnabled}
|
||||||
setStartedFreeTrial={setStartedFreeTrial}
|
setStartedFreeTrial={setStartedFreeTrial}
|
||||||
setShouldShowVisualSelection={setShouldShowVisualSelection}
|
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
Loading…
Reference in a new issue