From 037cc3b7a406119f1b3bee578f110d9d9bd32230 Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:10:46 +0000 Subject: [PATCH] Merge pull request #22930 from overleaf/dp-filetree Convert filetree components to typescript GitOrigin-RevId: f06abb9705013d3da1a87c8f4bef4a82c4fab5d2 --- .../{file-tree-doc.jsx => file-tree-doc.tsx} | 36 +++++------ ...ile-tree-error.jsx => file-tree-error.tsx} | 0 ...der-list.jsx => file-tree-folder-list.tsx} | 53 ++++++++-------- ...e-tree-folder.jsx => file-tree-folder.tsx} | 30 +++++---- ...tems.jsx => file-tree-item-menu-items.tsx} | 4 +- ...-item-menu.jsx => file-tree-item-menu.tsx} | 16 ++--- ...-item-name.jsx => file-tree-item-name.tsx} | 61 ++++++++++--------- .../file-tree/components/file-tree-root.tsx | 2 +- .../components/file-tree-folder-list.spec.tsx | 11 +++- 9 files changed, 114 insertions(+), 99 deletions(-) rename services/web/frontend/js/features/file-tree/components/{file-tree-doc.jsx => file-tree-doc.tsx} (83%) rename services/web/frontend/js/features/file-tree/components/{file-tree-error.jsx => file-tree-error.tsx} (100%) rename services/web/frontend/js/features/file-tree/components/{file-tree-folder-list.jsx => file-tree-folder-list.tsx} (51%) rename services/web/frontend/js/features/file-tree/components/{file-tree-folder.jsx => file-tree-folder.tsx} (88%) rename services/web/frontend/js/features/file-tree/components/file-tree-item/{file-tree-item-menu-items.jsx => file-tree-item-menu-items.tsx} (97%) rename services/web/frontend/js/features/file-tree/components/file-tree-item/{file-tree-item-menu.jsx => file-tree-item-menu.tsx} (83%) rename services/web/frontend/js/features/file-tree/components/file-tree-item/{file-tree-item-name.jsx => file-tree-item-name.tsx} (75%) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-doc.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-doc.tsx similarity index 83% rename from services/web/frontend/js/features/file-tree/components/file-tree-doc.jsx rename to services/web/frontend/js/features/file-tree/components/file-tree-doc.tsx index 3c0aa93a1c..8d9d6163fe 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-doc.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-doc.tsx @@ -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} > { +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 diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-error.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-error.tsx similarity index 100% rename from services/web/frontend/js/features/file-tree/components/file-tree-error.jsx rename to services/web/frontend/js/features/file-tree/components/file-tree-error.tsx diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.tsx similarity index 51% rename from services/web/frontend/js/features/file-tree/components/file-tree-folder-list.jsx rename to services/web/frontend/js/features/file-tree/components/file-tree-folder-list.tsx index 506084df27..7641ad906c 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.tsx @@ -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 ( ) } -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) } diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-folder.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-folder.tsx similarity index 88% rename from services/web/frontend/js/features/file-tree/components/file-tree-folder.jsx rename to services/web/frontend/js/features/file-tree/components/file-tree-folder.tsx index 01234dd3bd..abcb6474cd 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-folder.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-folder.tsx @@ -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 diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx similarity index 97% rename from services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx rename to services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx index 93da129758..1ef3b31a2a 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx @@ -89,7 +89,7 @@ function FileTreeItemMenuItems() { {t('download')} @@ -102,7 +102,7 @@ function FileTreeItemMenuItems() { ) : null} {canCreate ? ( <> - +
  • {t('new_file')} diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.tsx similarity index 83% rename from services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.jsx rename to services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.tsx index 48c5505395..f8cdc4ef94 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.tsx @@ -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(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 diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.tsx similarity index 75% rename from services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.jsx rename to services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.tsx index 624b64bd57..038b2f2572 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.tsx @@ -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) { const lastDotIndex = ev.target.value.lastIndexOf('.') ev.target.setSelectionRange(0, lastDotIndex) } - function handleChange(ev) { + function handleChange(ev: React.ChangeEvent) { setValue(ev.target.value) } - function handleKeyDown(ev) { + function handleKeyDown(ev: React.KeyboardEvent) { 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} /> ) } -InputName.propTypes = { - initialValue: PropTypes.string.isRequired, - finishRenaming: PropTypes.func.isRequired, - cancel: PropTypes.func.isRequired, -} - export default FileTreeItemName diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx index 66f547aa5c..9377faad7f 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx @@ -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" /> diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.spec.tsx index 2cf8e31fc8..dfac469266 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.spec.tsx +++ b/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.spec.tsx @@ -31,8 +31,15 @@ describe('', 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: {}, + }, + ]} />