mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #19400 from overleaf/dp-duplicate-file-folder-name
Improvements to handling of file/folder upload conflicts GitOrigin-RevId: 526edf30dfbaec7ee1e03ffd156365f09be25e86
This commit is contained in:
parent
c07d2f3fa2
commit
d7357b4d62
14 changed files with 203 additions and 94 deletions
|
@ -65,6 +65,12 @@ async function handleError(error, req, res, next) {
|
||||||
res.status(400)
|
res.status(400)
|
||||||
plainTextResponse(res, error.message)
|
plainTextResponse(res, error.message)
|
||||||
}
|
}
|
||||||
|
} else if (error instanceof Errors.DuplicateNameError) {
|
||||||
|
req.logger.setLevel('warn')
|
||||||
|
if (shouldSendErrorResponse) {
|
||||||
|
res.status(400)
|
||||||
|
plainTextResponse(res, error.message)
|
||||||
|
}
|
||||||
} else if (error instanceof Errors.InvalidNameError) {
|
} else if (error instanceof Errors.InvalidNameError) {
|
||||||
req.logger.setLevel('warn')
|
req.logger.setLevel('warn')
|
||||||
if (shouldSendErrorResponse) {
|
if (shouldSendErrorResponse) {
|
||||||
|
|
|
@ -38,6 +38,8 @@ class ServiceNotConfiguredError extends BackwardCompatibleError {}
|
||||||
|
|
||||||
class TooManyRequestsError extends BackwardCompatibleError {}
|
class TooManyRequestsError extends BackwardCompatibleError {}
|
||||||
|
|
||||||
|
class DuplicateNameError extends OError {}
|
||||||
|
|
||||||
class InvalidNameError extends BackwardCompatibleError {}
|
class InvalidNameError extends BackwardCompatibleError {}
|
||||||
|
|
||||||
class UnsupportedFileTypeError extends BackwardCompatibleError {}
|
class UnsupportedFileTypeError extends BackwardCompatibleError {}
|
||||||
|
@ -270,6 +272,7 @@ module.exports = {
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
ServiceNotConfiguredError,
|
ServiceNotConfiguredError,
|
||||||
TooManyRequestsError,
|
TooManyRequestsError,
|
||||||
|
DuplicateNameError,
|
||||||
InvalidNameError,
|
InvalidNameError,
|
||||||
UnsupportedFileTypeError,
|
UnsupportedFileTypeError,
|
||||||
FileTooLargeError,
|
FileTooLargeError,
|
||||||
|
|
|
@ -222,8 +222,8 @@ const RestoreManager = {
|
||||||
try {
|
try {
|
||||||
return await addEntityWithName(basename)
|
return await addEntityWithName(basename)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Errors.InvalidNameError) {
|
if (error instanceof Errors.DuplicateNameError) {
|
||||||
// likely a duplicate name, so try with a prefix
|
// Duplicate name, so try with a prefix
|
||||||
const date = moment(new Date()).format('Do MMM YY H:mm:ss')
|
const date = moment(new Date()).format('Do MMM YY H:mm:ss')
|
||||||
// Move extension to the end so the file type is preserved
|
// Move extension to the end so the file type is preserved
|
||||||
const extension = Path.extname(basename)
|
const extension = Path.extname(basename)
|
||||||
|
|
|
@ -618,7 +618,7 @@ function _checkValidElementName(folder, name) {
|
||||||
.concat(folder.folders || [])
|
.concat(folder.folders || [])
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
if (element.name === name) {
|
if (element.name === name) {
|
||||||
throw new Errors.InvalidNameError('file already exists')
|
throw new Errors.DuplicateNameError('file already exists')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,10 @@ async function updateProjectContents(req, res, next) {
|
||||||
try {
|
try {
|
||||||
await UpdateMerger.promises.mergeUpdate(null, projectId, path, req, source)
|
await UpdateMerger.promises.mergeUpdate(null, projectId, path, req, source)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.constructor === Errors.InvalidNameError) {
|
if (
|
||||||
|
error instanceof Errors.InvalidNameError ||
|
||||||
|
error instanceof Errors.DuplicateNameError
|
||||||
|
) {
|
||||||
return res.sendStatus(422)
|
return res.sendStatus(422)
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
|
|
|
@ -12,6 +12,7 @@ const { InvalidZipFileError } = require('./ArchiveErrors')
|
||||||
const multer = require('multer')
|
const multer = require('multer')
|
||||||
const { defaultsDeep } = require('lodash')
|
const { defaultsDeep } = require('lodash')
|
||||||
const { expressify } = require('@overleaf/promise-utils')
|
const { expressify } = require('@overleaf/promise-utils')
|
||||||
|
const { DuplicateNameError } = require('../Errors/Errors')
|
||||||
|
|
||||||
const upload = multer(
|
const upload = multer(
|
||||||
defaultsDeep(
|
defaultsDeep(
|
||||||
|
@ -107,6 +108,11 @@ async function uploadFile(req, res, next) {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'invalid_filename',
|
error: 'invalid_filename',
|
||||||
})
|
})
|
||||||
|
} else if (error instanceof DuplicateNameError) {
|
||||||
|
return res.status(422).json({
|
||||||
|
success: false,
|
||||||
|
error: 'duplicate_file_name',
|
||||||
|
})
|
||||||
} else if (error.message === 'project_has_too_many_files') {
|
} else if (error.message === 'project_has_too_many_files') {
|
||||||
return res.status(422).json({
|
return res.status(422).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
|
@ -428,6 +428,7 @@
|
||||||
"file_name_figure_modal": "",
|
"file_name_figure_modal": "",
|
||||||
"file_name_in_this_project": "",
|
"file_name_in_this_project": "",
|
||||||
"file_name_in_this_project_figure_modal": "",
|
"file_name_in_this_project_figure_modal": "",
|
||||||
|
"file_or_folder_name_already_exists": "",
|
||||||
"file_outline": "",
|
"file_outline": "",
|
||||||
"file_size": "",
|
"file_size": "",
|
||||||
"files_cannot_include_invalid_characters": "",
|
"files_cannot_include_invalid_characters": "",
|
||||||
|
@ -1385,6 +1386,7 @@
|
||||||
"thanks_for_subscribing_you_help_sl": "",
|
"thanks_for_subscribing_you_help_sl": "",
|
||||||
"thanks_settings_updated": "",
|
"thanks_settings_updated": "",
|
||||||
"the_following_files_already_exist_in_this_project": "",
|
"the_following_files_already_exist_in_this_project": "",
|
||||||
|
"the_following_files_and_folders_already_exist_in_this_project": "",
|
||||||
"the_following_folder_already_exists_in_this_project": "",
|
"the_following_folder_already_exists_in_this_project": "",
|
||||||
"the_following_folder_already_exists_in_this_project_plural": "",
|
"the_following_folder_already_exists_in_this_project_plural": "",
|
||||||
"the_original_text_has_changed": "",
|
"the_original_text_has_changed": "",
|
||||||
|
|
|
@ -42,6 +42,13 @@ export default function ErrorMessage({ error }) {
|
||||||
</DangerMessage>
|
</DangerMessage>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case 'duplicate_file_name':
|
||||||
|
return (
|
||||||
|
<DangerMessage>
|
||||||
|
{t('file_or_folder_name_already_exists')}
|
||||||
|
</DangerMessage>
|
||||||
|
)
|
||||||
|
|
||||||
case 'rate-limit-hit':
|
case 'rate-limit-hit':
|
||||||
return (
|
return (
|
||||||
<DangerMessage>
|
<DangerMessage>
|
||||||
|
|
|
@ -4,90 +4,110 @@ import { useProjectContext } from '@/shared/context/project-context'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { syncDelete } from '@/features/file-tree/util/sync-mutation'
|
import { syncDelete } from '@/features/file-tree/util/sync-mutation'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
|
import { TFunction } from 'i18next'
|
||||||
|
|
||||||
export function UploadConflicts({
|
export type Conflict = {
|
||||||
|
entity: FileTreeEntity
|
||||||
|
type: 'file' | 'folder'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getConflictText = (conflicts: Conflict[], t: TFunction) => {
|
||||||
|
const hasFolderConflict = conflicts.some(
|
||||||
|
conflict => conflict.type === 'folder'
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasFileConflict = conflicts.some(conflict => conflict.type === 'file')
|
||||||
|
|
||||||
|
if (hasFolderConflict && hasFileConflict) {
|
||||||
|
return t('the_following_files_and_folders_already_exist_in_this_project')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFolderConflict) {
|
||||||
|
return t('the_following_folder_already_exists_in_this_project', {
|
||||||
|
count: conflicts.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('the_following_files_already_exist_in_this_project')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileUploadConflicts({
|
||||||
cancel,
|
cancel,
|
||||||
conflicts,
|
conflicts,
|
||||||
folderConflicts,
|
|
||||||
handleOverwrite,
|
handleOverwrite,
|
||||||
setError,
|
|
||||||
}: {
|
}: {
|
||||||
cancel: () => void
|
cancel: () => void
|
||||||
conflicts: FileTreeEntity[]
|
conflicts: Conflict[]
|
||||||
folderConflicts: FileTreeEntity[]
|
|
||||||
handleOverwrite: () => void
|
handleOverwrite: () => void
|
||||||
setError: (error: string) => void
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
// ensure that no uploads happen while there are folder conflicts
|
// Don't allow overwriting folders with files
|
||||||
if (folderConflicts.length > 0) {
|
const hasFolderConflict = conflicts.some(
|
||||||
return (
|
conflict => conflict.type === 'folder'
|
||||||
<FolderUploadConflicts
|
)
|
||||||
cancel={cancel}
|
|
||||||
folderConflicts={folderConflicts}
|
|
||||||
handleOverwrite={handleOverwrite}
|
|
||||||
setError={setError}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="small modal-new-file--body-conflict">
|
<div className="small modal-new-file--body-conflict">
|
||||||
{conflicts.length > 0 && (
|
{conflicts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<p className="text-center mb-0">
|
<p className="text-center mb-0">{getConflictText(conflicts, t)}</p>
|
||||||
{t('the_following_files_already_exist_in_this_project')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul className="text-center list-unstyled row-spaced-small mt-1">
|
<ul className="text-center list-unstyled row-spaced-small mt-1">
|
||||||
{conflicts.map((conflict, index) => (
|
{conflicts.map((conflict, index) => (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
<strong>{conflict.name}</strong>
|
<strong>{conflict.entity.name}</strong>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-center row-spaced-small">
|
{!hasFolderConflict && (
|
||||||
{t('do_you_want_to_overwrite_them')}
|
<p className="text-center row-spaced-small">
|
||||||
</p>
|
{t('do_you_want_to_overwrite_them')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
|
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
|
||||||
{t('cancel')}
|
{t('cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button bsStyle="danger" onClick={handleOverwrite}>
|
{!hasFolderConflict && (
|
||||||
{t('overwrite')}
|
<Button bsStyle="danger" onClick={handleOverwrite}>
|
||||||
</Button>
|
{t('overwrite')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FolderUploadConflicts({
|
export function FolderUploadConflicts({
|
||||||
cancel,
|
cancel,
|
||||||
handleOverwrite,
|
handleOverwrite,
|
||||||
folderConflicts,
|
conflicts,
|
||||||
setError,
|
setError,
|
||||||
}: {
|
}: {
|
||||||
cancel: () => void
|
cancel: () => void
|
||||||
handleOverwrite: () => void
|
handleOverwrite: () => void
|
||||||
folderConflicts: FileTreeEntity[]
|
conflicts: Conflict[]
|
||||||
setError: (error: string) => void
|
setError: (error: string) => void
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { _id: projectId } = useProjectContext()
|
const { _id: projectId } = useProjectContext()
|
||||||
|
|
||||||
|
// Don't allow overwriting files with a folder
|
||||||
|
const hasFileConflict = conflicts.some(conflict => conflict.type === 'file')
|
||||||
|
|
||||||
const deleteAndRetry = useCallback(async () => {
|
const deleteAndRetry = useCallback(async () => {
|
||||||
// TODO: confirm deletion?
|
// TODO: confirm deletion?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
folderConflicts.map(
|
conflicts.map(conflict =>
|
||||||
entity => syncDelete(projectId, 'folder', entity._id) // TODO: might be a file!
|
syncDelete(projectId, 'folder', conflict.entity._id)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,40 +115,40 @@ function FolderUploadConflicts({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message)
|
setError(error.message)
|
||||||
}
|
}
|
||||||
}, [setError, folderConflicts, handleOverwrite, projectId])
|
}, [setError, conflicts, handleOverwrite, projectId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="small modal-new-file--body-conflict">
|
<div className="small modal-new-file--body-conflict">
|
||||||
<p className="text-center mb-0">
|
<p className="text-center mb-0">{getConflictText(conflicts, t)}</p>
|
||||||
{t('the_following_folder_already_exists_in_this_project', {
|
|
||||||
count: folderConflicts.length,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul className="text-center list-unstyled row-spaced-small mt-1">
|
<ul className="text-center list-unstyled row-spaced-small mt-1">
|
||||||
{folderConflicts.map((entity, index) => (
|
{conflicts.map((conflict, index) => (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
<strong>{entity.name}</strong>
|
<strong>{conflict.entity.name}</strong>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p className="text-center row-spaced-small">
|
{!hasFileConflict && (
|
||||||
{t('overwriting_the_original_folder')}
|
<p className="text-center row-spaced-small">
|
||||||
<br />
|
{t('overwriting_the_original_folder')}
|
||||||
{t('do_you_want_to_overwrite_it', {
|
<br />
|
||||||
count: folderConflicts.length,
|
{t('do_you_want_to_overwrite_it', {
|
||||||
})}
|
count: conflicts.length,
|
||||||
</p>
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
|
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
|
||||||
{t('cancel')}
|
{t('cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button bsStyle="danger" onClick={deleteAndRetry}>
|
{!hasFileConflict && (
|
||||||
{t('overwrite')}
|
<Button bsStyle="danger" onClick={deleteAndRetry}>
|
||||||
</Button>
|
{t('overwrite')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,10 +12,16 @@ import { refreshProjectMetadata } from '../../../util/api'
|
||||||
import ErrorMessage from '../error-message'
|
import ErrorMessage from '../error-message'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
|
import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
|
||||||
import { findByNameInFolder } from '@/features/file-tree/util/is-name-unique-in-folder'
|
import {
|
||||||
|
findFileByNameInFolder,
|
||||||
|
findFolderByNameInFolder,
|
||||||
|
} from '@/features/file-tree/util/is-name-unique-in-folder'
|
||||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||||
import { FileTreeEntity } from '../../../../../../../types/file-tree-entity'
|
import {
|
||||||
import { UploadConflicts } from '@/features/file-tree/components/file-tree-create/file-tree-upload-conflicts'
|
Conflict,
|
||||||
|
FileUploadConflicts,
|
||||||
|
FolderUploadConflicts,
|
||||||
|
} from '@/features/file-tree/components/file-tree-create/file-tree-upload-conflicts'
|
||||||
import getMeta from '@/utils/meta'
|
import getMeta from '@/utils/meta'
|
||||||
|
|
||||||
export default function FileTreeUploadDoc() {
|
export default function FileTreeUploadDoc() {
|
||||||
|
@ -26,8 +32,8 @@ export default function FileTreeUploadDoc() {
|
||||||
|
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
const [conflicts, setConflicts] = useState<FileTreeEntity[]>([])
|
const [conflicts, setConflicts] = useState<Conflict[]>([])
|
||||||
const [folderConflicts, setFolderConflicts] = useState<FileTreeEntity[]>([])
|
const [folderConflicts, setFolderConflicts] = useState<Conflict[]>([])
|
||||||
const [overwrite, setOverwrite] = useState(false)
|
const [overwrite, setOverwrite] = useState(false)
|
||||||
|
|
||||||
const maxNumberOfFiles = 180
|
const maxNumberOfFiles = 180
|
||||||
|
@ -35,46 +41,79 @@ export default function FileTreeUploadDoc() {
|
||||||
|
|
||||||
// calculate conflicts
|
// calculate conflicts
|
||||||
const buildConflicts = (files: Record<string, any>) => {
|
const buildConflicts = (files: Record<string, any>) => {
|
||||||
const conflicts = new Set<FileTreeEntity>()
|
const conflicts: Conflict[] = []
|
||||||
|
|
||||||
for (const file of Object.values(files)) {
|
for (const file of Object.values(files)) {
|
||||||
const { name, relativePath } = file.meta
|
const { name, relativePath } = file.meta
|
||||||
|
|
||||||
if (!relativePath) {
|
if (!relativePath) {
|
||||||
const targetFolderId = file.meta.targetFolderId ?? parentFolderId
|
const targetFolderId = file.meta.targetFolderId ?? parentFolderId
|
||||||
const duplicate = findByNameInFolder(fileTreeData, targetFolderId, name)
|
const duplicateFile = findFileByNameInFolder(
|
||||||
if (duplicate) {
|
fileTreeData,
|
||||||
conflicts.add(duplicate)
|
targetFolderId,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
if (duplicateFile) {
|
||||||
|
conflicts.push({
|
||||||
|
entity: duplicateFile,
|
||||||
|
type: 'file',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateFolder = findFolderByNameInFolder(
|
||||||
|
fileTreeData,
|
||||||
|
targetFolderId,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
if (duplicateFolder) {
|
||||||
|
conflicts.push({
|
||||||
|
entity: duplicateFolder,
|
||||||
|
type: 'folder',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...conflicts]
|
return conflicts
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildFolderConflicts = (files: Record<string, any>) => {
|
const buildFolderConflicts = (files: Record<string, any>) => {
|
||||||
const conflicts = new Set<FileTreeEntity>()
|
const conflicts: Conflict[] = []
|
||||||
|
|
||||||
for (const file of Object.values(files)) {
|
for (const file of Object.values(files)) {
|
||||||
const { relativePath } = file.meta
|
const { relativePath } = file.meta
|
||||||
|
|
||||||
if (relativePath) {
|
if (relativePath) {
|
||||||
const [rootName] = relativePath.replace(/^\//, '').split('/')
|
const [rootName] = relativePath.replace(/^\//, '').split('/')
|
||||||
if (!conflicts.has(rootName)) {
|
|
||||||
const targetFolderId = file.meta.targetFolderId ?? parentFolderId
|
const targetFolderId = file.meta.targetFolderId ?? parentFolderId
|
||||||
const duplicateEntity = findByNameInFolder(
|
const duplicateFile = findFileByNameInFolder(
|
||||||
fileTreeData,
|
fileTreeData,
|
||||||
targetFolderId,
|
targetFolderId,
|
||||||
rootName
|
rootName
|
||||||
)
|
)
|
||||||
if (duplicateEntity) {
|
if (duplicateFile) {
|
||||||
conflicts.add(duplicateEntity)
|
conflicts.push({
|
||||||
}
|
entity: duplicateFile,
|
||||||
|
type: 'file',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateFolder = findFolderByNameInFolder(
|
||||||
|
fileTreeData,
|
||||||
|
targetFolderId,
|
||||||
|
rootName
|
||||||
|
)
|
||||||
|
if (duplicateFolder) {
|
||||||
|
conflicts.push({
|
||||||
|
entity: duplicateFolder,
|
||||||
|
type: 'folder',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...conflicts]
|
return conflicts
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildEndpoint = (projectId: string, targetFolderId: string) => {
|
const buildEndpoint = (projectId: string, targetFolderId: string) => {
|
||||||
|
@ -214,25 +253,32 @@ export default function FileTreeUploadDoc() {
|
||||||
uppy.upload()
|
uppy.upload()
|
||||||
}, [uppy])
|
}, [uppy])
|
||||||
|
|
||||||
// whether to show a message about conflicting files
|
const showFolderUploadConflicts = !overwrite && folderConflicts.length > 0
|
||||||
const showConflicts =
|
const showFileUploadConfilcts =
|
||||||
!overwrite && (conflicts.length > 0 || folderConflicts.length > 0)
|
!overwrite && !showFolderUploadConflicts && conflicts.length > 0
|
||||||
|
const showDashboard = !showFileUploadConfilcts && !showFolderUploadConflicts
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && (
|
{error && (
|
||||||
<UploadErrorMessage error={error} maxNumberOfFiles={maxNumberOfFiles} />
|
<UploadErrorMessage error={error} maxNumberOfFiles={maxNumberOfFiles} />
|
||||||
)}
|
)}
|
||||||
|
{showFolderUploadConflicts && (
|
||||||
{showConflicts ? (
|
<FolderUploadConflicts
|
||||||
<UploadConflicts
|
|
||||||
cancel={cancel}
|
cancel={cancel}
|
||||||
conflicts={conflicts}
|
conflicts={folderConflicts}
|
||||||
folderConflicts={folderConflicts}
|
|
||||||
handleOverwrite={handleOverwrite}
|
handleOverwrite={handleOverwrite}
|
||||||
setError={setError}
|
setError={setError}
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
|
{showFileUploadConfilcts && (
|
||||||
|
<FileUploadConflicts
|
||||||
|
cancel={cancel}
|
||||||
|
conflicts={conflicts}
|
||||||
|
handleOverwrite={handleOverwrite}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showDashboard && (
|
||||||
<Dashboard
|
<Dashboard
|
||||||
uppy={uppy}
|
uppy={uppy}
|
||||||
showProgressDetails
|
showProgressDetails
|
||||||
|
|
|
@ -8,21 +8,35 @@ export function isNameUniqueInFolder(
|
||||||
parentFolderId: string,
|
parentFolderId: string,
|
||||||
name: string
|
name: string
|
||||||
): boolean {
|
): boolean {
|
||||||
return !findByNameInFolder(tree, parentFolderId, name)
|
return !(
|
||||||
|
findFileByNameInFolder(tree, parentFolderId, name) ||
|
||||||
|
findFolderByNameInFolder(tree, parentFolderId, name)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findByNameInFolder(
|
export function findFileByNameInFolder(
|
||||||
tree: Folder,
|
tree: Folder,
|
||||||
parentFolderId: string,
|
parentFolderId: string,
|
||||||
name: string
|
name: string
|
||||||
): Doc | FileRef | Folder | undefined {
|
): Doc | FileRef | undefined {
|
||||||
if (tree._id !== parentFolderId) {
|
if (tree._id !== parentFolderId) {
|
||||||
tree = findInTree(tree, parentFolderId).entity
|
tree = findInTree(tree, parentFolderId).entity
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
tree.docs.find(entity => entity.name === name) ||
|
tree.docs.find(entity => entity.name === name) ||
|
||||||
tree.fileRefs.find(entity => entity.name === name) ||
|
tree.fileRefs.find(entity => entity.name === name)
|
||||||
tree.folders.find(entity => entity.name === name)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findFolderByNameInFolder(
|
||||||
|
tree: Folder,
|
||||||
|
parentFolderId: string,
|
||||||
|
name: string
|
||||||
|
): Folder | undefined {
|
||||||
|
if (tree._id !== parentFolderId) {
|
||||||
|
tree = findInTree(tree, parentFolderId).entity
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree.folders.find(entity => entity.name === name)
|
||||||
|
}
|
||||||
|
|
|
@ -634,6 +634,7 @@
|
||||||
"file_name_figure_modal": "File name",
|
"file_name_figure_modal": "File name",
|
||||||
"file_name_in_this_project": "File Name In This Project",
|
"file_name_in_this_project": "File Name In This Project",
|
||||||
"file_name_in_this_project_figure_modal": "File name in this project",
|
"file_name_in_this_project_figure_modal": "File name in this project",
|
||||||
|
"file_or_folder_name_already_exists": "A file or folder with this name already exists",
|
||||||
"file_outline": "File outline",
|
"file_outline": "File outline",
|
||||||
"file_size": "File size",
|
"file_size": "File size",
|
||||||
"file_too_large": "File too large",
|
"file_too_large": "File too large",
|
||||||
|
@ -1990,6 +1991,7 @@
|
||||||
"thanks_settings_updated": "Thanks, your settings have been updated.",
|
"thanks_settings_updated": "Thanks, your settings have been updated.",
|
||||||
"the_file_supplied_is_of_an_unsupported_type ": "The link to open this content on Overleaf pointed to the wrong kind of file. Valid file types are .tex documents and .zip files. If this keeps happening for links on a particular site, please report this to them.",
|
"the_file_supplied_is_of_an_unsupported_type ": "The link to open this content on Overleaf pointed to the wrong kind of file. Valid file types are .tex documents and .zip files. If this keeps happening for links on a particular site, please report this to them.",
|
||||||
"the_following_files_already_exist_in_this_project": "The following files already exist in this project:",
|
"the_following_files_already_exist_in_this_project": "The following files already exist in this project:",
|
||||||
|
"the_following_files_and_folders_already_exist_in_this_project": "The following files and folders already exist in this project:",
|
||||||
"the_following_folder_already_exists_in_this_project": "The following folder already exists in this project:",
|
"the_following_folder_already_exists_in_this_project": "The following folder already exists in this project:",
|
||||||
"the_following_folder_already_exists_in_this_project_plural": "The following folders already exist in this project:",
|
"the_following_folder_already_exists_in_this_project_plural": "The following folders already exist in this project:",
|
||||||
"the_original_text_has_changed": "The original text has changed, so this suggestion can’t be applied",
|
"the_original_text_has_changed": "The original text has changed, so this suggestion can’t be applied",
|
||||||
|
|
|
@ -180,9 +180,9 @@ describe('RestoreManager', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with an invalid name', function () {
|
describe('with a duplicate name', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
this.addEntityWithName.rejects(new Errors.InvalidNameError())
|
this.addEntityWithName.rejects(new Errors.DuplicateNameError())
|
||||||
this.addEntityWithName
|
this.addEntityWithName
|
||||||
.onSecondCall()
|
.onSecondCall()
|
||||||
.resolves((this.entity = 'mock-entity'))
|
.resolves((this.entity = 'mock-entity'))
|
||||||
|
|
|
@ -856,7 +856,7 @@ describe('ProjectEntityMongoUpdateHandler', function () {
|
||||||
'doc',
|
'doc',
|
||||||
this.folder.name
|
this.folder.name
|
||||||
)
|
)
|
||||||
).to.be.rejectedWith(Errors.InvalidNameError)
|
).to.be.rejectedWith(Errors.DuplicateNameError)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -990,7 +990,7 @@ describe('ProjectEntityMongoUpdateHandler', function () {
|
||||||
file,
|
file,
|
||||||
'file'
|
'file'
|
||||||
)
|
)
|
||||||
).to.be.rejectedWith(Errors.InvalidNameError)
|
).to.be.rejectedWith(Errors.DuplicateNameError)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue