Ver código fonte

Merge pull request #22930 from overleaf/dp-filetree

Convert filetree components to typescript

GitOrigin-RevId: f06abb9705013d3da1a87c8f4bef4a82c4fab5d2
David 1 ano atrás
pai
commit
037cc3b7a4

+ 19 - 17
services/web/frontend/js/features/file-tree/components/file-tree-doc.jsx → 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 { useSelectableEntity } from '../contexts/file-tree-selectable'
 
 
 import FileTreeItemInner from './file-tree-item/file-tree-item-inner'
 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 BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import MaterialIcon from '@/shared/components/material-icon'
 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 type = isFile ? 'file' : 'doc'
 
 
   const { isSelected, props: selectableEntityProps } = useSelectableEntity(
   const { isSelected, props: selectableEntityProps } = useSelectableEntity(
@@ -25,7 +33,7 @@ function FileTreeDoc({ name, id, isFile, isLinkedFile }) {
       // aria-selected is provided in selectableEntityProps
       // aria-selected is provided in selectableEntityProps
       {...selectableEntityProps}
       {...selectableEntityProps}
       aria-label={name}
       aria-label={name}
-      tabIndex="0"
+      tabIndex={0}
     >
     >
       <FileTreeItemInner
       <FileTreeItemInner
         id={id}
         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 { t } = useTranslation()
 
 
   const className = classnames('file-tree-icon', {
   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
 export default FileTreeDoc

+ 0 - 0
services/web/frontend/js/features/file-tree/components/file-tree-error.jsx → services/web/frontend/js/features/file-tree/components/file-tree-error.tsx


+ 29 - 24
services/web/frontend/js/features/file-tree/components/file-tree-folder-list.jsx → 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 classNames from 'classnames'
 
 
 import FileTreeDoc from './file-tree-doc'
 import FileTreeDoc from './file-tree-doc'
 import FileTreeFolder from './file-tree-folder'
 import FileTreeFolder from './file-tree-folder'
 import { fileCollator } from '../util/file-collator'
 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({
 function FileTreeFolderList({
   folders,
   folders,
@@ -13,9 +18,17 @@ function FileTreeFolderList({
   dropRef = null,
   dropRef = null,
   children,
   children,
   dataTestId,
   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 }))
   files = files.map(file => ({ ...file, isFile: true }))
-  const docsAndFiles = [...docs, ...files]
+  const docsAndFiles: (Doc | ExtendedFileRef)[] = [...docs, ...files]
 
 
   return (
   return (
     <ul
     <ul
@@ -37,34 +50,26 @@ function FileTreeFolderList({
         )
         )
       })}
       })}
       {docsAndFiles.sort(compareFunction).map(doc => {
       {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}
       {children}
     </ul>
     </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)
   return fileCollator.compare(one.name, two.name)
 }
 }
 
 

+ 18 - 12
services/web/frontend/js/features/file-tree/components/file-tree-folder.jsx → services/web/frontend/js/features/file-tree/components/file-tree-folder.tsx

@@ -1,5 +1,4 @@
 import { useEffect } from 'react'
 import { useEffect } from 'react'
-import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import classNames from 'classnames'
 import classNames from 'classnames'
 
 
@@ -15,8 +14,23 @@ import FileTreeFolderList from './file-tree-folder-list'
 import usePersistedState from '../../../shared/hooks/use-persisted-state'
 import usePersistedState from '../../../shared/hooks/use-persisted-state'
 import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import MaterialIcon from '@/shared/components/material-icon'
 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 { t } = useTranslation()
 
 
   const { isSelected, props: selectableEntityProps } = useSelectableEntity(
   const { isSelected, props: selectableEntityProps } = useSelectableEntity(
@@ -24,7 +38,7 @@ function FileTreeFolder({ name, id, folders, docs, files }) {
     'folder'
     'folder'
   )
   )
 
 
-  const { selectedEntityParentIds } = useFileTreeSelectable(id)
+  const { selectedEntityParentIds } = useFileTreeSelectable()
 
 
   const [expanded, setExpanded] = usePersistedState(
   const [expanded, setExpanded] = usePersistedState(
     `folder.${id}.expanded`,
     `folder.${id}.expanded`,
@@ -94,7 +108,7 @@ function FileTreeFolder({ name, id, folders, docs, files }) {
         {...selectableEntityProps}
         {...selectableEntityProps}
         aria-expanded={expanded}
         aria-expanded={expanded}
         aria-label={name}
         aria-label={name}
-        tabIndex="0"
+        tabIndex={0}
         ref={dropRefRoot}
         ref={dropRefRoot}
         className={classNames(selectableEntityProps.className, {
         className={classNames(selectableEntityProps.className, {
           'dnd-droppable-hover': isOverRoot || isOverList,
           '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
 export default FileTreeFolder

+ 2 - 2
services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx → services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx

@@ -89,7 +89,7 @@ function FileTreeItemMenuItems() {
               <DropdownItem
               <DropdownItem
                 href={downloadPath}
                 href={downloadPath}
                 onClick={downloadWithAnalytics}
                 onClick={downloadWithAnalytics}
-                download={selectedFileName}
+                download={selectedFileName ?? undefined}
               >
               >
                 {t('download')}
                 {t('download')}
               </DropdownItem>
               </DropdownItem>
@@ -102,7 +102,7 @@ function FileTreeItemMenuItems() {
           ) : null}
           ) : null}
           {canCreate ? (
           {canCreate ? (
             <>
             <>
-              <DropdownDivider role="none" />
+              <DropdownDivider />
               <li role="none">
               <li role="none">
                 <DropdownItem onClick={createWithAnalytics}>
                 <DropdownItem onClick={createWithAnalytics}>
                   {t('new_file')}
                   {t('new_file')}

+ 4 - 12
services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.jsx → 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 { useRef } from 'react'
-import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-
 import Icon from '../../../../shared/components/icon'
 import Icon from '../../../../shared/components/icon'
-
 import { useFileTreeMainContext } from '../../contexts/file-tree-main'
 import { useFileTreeMainContext } from '../../contexts/file-tree-main'
 import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import MaterialIcon from '@/shared/components/material-icon'
 import MaterialIcon from '@/shared/components/material-icon'
 
 
-function FileTreeItemMenu({ id, name }) {
+function FileTreeItemMenu({ id, name }: { id: string; name: string }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
   const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
-  const menuButtonRef = useRef()
+  const menuButtonRef = useRef<HTMLButtonElement>(null)
 
 
   const isMenuOpen = Boolean(contextMenuCoords)
   const isMenuOpen = Boolean(contextMenuCoords)
 
 
-  function handleClick(event) {
+  function handleClick(event: React.MouseEvent) {
     event.stopPropagation()
     event.stopPropagation()
-    if (!contextMenuCoords) {
+    if (!contextMenuCoords && menuButtonRef.current) {
       const target = menuButtonRef.current.getBoundingClientRect()
       const target = menuButtonRef.current.getBoundingClientRect()
       setContextMenuCoords({
       setContextMenuCoords({
         top: target.top + target.height / 2,
         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
 export default FileTreeItemMenu

+ 32 - 29
services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.jsx → 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 { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
-
 import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
 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 } =
   const { isRenaming, startRenaming, finishRenaming, error, cancel } =
     useFileTreeActionable()
     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)
   const [clicksInSelectedCount, setClicksInSelectedCount] = useState(0)
 
 
   function onClick() {
   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)
   const [value, setValue] = useState(initialValue)
 
 
   // The react-bootstrap Dropdown re-focuses on the Dropdown.Toggle
   // The react-bootstrap Dropdown re-focuses on the Dropdown.Toggle
@@ -84,16 +93,16 @@ function InputName({ initialValue, finishRenaming, cancel }) {
   // shown
   // shown
   const { autoFocusedRef } = useRefWithAutoFocus()
   const { autoFocusedRef } = useRefWithAutoFocus()
 
 
-  function handleFocus(ev) {
+  function handleFocus(ev: React.FocusEvent<HTMLInputElement>) {
     const lastDotIndex = ev.target.value.lastIndexOf('.')
     const lastDotIndex = ev.target.value.lastIndexOf('.')
     ev.target.setSelectionRange(0, lastDotIndex)
     ev.target.setSelectionRange(0, lastDotIndex)
   }
   }
 
 
-  function handleChange(ev) {
+  function handleChange(ev: React.ChangeEvent<HTMLInputElement>) {
     setValue(ev.target.value)
     setValue(ev.target.value)
   }
   }
 
 
-  function handleKeyDown(ev) {
+  function handleKeyDown(ev: React.KeyboardEvent<HTMLInputElement>) {
     if (ev.key === 'Enter') {
     if (ev.key === 'Enter') {
       finishRenaming(value)
       finishRenaming(value)
     }
     }
@@ -115,16 +124,10 @@ function InputName({ initialValue, finishRenaming, cancel }) {
         onChange={handleChange}
         onChange={handleChange}
         onBlur={handleBlur}
         onBlur={handleBlur}
         onFocus={handleFocus}
         onFocus={handleFocus}
-        ref={autoFocusedRef}
+        ref={autoFocusedRef as RefObject<HTMLInputElement>}
       />
       />
     </span>
     </span>
   )
   )
 }
 }
 
 
-InputName.propTypes = {
-  initialValue: PropTypes.string.isRequired,
-  finishRenaming: PropTypes.func.isRequired,
-  cancel: PropTypes.func.isRequired,
-}
-
 export default FileTreeItemName
 export default FileTreeItemName

+ 1 - 1
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,
             'file-tree-dragging': dragLayer.isDragging,
           }),
           }),
         }}
         }}
-        dropRef={dropRef as any}
+        dropRef={dropRef}
         dataTestId="file-tree-list-root"
         dataTestId="file-tree-list-root"
       />
       />
     </>
     </>

+ 9 - 2
services/web/test/frontend/features/file-tree/components/file-tree-folder-list.spec.tsx

@@ -31,8 +31,15 @@ describe('<FileTreeFolderList/>', function () {
                 fileRefs: [],
                 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>
         </FileTreeProvider>
       </EditorProviders>
       </EditorProviders>