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:
M Fahru 2023-04-03 09:40:32 -07:00 committed by Copybot
parent 6c234e81c0
commit 9a55bbf325
9 changed files with 277 additions and 54 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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