mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-14 16:43:30 +00:00
Port editor react file tree to history file tree (#12453)
This new history file tree is mostly copied from the editor file tree, with some of the features stripped away: 1. Remove multiple selections 2. Remove drag and drop ability 3. Remove the ability to rename files & folders 4. No more right-click hijacking (context menu) 5. No more triple dots menu on a file tree item shown 6. No file references, since history doesn't have the data to differentiate between real files and linked file 7. etc (some other small changes that are not too important to be listed) Other notable changes: 1. Simplify the selectable provider (the only context provider being copied from react file tree) 2. Convert to typescript GitOrigin-RevId: 1017e545b2bd99775e01307a9b7eac2daf454014
This commit is contained in:
parent
6c234e81c0
commit
9a55bbf325
9 changed files with 277 additions and 54 deletions
|
@ -0,0 +1,36 @@
|
|||
import HistoryFileTreeItem from './history-file-tree-item'
|
||||
import iconTypeFromName from '../../../file-tree/util/icon-type-from-name'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import { useSelectableEntity } from '../../context/history-file-tree-selectable'
|
||||
|
||||
type HistoryFileTreeDocProps = {
|
||||
name: string
|
||||
id: string
|
||||
}
|
||||
|
||||
export default function HistoryFileTreeDoc({
|
||||
name,
|
||||
id,
|
||||
}: HistoryFileTreeDocProps) {
|
||||
const { props: selectableEntityProps } = useSelectableEntity(id)
|
||||
|
||||
return (
|
||||
<li
|
||||
role="treeitem"
|
||||
{...selectableEntityProps}
|
||||
aria-label={name}
|
||||
tabIndex={0}
|
||||
>
|
||||
<HistoryFileTreeItem
|
||||
name={name}
|
||||
icons={
|
||||
<Icon
|
||||
type={iconTypeFromName(name)}
|
||||
fw
|
||||
className="spaced file-tree-icon"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import classNames from 'classnames'
|
||||
|
||||
import HistoryFileTreeDoc from './history-file-tree-doc'
|
||||
import HistoryFileTreeFolder from './history-file-tree-folder'
|
||||
import { fileCollator } from '../../../file-tree/util/file-collator'
|
||||
import type { Doc } from '../../../../../../types/doc'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { HistoryFileTree } from '../../utils/file-tree'
|
||||
|
||||
type HistoryFileTreeFolderListProps = {
|
||||
folders: HistoryFileTree[]
|
||||
docs: Doc[]
|
||||
rootClassName?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export default function HistoryFileTreeFolderList({
|
||||
folders,
|
||||
docs,
|
||||
rootClassName,
|
||||
children,
|
||||
}: HistoryFileTreeFolderListProps) {
|
||||
return (
|
||||
<ul className={classNames('list-unstyled', rootClassName)} role="tree">
|
||||
{folders.sort(compareFunction).map(folder => {
|
||||
return (
|
||||
<HistoryFileTreeFolder
|
||||
key={folder._id}
|
||||
name={folder.name}
|
||||
folders={folder.folders}
|
||||
docs={folder.docs ?? []}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{docs.sort(compareFunction).map(doc => {
|
||||
return <HistoryFileTreeDoc key={doc._id} name={doc.name} id={doc._id} />
|
||||
})}
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
function compareFunction(
|
||||
one: HistoryFileTree | Doc,
|
||||
two: HistoryFileTree | Doc
|
||||
) {
|
||||
return fileCollator.compare(one.name, two.name)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import HistoryFileTreeItem from './history-file-tree-item'
|
||||
import HistoryFileTreeFolderList from './history-file-tree-folder-list'
|
||||
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import type { Doc } from '../../../../../../types/doc'
|
||||
import type { HistoryFileTree } from '../../utils/file-tree'
|
||||
|
||||
type HistoryFileTreeFolderProps = {
|
||||
name: string
|
||||
folders: HistoryFileTree[]
|
||||
docs: Doc[]
|
||||
}
|
||||
|
||||
export default function HistoryFileTreeFolder({
|
||||
name,
|
||||
folders,
|
||||
docs,
|
||||
}: HistoryFileTreeFolderProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
|
||||
const icons = (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
aria-label={expanded ? t('collapse') : t('expand')}
|
||||
>
|
||||
<Icon
|
||||
type={expanded ? 'angle-down' : 'angle-right'}
|
||||
fw
|
||||
className="file-tree-expand-icon"
|
||||
/>
|
||||
</button>
|
||||
<Icon
|
||||
type={expanded ? 'folder-open' : 'folder'}
|
||||
fw
|
||||
className="file-tree-folder-icon"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<li
|
||||
role="treeitem"
|
||||
aria-expanded={expanded}
|
||||
aria-label={name}
|
||||
tabIndex={0}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
onKeyDown={() => setExpanded(!expanded)}
|
||||
>
|
||||
<HistoryFileTreeItem name={name} icons={icons} />
|
||||
</li>
|
||||
{expanded ? (
|
||||
<HistoryFileTreeFolderList folders={folders} docs={docs} />
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import type { ReactNode } from 'react'
|
||||
|
||||
type FileTreeItemProps = {
|
||||
name: string
|
||||
icons: ReactNode
|
||||
}
|
||||
|
||||
export default function FileTreeItem({ name, icons }: FileTreeItemProps) {
|
||||
return (
|
||||
<div className="entity" role="presentation">
|
||||
<div className="entity-name entity-name-react" role="presentation">
|
||||
{icons}
|
||||
<button className="item-name-button">
|
||||
<span>{name}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,28 +1,24 @@
|
|||
import _ from 'lodash'
|
||||
import FileTreeContext from '../../file-tree/components/file-tree-context'
|
||||
import FileTreeFolderList from '../../file-tree/components/file-tree-folder-list'
|
||||
import { useCallback } from 'react'
|
||||
import { useHistoryContext } from '../context/history-context'
|
||||
import { HistoryFileTreeSelectableProvider } from '../context/history-file-tree-selectable'
|
||||
import {
|
||||
fileTreeDiffToFileTreeData,
|
||||
reducePathsToTree,
|
||||
} from '../utils/file-tree'
|
||||
import HistoryFileTreeFolderList from './file-tree/history-file-tree-folder-list'
|
||||
|
||||
type HistoryFileTreeProps = {
|
||||
setRefProviderEnabled: any
|
||||
setStartedFreeTrial: any
|
||||
reindexReferences: any
|
||||
onSelect: any
|
||||
refProviders: any
|
||||
}
|
||||
export default function HistoryFileTree() {
|
||||
const { fileSelection, setFileSelection } = useHistoryContext()
|
||||
|
||||
export default function HistoryFileTree({
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
reindexReferences,
|
||||
onSelect,
|
||||
refProviders,
|
||||
}: HistoryFileTreeProps) {
|
||||
const { fileSelection } = useHistoryContext()
|
||||
const handleSelectFile = useCallback(
|
||||
(pathname: string) => {
|
||||
if (fileSelection) {
|
||||
setFileSelection({ files: fileSelection.files, pathname })
|
||||
}
|
||||
},
|
||||
[fileSelection, setFileSelection]
|
||||
)
|
||||
|
||||
if (!fileSelection) {
|
||||
return null
|
||||
|
@ -33,21 +29,14 @@ export default function HistoryFileTree({
|
|||
const mappedFileTree = fileTreeDiffToFileTreeData(fileTree)
|
||||
|
||||
return (
|
||||
<FileTreeContext
|
||||
refProviders={refProviders}
|
||||
setRefProviderEnabled={setRefProviderEnabled}
|
||||
setStartedFreeTrial={setStartedFreeTrial}
|
||||
reindexReferences={reindexReferences}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<FileTreeFolderList
|
||||
<HistoryFileTreeSelectableProvider onSelectFile={handleSelectFile}>
|
||||
<HistoryFileTreeFolderList
|
||||
folders={mappedFileTree.folders}
|
||||
docs={mappedFileTree.docs ?? []}
|
||||
files={[]}
|
||||
classes={{ root: 'file-tree-list' }}
|
||||
rootClassName="file-tree-list"
|
||||
>
|
||||
<li className="bottom-buffer" />
|
||||
</FileTreeFolderList>
|
||||
</FileTreeContext>
|
||||
</HistoryFileTreeFolderList>
|
||||
</HistoryFileTreeSelectableProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,16 +19,7 @@ function Main() {
|
|||
return (
|
||||
<>
|
||||
{fileTreeContainer
|
||||
? createPortal(
|
||||
<HistoryFileTree
|
||||
onSelect={() => {}}
|
||||
refProviders={{}}
|
||||
reindexReferences={() => {}}
|
||||
setRefProviderEnabled={() => {}}
|
||||
setStartedFreeTrial={() => {}}
|
||||
/>,
|
||||
fileTreeContainer
|
||||
)
|
||||
? createPortal(<HistoryFileTree />, fileTreeContainer)
|
||||
: null}
|
||||
<div className="history-react">
|
||||
<DiffView />
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import classNames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
import usePreviousValue from '../../../shared/hooks/use-previous-value'
|
||||
import { Nullable } from '../../../../../types/utils'
|
||||
|
||||
type Context = {
|
||||
select: (id: string) => void
|
||||
selectedFile: Nullable<string>
|
||||
}
|
||||
|
||||
const FileTreeSelectableContext = createContext<Context>({
|
||||
select: () => {},
|
||||
selectedFile: null,
|
||||
})
|
||||
|
||||
type HistoryFileTreeSelectableProviderProps = {
|
||||
onSelectFile: (id: string) => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function HistoryFileTreeSelectableProvider({
|
||||
onSelectFile,
|
||||
children,
|
||||
}: HistoryFileTreeSelectableProviderProps) {
|
||||
const [selectedFile, setSelectedFile] =
|
||||
useState<Context['selectedFile']>(null)
|
||||
|
||||
const previousSelectedFile = usePreviousValue(selectedFile)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFile) {
|
||||
return
|
||||
}
|
||||
|
||||
if (_.isEqual(selectedFile, previousSelectedFile)) {
|
||||
return
|
||||
}
|
||||
|
||||
onSelectFile(selectedFile)
|
||||
}, [selectedFile, previousSelectedFile, onSelectFile])
|
||||
|
||||
const select = useCallback(id => {
|
||||
setSelectedFile(id)
|
||||
}, [])
|
||||
|
||||
const value = {
|
||||
selectedFile,
|
||||
select,
|
||||
}
|
||||
|
||||
return (
|
||||
<FileTreeSelectableContext.Provider value={value}>
|
||||
{children}
|
||||
</FileTreeSelectableContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useSelectableEntity(id: string) {
|
||||
const { selectedFile, select } = useContext(FileTreeSelectableContext)
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
select(id)
|
||||
}, [id, select])
|
||||
|
||||
const isSelected = selectedFile === id
|
||||
|
||||
const props = useMemo(
|
||||
() => ({
|
||||
className: classNames({ selected: isSelected }),
|
||||
'aria-selected': isSelected,
|
||||
onClick: handleClick,
|
||||
}),
|
||||
[handleClick, isSelected]
|
||||
)
|
||||
|
||||
return { isSelected, props }
|
||||
}
|
|
@ -5,11 +5,5 @@ import HistoryFileTree from '../components/history-file-tree'
|
|||
|
||||
App.component(
|
||||
'historyFileTreeReact',
|
||||
react2angular(rootContext.use(HistoryFileTree), [
|
||||
'refProviders',
|
||||
'setRefProviderEnabled',
|
||||
'setStartedFreeTrial',
|
||||
'reindexReferences',
|
||||
'onSelect',
|
||||
])
|
||||
react2angular(rootContext.use(HistoryFileTree))
|
||||
)
|
||||
|
|
|
@ -48,14 +48,11 @@ export function reducePathsToTree(
|
|||
return currentFileTree
|
||||
}
|
||||
|
||||
type HistoryFileTree = {
|
||||
export type HistoryFileTree = {
|
||||
docs?: Doc[]
|
||||
folders: HistoryFileTree[]
|
||||
name: string
|
||||
// `id` and `fileRefs` are both required from react file tree.
|
||||
// TODO: update react file tree to make the data optional so we can delete these keys
|
||||
id: ''
|
||||
fileRefs: []
|
||||
_id: string
|
||||
}
|
||||
|
||||
export function fileTreeDiffToFileTreeData(
|
||||
|
@ -68,7 +65,7 @@ export function fileTreeDiffToFileTreeData(
|
|||
for (const file of fileTreeDiff) {
|
||||
if (file.type === 'file') {
|
||||
docs.push({
|
||||
_id: '',
|
||||
_id: file.pathname as string,
|
||||
name: file.name ?? '',
|
||||
})
|
||||
} else if (file.type === 'folder') {
|
||||
|
@ -83,8 +80,7 @@ export function fileTreeDiffToFileTreeData(
|
|||
docs,
|
||||
folders,
|
||||
name: currentFolderName,
|
||||
id: '',
|
||||
fileRefs: [],
|
||||
_id: currentFolderName,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue