mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-13 21:53:43 +01:00
Merge pull request #22930 from overleaf/dp-filetree
Convert filetree components to typescript GitOrigin-RevId: f06abb9705013d3da1a87c8f4bef4a82c4fab5d2
This commit is contained in:
+19
-17
@@ -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
|
||||
+29
-24
@@ -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)
|
||||
}
|
||||
|
||||
+18
-12
@@ -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
|
||||
+2
-2
@@ -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')}
|
||||
+4
-12
@@ -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
|
||||
+32
-29
@@ -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"
|
||||
/>
|
||||
</>
|
||||
|
||||
+9
-2
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user