first commit
This commit is contained in:
4
services/web/frontend/js/features/file-tree/util/api.ts
Normal file
4
services/web/frontend/js/features/file-tree/util/api.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { postJSON } from '../../../infrastructure/fetch-json'
|
||||
|
||||
export const refreshProjectMetadata = (projectId: string, entityId: string) =>
|
||||
postJSON(`/project/${projectId}/doc/${entityId}/metadata`)
|
@@ -0,0 +1,65 @@
|
||||
import getMeta from '@/utils/meta'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const value = _countElements(fileTreeData)
|
||||
|
||||
const limit = getMeta('ol-ExposedSettings').maxEntitiesPerProject
|
||||
const status = fileCountStatus(value, limit, Math.ceil(limit / 20))
|
||||
|
||||
return { value, status, limit }
|
||||
}
|
||||
|
||||
function fileCountStatus(
|
||||
value: number,
|
||||
limit: number,
|
||||
range: number
|
||||
): FileCountStatus {
|
||||
if (value >= limit) {
|
||||
return 'error'
|
||||
}
|
||||
|
||||
if (value >= limit - range) {
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// Copied and adapted from ProjectEntityMongoUpdateHandler
|
||||
function _countElements(rootFolder: Folder): number {
|
||||
function countFolder(folder: Folder) {
|
||||
if (folder == null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
let total = 0
|
||||
if (folder.folders) {
|
||||
total += folder.folders.length
|
||||
for (const subfolder of folder.folders) {
|
||||
total += countFolder(subfolder)
|
||||
}
|
||||
}
|
||||
if (folder.docs) {
|
||||
total += folder.docs.length
|
||||
}
|
||||
if (folder.fileRefs) {
|
||||
total += folder.fileRefs.length
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
return countFolder(rootFolder)
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { DocId, MainDocument } from '../../../../../types/project-settings'
|
||||
|
||||
function findAllDocsInFolder(folder: Folder, path = '') {
|
||||
const docs = folder.docs.map<MainDocument>(doc => ({
|
||||
doc: { id: doc._id as DocId, name: doc.name },
|
||||
path: path + doc.name,
|
||||
}))
|
||||
for (const subFolder of folder.folders) {
|
||||
docs.push(...findAllDocsInFolder(subFolder, `${path}${subFolder.name}/`))
|
||||
}
|
||||
return docs
|
||||
}
|
||||
|
||||
export function docsInFolder(folder: Folder) {
|
||||
const docsInTree = findAllDocsInFolder(folder)
|
||||
docsInTree.sort(function (a, b) {
|
||||
const aDepth = (a.path.match(/\//g) || []).length
|
||||
const bDepth = (b.path.match(/\//g) || []).length
|
||||
if (aDepth - bDepth !== 0) {
|
||||
return -(aDepth - bDepth) // Deeper path == folder first
|
||||
} else if (a.path < b.path) {
|
||||
return -1
|
||||
} else if (a.path > b.path) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
return docsInTree
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
// The collator used to sort files docs and folders in the tree.
|
||||
// Uses English as base language for consistency.
|
||||
// Options used:
|
||||
// numeric: true so 10 comes after 2
|
||||
// sensitivity: 'variant' so case and accent are not equal
|
||||
// caseFirst: 'upper' so upper-case letters come first
|
||||
export const fileCollator = new Intl.Collator('en', {
|
||||
numeric: true,
|
||||
sensitivity: 'variant',
|
||||
caseFirst: 'upper',
|
||||
})
|
@@ -0,0 +1,95 @@
|
||||
import OError from '@overleaf/o-error'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { FileTreeFindResult } from '@/features/ide-react/types/file-tree'
|
||||
|
||||
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: Folder,
|
||||
ids: Set<string>
|
||||
): Set<FileTreeFindResult> {
|
||||
const list: Set<FileTreeFindResult> = new Set()
|
||||
ids.forEach(id => {
|
||||
list.add(findInTreeOrThrow(tree, id))
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
export function findAllFolderIdsInFolder(folder: Folder): Set<string> {
|
||||
const list = new Set([folder._id])
|
||||
for (const index in folder.folders) {
|
||||
const subFolder = folder.folders[index]
|
||||
findAllFolderIdsInFolder(subFolder).forEach(subFolderId => {
|
||||
list.add(subFolderId)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export function findAllFolderIdsInFolders(folders: Set<Folder>): Set<string> {
|
||||
const list: Set<string> = new Set()
|
||||
folders.forEach(folder => {
|
||||
findAllFolderIdsInFolder(folder).forEach(folderId => {
|
||||
list.add(folderId)
|
||||
})
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
export function findInTree(
|
||||
tree: Folder,
|
||||
id: string,
|
||||
path?: string[]
|
||||
): FileTreeFindResult | null {
|
||||
if (!path) {
|
||||
path = [tree._id]
|
||||
}
|
||||
for (const index in tree.docs) {
|
||||
const doc = tree.docs[index]
|
||||
if (doc._id === id) {
|
||||
return {
|
||||
entity: doc,
|
||||
type: 'doc',
|
||||
parent: tree.docs,
|
||||
parentFolderId: tree._id,
|
||||
path,
|
||||
index: Number(index),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const index in tree.fileRefs) {
|
||||
const file = tree.fileRefs[index]
|
||||
if (file._id === id) {
|
||||
return {
|
||||
entity: file,
|
||||
type: 'fileRef',
|
||||
parent: tree.fileRefs,
|
||||
parentFolderId: tree._id,
|
||||
path,
|
||||
index: Number(index),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const index in tree.folders) {
|
||||
const folder = tree.folders[index]
|
||||
if (folder._id === id) {
|
||||
return {
|
||||
entity: folder,
|
||||
type: 'folder',
|
||||
parent: tree.folders,
|
||||
parentFolderId: tree._id,
|
||||
path,
|
||||
index: Number(index),
|
||||
}
|
||||
}
|
||||
const found = findInTree(folder, id, path.concat(folder._id))
|
||||
if (found) return found
|
||||
}
|
||||
return null
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { AvailableUnfilledIcon } from '@/shared/components/material-icon'
|
||||
|
||||
// TODO ide-redesign-cleanup: Make this the default export and remove the legacy version
|
||||
export const newEditorIconTypeFromName = (
|
||||
name: string
|
||||
): AvailableUnfilledIcon => {
|
||||
let ext = name.split('.').pop()
|
||||
ext = ext ? ext.toLowerCase() : ext
|
||||
|
||||
if (ext && ['png', 'pdf', 'jpg', 'jpeg', 'gif'].includes(ext)) {
|
||||
return 'image'
|
||||
} else if (ext && ['csv', 'xls', 'xlsx'].includes(ext)) {
|
||||
return 'table_chart'
|
||||
} else if (ext && ['py', 'r'].includes(ext)) {
|
||||
return 'code'
|
||||
} else if (ext && ['bib'].includes(ext)) {
|
||||
return 'book_5'
|
||||
}
|
||||
return 'description'
|
||||
}
|
||||
|
||||
export default function iconTypeFromName(name: string): string {
|
||||
let ext = name.split('.').pop()
|
||||
ext = ext ? ext.toLowerCase() : ext
|
||||
|
||||
if (ext && ['png', 'pdf', 'jpg', 'jpeg', 'gif'].includes(ext)) {
|
||||
return 'image'
|
||||
} else if (ext && ['csv', 'xls', 'xlsx'].includes(ext)) {
|
||||
return 'table_chart'
|
||||
} else if (ext && ['py', 'r'].includes(ext)) {
|
||||
return 'code'
|
||||
} else if (ext && ['bib'].includes(ext)) {
|
||||
return 'menu_book'
|
||||
} else {
|
||||
return 'description'
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import { Minimatch } from 'minimatch'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
let fileIgnoreMatcher: Minimatch
|
||||
|
||||
export const isAcceptableFile = (name?: string, relativePath?: string) => {
|
||||
if (!fileIgnoreMatcher) {
|
||||
fileIgnoreMatcher = new Minimatch(
|
||||
getMeta('ol-ExposedSettings').fileIgnorePattern,
|
||||
{ nocase: true, dot: true }
|
||||
)
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
// the file must have a name, of course
|
||||
return false
|
||||
}
|
||||
|
||||
if (!relativePath) {
|
||||
// uploading an individual file, so allow anything
|
||||
return true
|
||||
}
|
||||
|
||||
// uploading a file in a folder, so exclude unwanted file paths
|
||||
return !fileIgnoreMatcher.match(relativePath + '/' + name)
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
import { findInTreeOrThrow } from '../util/find-in-tree'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { Doc } from '../../../../../types/doc'
|
||||
import { FileRef } from '../../../../../types/file-ref'
|
||||
|
||||
export function isNameUniqueInFolder(
|
||||
tree: Folder,
|
||||
parentFolderId: string,
|
||||
name: string
|
||||
): boolean {
|
||||
return !(
|
||||
findFileByNameInFolder(tree, parentFolderId, name) ||
|
||||
findFolderByNameInFolder(tree, parentFolderId, name)
|
||||
)
|
||||
}
|
||||
|
||||
export function findFileByNameInFolder(
|
||||
tree: Folder,
|
||||
parentFolderId: string,
|
||||
name: string
|
||||
): Doc | FileRef | undefined {
|
||||
if (tree._id !== parentFolderId) {
|
||||
tree = findInTreeOrThrow(tree, parentFolderId).entity as Folder
|
||||
}
|
||||
|
||||
return (
|
||||
tree.docs.find(entity => entity.name === name) ||
|
||||
tree.fileRefs.find(entity => entity.name === name)
|
||||
)
|
||||
}
|
||||
|
||||
export function findFolderByNameInFolder(
|
||||
tree: Folder,
|
||||
parentFolderId: string,
|
||||
name: string
|
||||
): Folder | undefined {
|
||||
if (tree._id !== parentFolderId) {
|
||||
tree = findInTreeOrThrow(tree, parentFolderId).entity as Folder
|
||||
}
|
||||
|
||||
return tree.folders.find(entity => entity.name === name)
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
import { findInTreeOrThrow } from './find-in-tree'
|
||||
|
||||
export function renameInTree(tree, id, { newName }) {
|
||||
return mutateInTree(tree, id, (parent, entity, index) => {
|
||||
const newParent = Object.assign([], parent)
|
||||
const newEntity = {
|
||||
...entity,
|
||||
name: newName,
|
||||
}
|
||||
newParent[index] = newEntity
|
||||
return newParent
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteInTree(tree, id) {
|
||||
return mutateInTree(tree, id, (parent, entity, index) => {
|
||||
return [...parent.slice(0, index), ...parent.slice(index + 1)]
|
||||
})
|
||||
}
|
||||
|
||||
export function moveInTree(tree, entityId, toFolderId) {
|
||||
const found = findInTreeOrThrow(tree, entityId)
|
||||
if (found.parentFolderId === toFolderId) {
|
||||
// nothing to do (the entity was probably already moved)
|
||||
return tree
|
||||
}
|
||||
const newFileTreeData = deleteInTree(tree, entityId)
|
||||
return createEntityInTree(newFileTreeData, toFolderId, {
|
||||
...found.entity,
|
||||
type: found.type,
|
||||
})
|
||||
}
|
||||
|
||||
export function createEntityInTree(tree, parentFolderId, newEntityData) {
|
||||
const { type, ...newEntity } = newEntityData
|
||||
if (!type) throw new Error('Entity has no type')
|
||||
const entityType = `${type}s`
|
||||
|
||||
return mutateInTree(tree, parentFolderId, (parent, folder, index) => {
|
||||
parent[index] = {
|
||||
...folder,
|
||||
[entityType]: [...folder[entityType], newEntity],
|
||||
}
|
||||
return parent
|
||||
})
|
||||
}
|
||||
|
||||
function mutateInTree(tree, id, mutationFunction) {
|
||||
if (!id || tree._id === id) {
|
||||
// covers the root folder case: it has no parent so in order to use
|
||||
// mutationFunction we pass an empty array as the parent and return the
|
||||
// mutated tree directly
|
||||
const [newTree] = mutationFunction([], tree, 0)
|
||||
return newTree
|
||||
}
|
||||
|
||||
for (const entityType of ['docs', 'fileRefs', 'folders']) {
|
||||
for (let index = 0; index < tree[entityType].length; index++) {
|
||||
const entity = tree[entityType][index]
|
||||
if (entity._id === id) {
|
||||
return {
|
||||
...tree,
|
||||
[entityType]: mutationFunction(tree[entityType], entity, index),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newFolders = tree.folders.map(folder =>
|
||||
mutateInTree(folder, id, mutationFunction)
|
||||
)
|
||||
|
||||
return { ...tree, folders: newFolders }
|
||||
}
|
139
services/web/frontend/js/features/file-tree/util/path.ts
Normal file
139
services/web/frontend/js/features/file-tree/util/path.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { FileTreeEntity } from '../../../../../types/file-tree-entity'
|
||||
import { Doc } from '../../../../../types/doc'
|
||||
import { FileRef } from '../../../../../types/file-ref'
|
||||
import { PreviewPath } from '../../../../../types/preview-path'
|
||||
import { fileUrl } from '../../utils/fileUrl'
|
||||
|
||||
type DocFindResult = {
|
||||
entity: Doc
|
||||
type: 'doc'
|
||||
}
|
||||
|
||||
type FolderFindResult = {
|
||||
entity: Folder
|
||||
type: 'folder'
|
||||
}
|
||||
|
||||
type FileRefFindResult = {
|
||||
entity: FileRef
|
||||
type: 'fileRef'
|
||||
}
|
||||
|
||||
export type FindResult = DocFindResult | FolderFindResult | FileRefFindResult
|
||||
|
||||
// Finds the entity with a given ID in the tree represented by `folder` and
|
||||
// returns a path to that entity, represented by an array of folders starting at
|
||||
// the root plus the entity itself
|
||||
function pathComponentsInFolder(
|
||||
folder: Folder,
|
||||
id: string,
|
||||
ancestors: FileTreeEntity[] = []
|
||||
): FileTreeEntity[] | null {
|
||||
const docOrFileRef =
|
||||
folder.docs.find(doc => doc._id === id) ||
|
||||
folder.fileRefs.find(fileRef => fileRef._id === id)
|
||||
if (docOrFileRef) {
|
||||
return ancestors.concat([docOrFileRef])
|
||||
}
|
||||
|
||||
for (const subfolder of folder.folders) {
|
||||
if (subfolder._id === id) {
|
||||
return ancestors.concat([subfolder])
|
||||
} else {
|
||||
const path = pathComponentsInFolder(
|
||||
subfolder,
|
||||
id,
|
||||
ancestors.concat([subfolder])
|
||||
)
|
||||
if (path !== null) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Finds the entity with a given ID in the tree represented by `folder` and
|
||||
// returns a path to that entity as a string
|
||||
export function pathInFolder(folder: Folder, id: string): string | null {
|
||||
return (
|
||||
pathComponentsInFolder(folder, id)
|
||||
?.map(entity => entity.name)
|
||||
.join('/') || null
|
||||
)
|
||||
}
|
||||
|
||||
export function findEntityByPath(
|
||||
folder: Folder,
|
||||
path: string
|
||||
): FindResult | null {
|
||||
if (path === '') {
|
||||
return { entity: folder, type: 'folder' }
|
||||
}
|
||||
|
||||
const parts = path.split('/')
|
||||
const name = parts.shift()
|
||||
const rest = parts.join('/')
|
||||
|
||||
if (name === '.') {
|
||||
return findEntityByPath(folder, rest)
|
||||
}
|
||||
|
||||
const doc = folder.docs.find(doc => doc.name === name)
|
||||
if (doc) {
|
||||
return { entity: doc, type: 'doc' }
|
||||
}
|
||||
|
||||
const fileRef = folder.fileRefs.find(fileRef => fileRef.name === name)
|
||||
if (fileRef) {
|
||||
return { entity: fileRef, type: 'fileRef' }
|
||||
}
|
||||
|
||||
for (const subfolder of folder.folders) {
|
||||
if (subfolder.name === name) {
|
||||
if (rest === '') {
|
||||
return { entity: subfolder, type: 'folder' }
|
||||
} else {
|
||||
return findEntityByPath(subfolder, rest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function previewByPath(
|
||||
folder: Folder,
|
||||
projectId: string,
|
||||
path: string
|
||||
): PreviewPath | null {
|
||||
for (const suffix of [
|
||||
'',
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.pdf',
|
||||
'.PNG',
|
||||
'.JPG',
|
||||
'.JPEG',
|
||||
'.PDF',
|
||||
]) {
|
||||
const result = findEntityByPath(folder, path + suffix)
|
||||
|
||||
if (result?.type === 'fileRef') {
|
||||
const { name, _id: id, hash } = result.entity
|
||||
return {
|
||||
url: fileUrl(projectId, id, hash),
|
||||
extension: name.slice(name.lastIndexOf('.') + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function dirname(fileTreeData: Folder, id: string) {
|
||||
const path = pathInFolder(fileTreeData, id)
|
||||
return path?.split('/').slice(0, -1).join('/') || null
|
||||
}
|
110
services/web/frontend/js/features/file-tree/util/safe-path.ts
Normal file
110
services/web/frontend/js/features/file-tree/util/safe-path.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// This file is shared between the frontend and server code of web, so that
|
||||
// filename validation is the same in both implementations.
|
||||
// The logic in all copies must be kept in sync:
|
||||
// app/src/Features/Project/SafePath.js
|
||||
// frontend/js/ide/directives/SafePath.js
|
||||
// frontend/js/features/file-tree/util/safe-path.js
|
||||
// eslint-disable-next-line prefer-regex-literals
|
||||
const BADCHAR_RX = new RegExp(
|
||||
`\
|
||||
[\
|
||||
\\/\
|
||||
\\\\\
|
||||
\\*\
|
||||
\\u0000-\\u001F\
|
||||
\\u007F\
|
||||
\\u0080-\\u009F\
|
||||
\\uD800-\\uDFFF\
|
||||
]\
|
||||
`,
|
||||
'g'
|
||||
)
|
||||
// eslint-disable-next-line prefer-regex-literals
|
||||
const BADFILE_RX = new RegExp(
|
||||
`\
|
||||
(^\\.$)\
|
||||
|(^\\.\\.$)\
|
||||
|(^\\s+)\
|
||||
|(\\s+$)\
|
||||
`,
|
||||
'g'
|
||||
)
|
||||
|
||||
// Put a block on filenames which match javascript property names, as they
|
||||
// can cause exceptions where the code puts filenames into a hash. This is a
|
||||
// temporary workaround until the code in other places is made safe against
|
||||
// property names.
|
||||
//
|
||||
// The list of property names is taken from
|
||||
// ['prototype'].concat(Object.getOwnPropertyNames(Object.prototype))
|
||||
// eslint-disable-next-line prefer-regex-literals
|
||||
const BLOCKEDFILE_RX = new RegExp(`\
|
||||
^(\
|
||||
prototype\
|
||||
|constructor\
|
||||
|toString\
|
||||
|toLocaleString\
|
||||
|valueOf\
|
||||
|hasOwnProperty\
|
||||
|isPrototypeOf\
|
||||
|propertyIsEnumerable\
|
||||
|__defineGetter__\
|
||||
|__lookupGetter__\
|
||||
|__defineSetter__\
|
||||
|__lookupSetter__\
|
||||
|__proto__\
|
||||
)$\
|
||||
`)
|
||||
|
||||
const MAX_PATH = 1024 // Maximum path length, in characters. This is fairly arbitrary.
|
||||
|
||||
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 =>
|
||||
new Array(match.length + 1).join('_')
|
||||
)
|
||||
// replace blocked filenames 'prototype' with '@prototype'
|
||||
filename = filename.replace(BLOCKEDFILE_RX, '@$1')
|
||||
return filename
|
||||
}
|
||||
|
||||
export function isCleanFilename(filename: string): boolean {
|
||||
return (
|
||||
isAllowedLength(filename) &&
|
||||
!filename.match(BADCHAR_RX) &&
|
||||
!filename.match(BADFILE_RX)
|
||||
)
|
||||
}
|
||||
|
||||
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: string): boolean {
|
||||
const elements = path.split('/')
|
||||
|
||||
const lastElementIsEmpty = elements[elements.length - 1].length === 0
|
||||
if (lastElementIsEmpty) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const element of Array.from(elements)) {
|
||||
if (element.length > 0 && !isCleanFilename(element)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// check for a top-level reserved name
|
||||
if (BLOCKEDFILE_RX.test(path.replace(/^\/?/, ''))) {
|
||||
return false
|
||||
} // remove leading slash if present
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function isAllowedLength(pathname: string): boolean {
|
||||
return pathname.length > 0 && pathname.length <= MAX_PATH
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
import { postJSON, deleteJSON } from '../../../infrastructure/fetch-json'
|
||||
|
||||
export function syncRename(
|
||||
projectId: string,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
newName: string
|
||||
) {
|
||||
return postJSON(
|
||||
`/project/${projectId}/${getEntityPathName(entityType)}/${entityId}/rename`,
|
||||
{
|
||||
body: {
|
||||
name: newName,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function syncDelete(
|
||||
projectId: string,
|
||||
entityType: string,
|
||||
entityId: string
|
||||
) {
|
||||
return deleteJSON(
|
||||
`/project/${projectId}/${getEntityPathName(entityType)}/${entityId}`
|
||||
)
|
||||
}
|
||||
|
||||
export function syncMove(
|
||||
projectId: string,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
toFolderId: string
|
||||
) {
|
||||
return postJSON(
|
||||
`/project/${projectId}/${getEntityPathName(entityType)}/${entityId}/move`,
|
||||
{
|
||||
body: {
|
||||
folder_id: toFolderId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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: {
|
||||
parent_folder_id: parentFolderId,
|
||||
...newEntity,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getEntityPathName(entityType: string) {
|
||||
return entityType === 'fileRef' ? 'file' : entityType
|
||||
}
|
Reference in New Issue
Block a user