Merge pull request #22930 from overleaf/dp-filetree

Convert filetree components to typescript

GitOrigin-RevId: f06abb9705013d3da1a87c8f4bef4a82c4fab5d2
This commit is contained in:
David
2025-01-22 09:05:01 +00:00
committed by Copybot
parent 2365d8c199
commit 037cc3b7a4
9 changed files with 114 additions and 99 deletions
@@ -1,5 +1,3 @@
import PropTypes from 'prop-types'
import { useSelectableEntity } from '../contexts/file-tree-selectable'
import FileTreeItemInner from './file-tree-item/file-tree-item-inner'
@@ -10,7 +8,17 @@ import classnames from 'classnames'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
function FileTreeDoc({ name, id, isFile, isLinkedFile }) {
function FileTreeDoc({
name,
id,
isFile,
isLinkedFile,
}: {
name: string
id: string
isFile?: boolean
isLinkedFile?: boolean
}) {
const type = isFile ? 'file' : 'doc'
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
@@ -25,7 +33,7 @@ function FileTreeDoc({ name, id, isFile, isLinkedFile }) {
// aria-selected is provided in selectableEntityProps
{...selectableEntityProps}
aria-label={name}
tabIndex="0"
tabIndex={0}
>
<FileTreeItemInner
id={id}
@@ -38,14 +46,13 @@ function FileTreeDoc({ name, id, isFile, isLinkedFile }) {
)
}
FileTreeDoc.propTypes = {
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
isFile: PropTypes.bool,
isLinkedFile: PropTypes.bool,
}
export const FileTreeIcon = ({ isLinkedFile, name }) => {
export const FileTreeIcon = ({
isLinkedFile,
name,
}: {
name: string
isLinkedFile?: boolean
}) => {
const { t } = useTranslation()
const className = classnames('file-tree-icon', {
@@ -87,9 +94,4 @@ export const FileTreeIcon = ({ isLinkedFile, name }) => {
)
}
FileTreeIcon.propTypes = {
name: PropTypes.string.isRequired,
isLinkedFile: PropTypes.bool,
}
export default FileTreeDoc
@@ -1,9 +1,14 @@
import PropTypes from 'prop-types'
import classNames from 'classnames'
import FileTreeDoc from './file-tree-doc'
import FileTreeFolder from './file-tree-folder'
import { fileCollator } from '../util/file-collator'
import { Folder } from '../../../../../types/folder'
import { Doc } from '../../../../../types/doc'
import { FileRef } from '../../../../../types/file-ref'
import { ConnectDropTarget } from 'react-dnd'
type ExtendedFileRef = FileRef & { isFile: true }
function FileTreeFolderList({
folders,
@@ -13,9 +18,17 @@ function FileTreeFolderList({
dropRef = null,
children,
dataTestId,
}: {
folders: Folder[]
docs: Doc[]
files: FileRef[]
classes?: { root?: string }
dropRef?: ConnectDropTarget | null
children?: React.ReactNode
dataTestId?: string
}) {
files = files.map(file => ({ ...file, isFile: true }))
const docsAndFiles = [...docs, ...files]
const docsAndFiles: (Doc | ExtendedFileRef)[] = [...docs, ...files]
return (
<ul
@@ -37,34 +50,26 @@ function FileTreeFolderList({
)
})}
{docsAndFiles.sort(compareFunction).map(doc => {
return (
<FileTreeDoc
key={doc._id}
name={doc.name}
id={doc._id}
isFile={doc.isFile}
isLinkedFile={doc.linkedFileData && !!doc.linkedFileData.provider}
/>
)
if ('isFile' in doc) {
return (
<FileTreeDoc
key={doc._id}
name={doc.name}
id={doc._id}
isFile={doc.isFile}
isLinkedFile={doc.linkedFileData && !!doc.linkedFileData.provider}
/>
)
}
return <FileTreeDoc key={doc._id} name={doc.name} id={doc._id} />
})}
{children}
</ul>
)
}
FileTreeFolderList.propTypes = {
folders: PropTypes.array.isRequired,
docs: PropTypes.array.isRequired,
files: PropTypes.array.isRequired,
classes: PropTypes.exact({
root: PropTypes.string,
}),
dropRef: PropTypes.func,
children: PropTypes.node,
dataTestId: PropTypes.string,
}
function compareFunction(one, two) {
function compareFunction(one: { name: string }, two: { name: string }) {
return fileCollator.compare(one.name, two.name)
}
@@ -1,5 +1,4 @@
import { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
@@ -15,8 +14,23 @@ import FileTreeFolderList from './file-tree-folder-list'
import usePersistedState from '../../../shared/hooks/use-persisted-state'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
import { Folder } from '../../../../../types/folder'
import { Doc } from '../../../../../types/doc'
import { FileRef } from '../../../../../types/file-ref'
function FileTreeFolder({ name, id, folders, docs, files }) {
function FileTreeFolder({
name,
id,
folders,
docs,
files,
}: {
name: string
id: string
folders: Folder[]
docs: Doc[]
files: FileRef[]
}) {
const { t } = useTranslation()
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
@@ -24,7 +38,7 @@ function FileTreeFolder({ name, id, folders, docs, files }) {
'folder'
)
const { selectedEntityParentIds } = useFileTreeSelectable(id)
const { selectedEntityParentIds } = useFileTreeSelectable()
const [expanded, setExpanded] = usePersistedState(
`folder.${id}.expanded`,
@@ -94,7 +108,7 @@ function FileTreeFolder({ name, id, folders, docs, files }) {
{...selectableEntityProps}
aria-expanded={expanded}
aria-label={name}
tabIndex="0"
tabIndex={0}
ref={dropRefRoot}
className={classNames(selectableEntityProps.className, {
'dnd-droppable-hover': isOverRoot || isOverList,
@@ -120,12 +134,4 @@ function FileTreeFolder({ name, id, folders, docs, files }) {
)
}
FileTreeFolder.propTypes = {
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
folders: PropTypes.array.isRequired,
docs: PropTypes.array.isRequired,
files: PropTypes.array.isRequired,
}
export default FileTreeFolder
@@ -89,7 +89,7 @@ function FileTreeItemMenuItems() {
<DropdownItem
href={downloadPath}
onClick={downloadWithAnalytics}
download={selectedFileName}
download={selectedFileName ?? undefined}
>
{t('download')}
</DropdownItem>
@@ -102,7 +102,7 @@ function FileTreeItemMenuItems() {
) : null}
{canCreate ? (
<>
<DropdownDivider role="none" />
<DropdownDivider />
<li role="none">
<DropdownItem onClick={createWithAnalytics}>
{t('new_file')}
@@ -1,23 +1,20 @@
import { useRef } from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import Icon from '../../../../shared/components/icon'
import { useFileTreeMainContext } from '../../contexts/file-tree-main'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
function FileTreeItemMenu({ id, name }) {
function FileTreeItemMenu({ id, name }: { id: string; name: string }) {
const { t } = useTranslation()
const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
const menuButtonRef = useRef()
const menuButtonRef = useRef<HTMLButtonElement>(null)
const isMenuOpen = Boolean(contextMenuCoords)
function handleClick(event) {
function handleClick(event: React.MouseEvent) {
event.stopPropagation()
if (!contextMenuCoords) {
if (!contextMenuCoords && menuButtonRef.current) {
const target = menuButtonRef.current.getBoundingClientRect()
setContextMenuCoords({
top: target.top + target.height / 2,
@@ -48,9 +45,4 @@ function FileTreeItemMenu({ id, name }) {
)
}
FileTreeItemMenu.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}
export default FileTreeItemMenu
@@ -1,11 +1,16 @@
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { useState, useEffect, RefObject } from 'react'
import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
function FileTreeItemName({ name, isSelected, setIsDraggable }) {
function FileTreeItemName({
name,
isSelected,
setIsDraggable,
}: {
name: string
isSelected: boolean
setIsDraggable: (isDraggable: boolean) => void
}) {
const { isRenaming, startRenaming, finishRenaming, error, cancel } =
useFileTreeActionable()
@@ -33,13 +38,15 @@ function FileTreeItemName({ name, isSelected, setIsDraggable }) {
)
}
FileTreeItemName.propTypes = {
name: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired,
setIsDraggable: PropTypes.func.isRequired,
}
function DisplayName({ name, isSelected, startRenaming }) {
function DisplayName({
name,
isSelected,
startRenaming,
}: {
name: string
isSelected: boolean
startRenaming: () => void
}) {
const [clicksInSelectedCount, setClicksInSelectedCount] = useState(0)
function onClick() {
@@ -67,13 +74,15 @@ function DisplayName({ name, isSelected, startRenaming }) {
)
}
DisplayName.propTypes = {
name: PropTypes.string.isRequired,
startRenaming: PropTypes.func.isRequired,
isSelected: PropTypes.bool.isRequired,
}
function InputName({ initialValue, finishRenaming, cancel }) {
function InputName({
initialValue,
finishRenaming,
cancel,
}: {
initialValue: string
finishRenaming: (value: string) => void
cancel: () => void
}) {
const [value, setValue] = useState(initialValue)
// The react-bootstrap Dropdown re-focuses on the Dropdown.Toggle
@@ -84,16 +93,16 @@ function InputName({ initialValue, finishRenaming, cancel }) {
// shown
const { autoFocusedRef } = useRefWithAutoFocus()
function handleFocus(ev) {
function handleFocus(ev: React.FocusEvent<HTMLInputElement>) {
const lastDotIndex = ev.target.value.lastIndexOf('.')
ev.target.setSelectionRange(0, lastDotIndex)
}
function handleChange(ev) {
function handleChange(ev: React.ChangeEvent<HTMLInputElement>) {
setValue(ev.target.value)
}
function handleKeyDown(ev) {
function handleKeyDown(ev: React.KeyboardEvent<HTMLInputElement>) {
if (ev.key === 'Enter') {
finishRenaming(value)
}
@@ -115,16 +124,10 @@ function InputName({ initialValue, finishRenaming, cancel }) {
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
ref={autoFocusedRef}
ref={autoFocusedRef as RefObject<HTMLInputElement>}
/>
</span>
)
}
InputName.propTypes = {
initialValue: PropTypes.string.isRequired,
finishRenaming: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
}
export default FileTreeItemName
@@ -136,7 +136,7 @@ function FileTreeRootFolder({ onDelete }: { onDelete: () => void }) {
'file-tree-dragging': dragLayer.isDragging,
}),
}}
dropRef={dropRef as any}
dropRef={dropRef}
dataTestId="file-tree-list-root"
/>
</>
@@ -31,8 +31,15 @@ describe('<FileTreeFolderList/>', function () {
fileRefs: [],
},
]}
docs={[{ _id: '789ghi', name: 'doc.tex', linkedFileData: {} }]}
files={[{ _id: '987jkl', name: 'file.bib', linkedFileData: {} }]}
docs={[{ _id: '789ghi', name: 'doc.tex' }]}
files={[
{
_id: '987jkl',
name: 'file.bib',
hash: 'some hash',
linkedFileData: {},
},
]}
/>
</FileTreeProvider>
</EditorProviders>