From 60698d8d3741cc4830aeb452b77ab561894734de Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:10:39 +0000 Subject: [PATCH] Merge pull request #22836 from overleaf/dp-filetree-typescript Convert file-tree util files to typescript GitOrigin-RevId: bdf8d0655a543a216f028bc8477c3ee47aba5566 --- .../contexts/file-tree-actionable.tsx | 6 ++-- .../contexts/file-tree-draggable.tsx | 3 +- .../contexts/file-tree-selectable.tsx | 6 ++-- .../file-tree/util/{api.js => api.ts} | 2 +- .../{count-in-tree.js => count-in-tree.ts} | 21 ++++++++++--- .../{file-collator.js => file-collator.ts} | 0 .../util/{find-in-tree.js => find-in-tree.ts} | 29 +++++++++++------ ...pe-from-name.js => icon-type-from-name.ts} | 10 +++--- .../util/is-name-unique-in-folder.ts | 6 ++-- .../util/{safe-path.js => safe-path.ts} | 10 +++--- .../{sync-mutation.js => sync-mutation.ts} | 31 ++++++++++++++++--- .../js/features/ide-react/types/file-tree.ts | 2 +- 12 files changed, 85 insertions(+), 41 deletions(-) rename services/web/frontend/js/features/file-tree/util/{api.js => api.ts} (61%) rename services/web/frontend/js/features/file-tree/util/{count-in-tree.js => count-in-tree.ts} (66%) rename services/web/frontend/js/features/file-tree/util/{file-collator.js => file-collator.ts} (100%) rename services/web/frontend/js/features/file-tree/util/{find-in-tree.js => find-in-tree.ts} (68%) rename services/web/frontend/js/features/file-tree/util/{icon-type-from-name.js => icon-type-from-name.ts} (56%) rename services/web/frontend/js/features/file-tree/util/{safe-path.js => safe-path.ts} (90%) rename services/web/frontend/js/features/file-tree/util/{sync-mutation.js => sync-mutation.ts} (58%) diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx index aaba66661d..6223c49054 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx @@ -60,7 +60,7 @@ const FileTreeActionableContext = createContext< canRename: boolean canCreate: boolean parentFolderId: string - selectedFileName: string | null + selectedFileName: string | null | undefined isDuplicate: (parentFolderId: string, name: string) => boolean startRenaming: any finishRenaming: any @@ -320,7 +320,7 @@ export const FileTreeActionableProvider: FC = ({ children }) => { // moves entities. Tree is updated immediately and data are sync'd after. const finishMoving = useCallback( - (toFolderId, draggedEntityIds) => { + (toFolderId: string, draggedEntityIds: Set) => { dispatch({ type: ACTION_TYPES.MOVING }) // find entities and filter out no-ops and nested files @@ -624,7 +624,7 @@ function validateCreate( function validateRename( fileTreeData: Folder, - found: { parentFolderId: string; path: string; type: string }, + found: { parentFolderId: string; path: string[]; type: string }, newName: string ) { if (!isCleanFilename(newName)) { diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.tsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.tsx index 164611042e..c255c25231 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.tsx +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.tsx @@ -15,6 +15,7 @@ import { useFileTreeActionable } from './file-tree-actionable' import { useFileTreeData } from '@/shared/context/file-tree-data-context' import { useFileTreeSelectable } from '../contexts/file-tree-selectable' import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file' +import { FileTreeFindResult } from '@/features/ide-react/types/file-tree' const DRAGGABLE_TYPE = 'ENTITY' export const FileTreeDraggableProvider: FC<{ @@ -171,7 +172,7 @@ function getDraggedTitle( } // Get all children folder ids of any of the dragged items. -function getForbiddenFolderIds(draggedItems: Set) { +function getForbiddenFolderIds(draggedItems: Set) { const draggedFoldersArray = Array.from(draggedItems) .filter(draggedItem => { return draggedItem.type === 'folder' diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.tsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.tsx index 2d3b8cd0f9..e7ee96803c 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.tsx +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.tsx @@ -10,7 +10,7 @@ import { } from 'react' import classNames from 'classnames' import _ from 'lodash' -import { findInTree } from '../util/find-in-tree' +import { findInTree, findInTreeOrThrow } from '../util/find-in-tree' import { useFileTreeData } from '../../../shared/context/file-tree-data-context' import { useProjectContext } from '../../../shared/context/project-context' import { useLayoutContext } from '../../../shared/context/layout-context' @@ -177,7 +177,7 @@ export const FileTreeSelectableProvider: FC<{ } const _selectedEntities = Array.from(selectedEntityIds) .map(id => findInTree(fileTreeData, id)) - .filter(Boolean) + .filter(entity => entity !== null) onSelect(_selectedEntities) setSelectedEntities(_selectedEntities) }, [ @@ -282,7 +282,7 @@ export function useSelectableEntity(id: string, type: string) { const chooseView = useCallback(() => { for (const id of selectedEntityIds) { - const selectedEntity = findInTree(fileTreeData, id) + const selectedEntity = findInTreeOrThrow(fileTreeData, id) if (selectedEntity.type === 'doc') { return 'editor' diff --git a/services/web/frontend/js/features/file-tree/util/api.js b/services/web/frontend/js/features/file-tree/util/api.ts similarity index 61% rename from services/web/frontend/js/features/file-tree/util/api.js rename to services/web/frontend/js/features/file-tree/util/api.ts index 6bd6bd90b6..cbc67a255b 100644 --- a/services/web/frontend/js/features/file-tree/util/api.js +++ b/services/web/frontend/js/features/file-tree/util/api.ts @@ -1,4 +1,4 @@ import { postJSON } from '../../../infrastructure/fetch-json' -export const refreshProjectMetadata = (projectId, entityId) => +export const refreshProjectMetadata = (projectId: string, entityId: string) => postJSON(`/project/${projectId}/doc/${entityId}/metadata`) diff --git a/services/web/frontend/js/features/file-tree/util/count-in-tree.js b/services/web/frontend/js/features/file-tree/util/count-in-tree.ts similarity index 66% rename from services/web/frontend/js/features/file-tree/util/count-in-tree.js rename to services/web/frontend/js/features/file-tree/util/count-in-tree.ts index 34020a6366..04654d0953 100644 --- a/services/web/frontend/js/features/file-tree/util/count-in-tree.js +++ b/services/web/frontend/js/features/file-tree/util/count-in-tree.ts @@ -1,6 +1,15 @@ import getMeta from '@/utils/meta' +import { Folder } from '../../../../../types/folder' -export function countFiles(fileTreeData) { +type FileCountStatus = 'success' | 'warning' | 'error' + +type FileCount = { + value: number + status: FileCountStatus + limit: number +} + +export function countFiles(fileTreeData: Folder | undefined): 0 | FileCount { if (!fileTreeData) { return 0 } @@ -13,7 +22,11 @@ export function countFiles(fileTreeData) { return { value, status, limit } } -function fileCountStatus(value, limit, range) { +function fileCountStatus( + value: number, + limit: number, + range: number +): FileCountStatus { if (value >= limit) { return 'error' } @@ -26,8 +39,8 @@ function fileCountStatus(value, limit, range) { } // Copied and adapted from ProjectEntityMongoUpdateHandler -function _countElements(rootFolder) { - function countFolder(folder) { +function _countElements(rootFolder: Folder): number { + function countFolder(folder: Folder) { if (folder == null) { return 0 } diff --git a/services/web/frontend/js/features/file-tree/util/file-collator.js b/services/web/frontend/js/features/file-tree/util/file-collator.ts similarity index 100% rename from services/web/frontend/js/features/file-tree/util/file-collator.js rename to services/web/frontend/js/features/file-tree/util/file-collator.ts diff --git a/services/web/frontend/js/features/file-tree/util/find-in-tree.js b/services/web/frontend/js/features/file-tree/util/find-in-tree.ts similarity index 68% rename from services/web/frontend/js/features/file-tree/util/find-in-tree.js rename to services/web/frontend/js/features/file-tree/util/find-in-tree.ts index 9e82c25a08..2cf3ef49c0 100644 --- a/services/web/frontend/js/features/file-tree/util/find-in-tree.js +++ b/services/web/frontend/js/features/file-tree/util/find-in-tree.ts @@ -1,20 +1,25 @@ import OError from '@overleaf/o-error' +import { Folder } from '../../../../../types/folder' +import { FileTreeFindResult } from '@/features/ide-react/types/file-tree' -export function findInTreeOrThrow(tree, id) { +export function findInTreeOrThrow(tree: Folder, id: string) { const found = findInTree(tree, id) if (found) return found throw new OError('Entity not found in tree', { entityId: id }) } -export function findAllInTreeOrThrow(tree, ids) { - const list = new Set() +export function findAllInTreeOrThrow( + tree: Folder, + ids: Set +): Set { + const list: Set = new Set() ids.forEach(id => { list.add(findInTreeOrThrow(tree, id)) }) return list } -export function findAllFolderIdsInFolder(folder) { +export function findAllFolderIdsInFolder(folder: Folder): Set { const list = new Set([folder._id]) for (const index in folder.folders) { const subFolder = folder.folders[index] @@ -25,8 +30,8 @@ export function findAllFolderIdsInFolder(folder) { return list } -export function findAllFolderIdsInFolders(folders) { - const list = new Set() +export function findAllFolderIdsInFolders(folders: Set): Set { + const list: Set = new Set() folders.forEach(folder => { findAllFolderIdsInFolder(folder).forEach(folderId => { list.add(folderId) @@ -35,7 +40,11 @@ export function findAllFolderIdsInFolders(folders) { return list } -export function findInTree(tree, id, path) { +export function findInTree( + tree: Folder, + id: string, + path?: string[] +): FileTreeFindResult | null { if (!path) { path = [tree._id] } @@ -48,7 +57,7 @@ export function findInTree(tree, id, path) { parent: tree.docs, parentFolderId: tree._id, path, - index, + index: Number(index), } } } @@ -62,7 +71,7 @@ export function findInTree(tree, id, path) { parent: tree.fileRefs, parentFolderId: tree._id, path, - index, + index: Number(index), } } } @@ -76,7 +85,7 @@ export function findInTree(tree, id, path) { parent: tree.folders, parentFolderId: tree._id, path, - index, + index: Number(index), } } const found = findInTree(folder, id, path.concat(folder._id)) diff --git a/services/web/frontend/js/features/file-tree/util/icon-type-from-name.js b/services/web/frontend/js/features/file-tree/util/icon-type-from-name.ts similarity index 56% rename from services/web/frontend/js/features/file-tree/util/icon-type-from-name.js rename to services/web/frontend/js/features/file-tree/util/icon-type-from-name.ts index 05f8ed0e1f..68eb393fec 100644 --- a/services/web/frontend/js/features/file-tree/util/icon-type-from-name.js +++ b/services/web/frontend/js/features/file-tree/util/icon-type-from-name.ts @@ -1,16 +1,16 @@ import { isBootstrap5 } from '@/features/utils/bootstrap-5' -export default function iconTypeFromName(name) { +export default function iconTypeFromName(name: string): string { let ext = name.split('.').pop() ext = ext ? ext.toLowerCase() : ext - if (['png', 'pdf', 'jpg', 'jpeg', 'gif'].includes(ext)) { + if (ext && ['png', 'pdf', 'jpg', 'jpeg', 'gif'].includes(ext)) { return 'image' - } else if (['csv', 'xls', 'xlsx'].includes(ext)) { + } else if (ext && ['csv', 'xls', 'xlsx'].includes(ext)) { return isBootstrap5() ? 'table_chart' : 'table' - } else if (['py', 'r'].includes(ext)) { + } else if (ext && ['py', 'r'].includes(ext)) { return isBootstrap5() ? 'code' : 'file-text' - } else if (['bib'].includes(ext)) { + } else if (ext && ['bib'].includes(ext)) { return isBootstrap5() ? 'menu_book' : 'book' } else { return isBootstrap5() ? 'description' : 'file' diff --git a/services/web/frontend/js/features/file-tree/util/is-name-unique-in-folder.ts b/services/web/frontend/js/features/file-tree/util/is-name-unique-in-folder.ts index 7a53ceb446..63d35703f4 100644 --- a/services/web/frontend/js/features/file-tree/util/is-name-unique-in-folder.ts +++ b/services/web/frontend/js/features/file-tree/util/is-name-unique-in-folder.ts @@ -1,4 +1,4 @@ -import { findInTree } from '../util/find-in-tree' +import { findInTreeOrThrow } from '../util/find-in-tree' import { Folder } from '../../../../../types/folder' import { Doc } from '../../../../../types/doc' import { FileRef } from '../../../../../types/file-ref' @@ -20,7 +20,7 @@ export function findFileByNameInFolder( name: string ): Doc | FileRef | undefined { if (tree._id !== parentFolderId) { - tree = findInTree(tree, parentFolderId).entity + tree = findInTreeOrThrow(tree, parentFolderId).entity as Folder } return ( @@ -35,7 +35,7 @@ export function findFolderByNameInFolder( name: string ): Folder | undefined { if (tree._id !== parentFolderId) { - tree = findInTree(tree, parentFolderId).entity + tree = findInTreeOrThrow(tree, parentFolderId).entity as Folder } return tree.folders.find(entity => entity.name === name) diff --git a/services/web/frontend/js/features/file-tree/util/safe-path.js b/services/web/frontend/js/features/file-tree/util/safe-path.ts similarity index 90% rename from services/web/frontend/js/features/file-tree/util/safe-path.js rename to services/web/frontend/js/features/file-tree/util/safe-path.ts index 26f76b4cad..2228440298 100644 --- a/services/web/frontend/js/features/file-tree/util/safe-path.js +++ b/services/web/frontend/js/features/file-tree/util/safe-path.ts @@ -58,7 +58,7 @@ prototype\ const MAX_PATH = 1024 // Maximum path length, in characters. This is fairly arbitrary. -export function clean(filename) { +export function clean(filename: string): string { filename = filename.replace(BADCHAR_RX, '_') // for BADFILE_RX replace any matches with an equal number of underscores filename = filename.replace(BADFILE_RX, match => @@ -69,7 +69,7 @@ export function clean(filename) { return filename } -export function isCleanFilename(filename) { +export function isCleanFilename(filename: string): boolean { return ( isAllowedLength(filename) && !filename.match(BADCHAR_RX) && @@ -77,13 +77,13 @@ export function isCleanFilename(filename) { ) } -export function isBlockedFilename(filename) { +export function isBlockedFilename(filename: string): boolean { return BLOCKEDFILE_RX.test(filename) } // returns whether a full path is 'clean' - e.g. is a full or relative path // that points to a file, and each element passes the rules in 'isCleanFilename' -export function isCleanPath(path) { +export function isCleanPath(path: string): boolean { const elements = path.split('/') const lastElementIsEmpty = elements[elements.length - 1].length === 0 @@ -105,6 +105,6 @@ export function isCleanPath(path) { return true } -export function isAllowedLength(pathname) { +export function isAllowedLength(pathname: string): boolean { return pathname.length > 0 && pathname.length <= MAX_PATH } diff --git a/services/web/frontend/js/features/file-tree/util/sync-mutation.js b/services/web/frontend/js/features/file-tree/util/sync-mutation.ts similarity index 58% rename from services/web/frontend/js/features/file-tree/util/sync-mutation.js rename to services/web/frontend/js/features/file-tree/util/sync-mutation.ts index 74bd98a833..343a663b96 100644 --- a/services/web/frontend/js/features/file-tree/util/sync-mutation.js +++ b/services/web/frontend/js/features/file-tree/util/sync-mutation.ts @@ -1,6 +1,11 @@ import { postJSON, deleteJSON } from '../../../infrastructure/fetch-json' -export function syncRename(projectId, entityType, entityId, newName) { +export function syncRename( + projectId: string, + entityType: string, + entityId: string, + newName: string +) { return postJSON( `/project/${projectId}/${getEntityPathName(entityType)}/${entityId}/rename`, { @@ -11,13 +16,22 @@ export function syncRename(projectId, entityType, entityId, newName) { ) } -export function syncDelete(projectId, entityType, entityId) { +export function syncDelete( + projectId: string, + entityType: string, + entityId: string +) { return deleteJSON( `/project/${projectId}/${getEntityPathName(entityType)}/${entityId}` ) } -export function syncMove(projectId, entityType, entityId, toFolderId) { +export function syncMove( + projectId: string, + entityType: string, + entityId: string, + toFolderId: string +) { return postJSON( `/project/${projectId}/${getEntityPathName(entityType)}/${entityId}/move`, { @@ -28,7 +42,14 @@ export function syncMove(projectId, entityType, entityId, toFolderId) { ) } -export function syncCreateEntity(projectId, parentFolderId, newEntityData) { +export function syncCreateEntity( + projectId: string, + parentFolderId: string, + newEntityData: { + endpoint: 'doc' | 'folder' | 'linked-file' + [key: string]: unknown + } +) { const { endpoint, ...newEntity } = newEntityData return postJSON(`/project/${projectId}/${endpoint}`, { body: { @@ -38,6 +59,6 @@ export function syncCreateEntity(projectId, parentFolderId, newEntityData) { }) } -function getEntityPathName(entityType) { +function getEntityPathName(entityType: string) { return entityType === 'fileRef' ? 'file' : entityType } diff --git a/services/web/frontend/js/features/ide-react/types/file-tree.ts b/services/web/frontend/js/features/ide-react/types/file-tree.ts index d115cafff6..648ed99a25 100644 --- a/services/web/frontend/js/features/ide-react/types/file-tree.ts +++ b/services/web/frontend/js/features/ide-react/types/file-tree.ts @@ -9,7 +9,7 @@ interface BaseFileTreeFindResult { entity: T parent: T[] parentFolderId: string - path: string + path: string[] index: number }