first commit
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import FileTreeCreateNameInput from '../file-tree-create-name-input'
|
||||
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
|
||||
import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
|
||||
import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form'
|
||||
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
||||
import ErrorMessage from '../error-message'
|
||||
|
||||
export default function FileTreeCreateNewDoc() {
|
||||
const { name, validName } = useFileTreeCreateName()
|
||||
const { setValid } = useFileTreeCreateForm()
|
||||
const { error, finishCreatingDoc, inFlight } = useFileTreeActionable()
|
||||
// form validation: name is valid
|
||||
useEffect(() => {
|
||||
setValid(validName)
|
||||
}, [setValid, validName])
|
||||
|
||||
// form submission: create an empty doc with this name
|
||||
const handleSubmit = useCallback(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
|
||||
finishCreatingDoc({ name })
|
||||
eventTracking.sendMB('new-file-created', {
|
||||
method: 'doc',
|
||||
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
|
||||
})
|
||||
},
|
||||
[finishCreatingDoc, name]
|
||||
)
|
||||
|
||||
return (
|
||||
<form noValidate id="create-file" onSubmit={handleSubmit}>
|
||||
<FileTreeCreateNameInput focusName error={error} inFlight={inFlight} />
|
||||
{error && <ErrorMessage error={error} />}
|
||||
</form>
|
||||
)
|
||||
}
|
@@ -0,0 +1,373 @@
|
||||
import {
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
FormEventHandler,
|
||||
} from 'react'
|
||||
import FileTreeCreateNameInput from '../file-tree-create-name-input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUserProjects } from '../../../hooks/use-user-projects'
|
||||
import { Entity, useProjectEntities } from '../../../hooks/use-project-entities'
|
||||
import { useProjectOutputFiles } from '../../../hooks/use-project-output-files'
|
||||
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
|
||||
import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
|
||||
import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form'
|
||||
import { useProjectContext } from '../../../../../shared/context/project-context'
|
||||
import ErrorMessage from '../error-message'
|
||||
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
||||
import { File } from '@/features/source-editor/utils/file'
|
||||
import { Project } from '../../../../../../../types/project'
|
||||
import getMeta from '@/utils/meta'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
import OLForm from '@/features/ui/components/ol/ol-form'
|
||||
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
|
||||
import { Spinner } from 'react-bootstrap-5'
|
||||
|
||||
export default function FileTreeImportFromProject() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { hasLinkedProjectFileFeature, hasLinkedProjectOutputFileFeature } =
|
||||
getMeta('ol-ExposedSettings')
|
||||
const canSwitchOutputFilesMode =
|
||||
hasLinkedProjectFileFeature && hasLinkedProjectOutputFileFeature
|
||||
|
||||
const { name, setName, validName } = useFileTreeCreateName()
|
||||
const { setValid } = useFileTreeCreateForm()
|
||||
const { error, finishCreatingLinkedFile, inFlight } = useFileTreeActionable()
|
||||
|
||||
const [selectedProject, setSelectedProject] = useState<Project>()
|
||||
const [selectedProjectEntity, setSelectedProjectEntity] = useState<Entity>()
|
||||
const [selectedProjectOutputFile, setSelectedProjectOutputFile] = useState<
|
||||
File & { build: string; clsiServerId: string }
|
||||
>()
|
||||
const [isOutputFilesMode, setOutputFilesMode] = useState(
|
||||
// default to project file mode, unless the feature is not enabled
|
||||
!hasLinkedProjectFileFeature
|
||||
)
|
||||
|
||||
// use the basename of a path as the file name
|
||||
const setNameFromPath = useCallback(
|
||||
path => {
|
||||
const filename = path.split('/').pop()
|
||||
|
||||
if (filename) {
|
||||
setName(filename)
|
||||
}
|
||||
},
|
||||
[setName]
|
||||
)
|
||||
|
||||
// update the name when an output file is selected
|
||||
useEffect(() => {
|
||||
if (selectedProjectOutputFile) {
|
||||
if (
|
||||
selectedProjectOutputFile.path === 'output.pdf' &&
|
||||
selectedProject!.name
|
||||
) {
|
||||
// if the output PDF is selected, use the project's name as the filename
|
||||
setName(`${selectedProject!.name}.pdf`)
|
||||
} else {
|
||||
setNameFromPath(selectedProjectOutputFile.path)
|
||||
}
|
||||
}
|
||||
}, [selectedProject, selectedProjectOutputFile, setName, setNameFromPath])
|
||||
|
||||
// update the name when an entity is selected
|
||||
useEffect(() => {
|
||||
if (selectedProjectEntity) {
|
||||
setNameFromPath(selectedProjectEntity.path)
|
||||
}
|
||||
}, [selectedProjectEntity, setNameFromPath])
|
||||
|
||||
// form validation: name is valid and entity or output file is selected
|
||||
useEffect(() => {
|
||||
const hasSelectedEntity = Boolean(
|
||||
isOutputFilesMode ? selectedProjectOutputFile : selectedProjectEntity
|
||||
)
|
||||
|
||||
setValid(validName && hasSelectedEntity)
|
||||
}, [
|
||||
setValid,
|
||||
validName,
|
||||
isOutputFilesMode,
|
||||
selectedProjectEntity,
|
||||
selectedProjectOutputFile,
|
||||
])
|
||||
|
||||
// form submission: create a linked file with this name, from this entity or output file
|
||||
const handleSubmit: FormEventHandler = event => {
|
||||
event.preventDefault()
|
||||
eventTracking.sendMB('new-file-created', {
|
||||
method: 'project',
|
||||
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
|
||||
})
|
||||
|
||||
if (isOutputFilesMode) {
|
||||
finishCreatingLinkedFile({
|
||||
name,
|
||||
provider: 'project_output_file',
|
||||
data: {
|
||||
source_project_id: selectedProject!._id,
|
||||
source_output_file_path: selectedProjectOutputFile!.path,
|
||||
build_id: selectedProjectOutputFile!.build,
|
||||
clsiServerId: selectedProjectOutputFile!.clsiServerId,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
finishCreatingLinkedFile({
|
||||
name,
|
||||
provider: 'project_file',
|
||||
data: {
|
||||
source_project_id: selectedProject!._id,
|
||||
source_entity_path: selectedProjectEntity!.path,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<OLForm id="create-file" onSubmit={handleSubmit}>
|
||||
<SelectProject
|
||||
selectedProject={selectedProject}
|
||||
setSelectedProject={setSelectedProject}
|
||||
/>
|
||||
|
||||
{isOutputFilesMode ? (
|
||||
<SelectProjectOutputFile
|
||||
selectedProjectId={selectedProject?._id}
|
||||
selectedProjectOutputFile={selectedProjectOutputFile}
|
||||
setSelectedProjectOutputFile={setSelectedProjectOutputFile}
|
||||
/>
|
||||
) : (
|
||||
<SelectProjectEntity
|
||||
selectedProjectId={selectedProject?._id}
|
||||
selectedProjectEntity={selectedProjectEntity}
|
||||
setSelectedProjectEntity={setSelectedProjectEntity}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canSwitchOutputFilesMode && (
|
||||
<div className="toggle-file-type-button">
|
||||
or
|
||||
<OLButton
|
||||
variant="link"
|
||||
type="button"
|
||||
onClick={() => setOutputFilesMode(value => !value)}
|
||||
>
|
||||
<span>
|
||||
{isOutputFilesMode
|
||||
? t('select_from_source_files')
|
||||
: t('select_from_output_files')}
|
||||
</span>
|
||||
</OLButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FileTreeCreateNameInput
|
||||
label={t('file_name_in_this_project')}
|
||||
classes={{
|
||||
formGroup: 'form-controls row-spaced-small',
|
||||
}}
|
||||
placeholder="example.tex"
|
||||
error={error}
|
||||
inFlight={inFlight}
|
||||
/>
|
||||
|
||||
{error && <ErrorMessage error={error} />}
|
||||
</OLForm>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectProjectProps = {
|
||||
selectedProject?: any
|
||||
setSelectedProject(project: any): void
|
||||
}
|
||||
|
||||
function SelectProject({
|
||||
selectedProject,
|
||||
setSelectedProject,
|
||||
}: SelectProjectProps) {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const { data, error, loading } = useUserProjects()
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return data.filter(item => item._id !== projectId)
|
||||
}, [data, projectId])
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />
|
||||
}
|
||||
|
||||
return (
|
||||
<OLFormGroup controlId="project-select">
|
||||
<OLFormLabel>{t('select_a_project')}</OLFormLabel>
|
||||
|
||||
{loading && (
|
||||
<span>
|
||||
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
size="sm"
|
||||
role="status"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<OLFormSelect
|
||||
disabled={!data}
|
||||
value={selectedProject ? selectedProject._id : ''}
|
||||
onChange={event => {
|
||||
const projectId = (event.target as HTMLSelectElement).value
|
||||
const project = data!.find(item => item._id === projectId)
|
||||
setSelectedProject(project)
|
||||
}}
|
||||
>
|
||||
<option disabled value="">
|
||||
- {t('please_select_a_project')}
|
||||
</option>
|
||||
|
||||
{filteredData &&
|
||||
filteredData.map(project => (
|
||||
<option key={project._id} value={project._id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))}
|
||||
</OLFormSelect>
|
||||
|
||||
{filteredData && !filteredData.length && (
|
||||
<small>{t('no_other_projects_found')}</small>
|
||||
)}
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectProjectOutputFileProps = {
|
||||
selectedProjectId?: string
|
||||
selectedProjectOutputFile?: any
|
||||
setSelectedProjectOutputFile(file: any): void
|
||||
}
|
||||
|
||||
function SelectProjectOutputFile({
|
||||
selectedProjectId,
|
||||
selectedProjectOutputFile,
|
||||
setSelectedProjectOutputFile,
|
||||
}: SelectProjectOutputFileProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data, error, loading } = useProjectOutputFiles(selectedProjectId)
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />
|
||||
}
|
||||
|
||||
return (
|
||||
<OLFormGroup
|
||||
className="row-spaced-small"
|
||||
controlId="project-output-file-select"
|
||||
>
|
||||
<OLFormLabel>{t('select_an_output_file')}</OLFormLabel>
|
||||
|
||||
{loading && (
|
||||
<span>
|
||||
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
size="sm"
|
||||
role="status"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<OLFormSelect
|
||||
disabled={!data}
|
||||
value={selectedProjectOutputFile?.path || ''}
|
||||
onChange={event => {
|
||||
const path = (event.target as unknown as HTMLSelectElement).value
|
||||
const file = data?.find(item => item.path === path)
|
||||
setSelectedProjectOutputFile(file)
|
||||
}}
|
||||
>
|
||||
<option disabled value="">
|
||||
- {t('please_select_an_output_file')}
|
||||
</option>
|
||||
|
||||
{data &&
|
||||
data.map(file => (
|
||||
<option key={file.path} value={file.path}>
|
||||
{file.path}
|
||||
</option>
|
||||
))}
|
||||
</OLFormSelect>
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectProjectEntityProps = {
|
||||
selectedProjectId?: string
|
||||
selectedProjectEntity?: any
|
||||
setSelectedProjectEntity(entity: any): void
|
||||
}
|
||||
|
||||
function SelectProjectEntity({
|
||||
selectedProjectId,
|
||||
selectedProjectEntity,
|
||||
setSelectedProjectEntity,
|
||||
}: SelectProjectEntityProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data, error, loading } = useProjectEntities(selectedProjectId)
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />
|
||||
}
|
||||
|
||||
return (
|
||||
<OLFormGroup className="row-spaced-small" controlId="project-entity-select">
|
||||
<OLFormLabel>{t('select_a_file')}</OLFormLabel>
|
||||
|
||||
{loading && (
|
||||
<span>
|
||||
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
size="sm"
|
||||
role="status"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<OLFormSelect
|
||||
disabled={!data}
|
||||
value={selectedProjectEntity?.path || ''}
|
||||
onChange={event => {
|
||||
const path = (event.target as HTMLSelectElement).value
|
||||
const entity = data!.find(item => item.path === path)
|
||||
setSelectedProjectEntity(entity)
|
||||
}}
|
||||
>
|
||||
<option disabled value="">
|
||||
- {t('please_select_a_file')}
|
||||
</option>
|
||||
|
||||
{data &&
|
||||
data.map(entity => (
|
||||
<option key={entity.path} value={entity.path}>
|
||||
{entity.path.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</OLFormSelect>
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileTreeCreateNameInput from '../file-tree-create-name-input'
|
||||
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
|
||||
import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
|
||||
import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form'
|
||||
import ErrorMessage from '../error-message'
|
||||
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
|
||||
export default function FileTreeImportFromUrl() {
|
||||
const { t } = useTranslation()
|
||||
const { name, setName, validName } = useFileTreeCreateName()
|
||||
const { setValid } = useFileTreeCreateForm()
|
||||
const { finishCreatingLinkedFile, error, inFlight } = useFileTreeActionable()
|
||||
|
||||
const [url, setUrl] = useState('')
|
||||
|
||||
const handleChange = useCallback(event => {
|
||||
setUrl(event.target.value)
|
||||
}, [])
|
||||
|
||||
// set the name when the URL changes
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
const matches = url.match(/^\s*https?:\/\/.+\/([^/]+\.(\w+))\s*$/)
|
||||
setName(matches ? matches[1] : '')
|
||||
}
|
||||
}, [setName, url])
|
||||
|
||||
// form validation: URL is set and name is valid
|
||||
useEffect(() => {
|
||||
setValid(validName && !!url)
|
||||
}, [setValid, validName, url])
|
||||
|
||||
// form submission: create a linked file with this name, from this URL
|
||||
const handleSubmit = event => {
|
||||
event.preventDefault()
|
||||
eventTracking.sendMB('new-file-created', {
|
||||
method: 'url',
|
||||
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
|
||||
})
|
||||
finishCreatingLinkedFile({
|
||||
name,
|
||||
provider: 'url',
|
||||
data: { url: url.trim() },
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className="form-controls"
|
||||
id="create-file"
|
||||
noValidate
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<OLFormGroup controlId="import-from-url">
|
||||
<OLFormLabel>{t('url_to_fetch_the_file_from')}</OLFormLabel>
|
||||
<OLFormControl
|
||||
type="url"
|
||||
placeholder="https://example.com/my-file.png"
|
||||
required
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
|
||||
<FileTreeCreateNameInput
|
||||
label={t('file_name_in_this_project')}
|
||||
placeholder="my_file"
|
||||
error={error}
|
||||
inFlight={inFlight}
|
||||
/>
|
||||
|
||||
{error && <ErrorMessage error={error} />}
|
||||
</form>
|
||||
)
|
||||
}
|
@@ -0,0 +1,339 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Uppy from '@uppy/core'
|
||||
import XHRUpload from '@uppy/xhr-upload'
|
||||
import { Dashboard } from '@uppy/react'
|
||||
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
|
||||
import { useProjectContext } from '../../../../../shared/context/project-context'
|
||||
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
||||
import '@uppy/core/dist/style.css'
|
||||
import '@uppy/dashboard/dist/style.css'
|
||||
import { refreshProjectMetadata } from '../../../util/api'
|
||||
import ErrorMessage from '../error-message'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
|
||||
import {
|
||||
findFileByNameInFolder,
|
||||
findFolderByNameInFolder,
|
||||
} from '@/features/file-tree/util/is-name-unique-in-folder'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import {
|
||||
Conflict,
|
||||
FileUploadConflicts,
|
||||
FolderUploadConflicts,
|
||||
} from '@/features/file-tree/components/file-tree-create/file-tree-upload-conflicts'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export default function FileTreeUploadDoc() {
|
||||
const { parentFolderId, cancel, droppedFiles, setDroppedFiles } =
|
||||
useFileTreeActionable()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const [conflicts, setConflicts] = useState<Conflict[]>([])
|
||||
const [folderConflicts, setFolderConflicts] = useState<Conflict[]>([])
|
||||
const [overwrite, setOverwrite] = useState(false)
|
||||
|
||||
const maxNumberOfFiles = 180
|
||||
const maxFileSize = getMeta('ol-ExposedSettings').maxUploadSize
|
||||
|
||||
// calculate conflicts
|
||||
const buildConflicts = (files: Record<string, any>) => {
|
||||
const conflicts: Conflict[] = []
|
||||
|
||||
for (const file of Object.values(files)) {
|
||||
const { name, relativePath } = file.meta
|
||||
|
||||
if (!relativePath) {
|
||||
const targetFolderId = file.meta.targetFolderId ?? parentFolderId
|
||||
const duplicateFile = findFileByNameInFolder(
|
||||
fileTreeData,
|
||||
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
|
||||
}
|
||||
|
||||
const buildFolderConflicts = (files: Record<string, any>) => {
|
||||
const conflicts: Conflict[] = []
|
||||
|
||||
for (const file of Object.values(files)) {
|
||||
const { relativePath } = file.meta
|
||||
|
||||
if (relativePath) {
|
||||
const [rootName] = relativePath.replace(/^\//, '').split('/')
|
||||
|
||||
const targetFolderId = file.meta.targetFolderId ?? parentFolderId
|
||||
const duplicateFile = findFileByNameInFolder(
|
||||
fileTreeData,
|
||||
targetFolderId,
|
||||
rootName
|
||||
)
|
||||
if (duplicateFile) {
|
||||
conflicts.push({
|
||||
entity: duplicateFile,
|
||||
type: 'file',
|
||||
})
|
||||
}
|
||||
|
||||
const duplicateFolder = findFolderByNameInFolder(
|
||||
fileTreeData,
|
||||
targetFolderId,
|
||||
rootName
|
||||
)
|
||||
if (duplicateFolder) {
|
||||
conflicts.push({
|
||||
entity: duplicateFolder,
|
||||
type: 'folder',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts
|
||||
}
|
||||
|
||||
const buildEndpoint = (projectId: string, targetFolderId: string) => {
|
||||
let endpoint = `/project/${projectId}/upload`
|
||||
|
||||
if (targetFolderId) {
|
||||
endpoint += `?folder_id=${targetFolderId}`
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// initialise the Uppy object
|
||||
const [uppy] = useState(() => {
|
||||
const endpoint = buildEndpoint(projectId, parentFolderId)
|
||||
|
||||
return (
|
||||
new Uppy<{ relativePath?: string; targetFolderId: string }>({
|
||||
// logger: Uppy.debugLogger,
|
||||
allowMultipleUploadBatches: false,
|
||||
restrictions: {
|
||||
maxNumberOfFiles,
|
||||
maxFileSize: maxFileSize || null,
|
||||
},
|
||||
onBeforeFileAdded(file) {
|
||||
if (
|
||||
!isAcceptableFile(
|
||||
file.name,
|
||||
file.meta.relativePath as string | undefined
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
onBeforeUpload(files) {
|
||||
const conflicts = buildConflicts(files)
|
||||
const folderConflicts = buildFolderConflicts(files)
|
||||
setConflicts(conflicts)
|
||||
setFolderConflicts(folderConflicts)
|
||||
return conflicts.length === 0 && folderConflicts.length === 0
|
||||
},
|
||||
autoProceed: true,
|
||||
locale: {
|
||||
strings: {
|
||||
youCanOnlyUploadX:
|
||||
'You can only upload %{smart_count} files at a time',
|
||||
},
|
||||
},
|
||||
})
|
||||
// use the basic XHR uploader
|
||||
.use(XHRUpload, {
|
||||
endpoint,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': getMeta('ol-csrfToken'),
|
||||
},
|
||||
// limit: maxConnections || 1,
|
||||
limit: 1,
|
||||
fieldName: 'qqfile', // "qqfile" field inherited from FineUploader
|
||||
})
|
||||
// close the modal when all the uploads completed successfully
|
||||
.on('complete', result => {
|
||||
if (!result.failed.length) {
|
||||
// $scope.$emit('done', { name: name })
|
||||
cancel()
|
||||
}
|
||||
})
|
||||
// broadcast doc metadata after each successful upload
|
||||
.on('upload-success', (file, response) => {
|
||||
eventTracking.sendMB('new-file-created', {
|
||||
method: 'upload',
|
||||
extension:
|
||||
file?.name && file?.name.split('.').length > 1
|
||||
? file?.name.split('.').pop()
|
||||
: '',
|
||||
})
|
||||
if (response.body.entity_type === 'doc') {
|
||||
window.setTimeout(() => {
|
||||
refreshProjectMetadata(projectId, response.body.entity_id)
|
||||
}, 250)
|
||||
}
|
||||
})
|
||||
// handle upload errors
|
||||
.on('upload-error', (file, error, response) => {
|
||||
switch (response?.status) {
|
||||
case 429:
|
||||
setError('rate-limit-hit')
|
||||
break
|
||||
|
||||
case 403:
|
||||
setError('not-logged-in')
|
||||
break
|
||||
|
||||
default:
|
||||
debugConsole.error(error)
|
||||
setError(response?.body?.error || 'generic_something_went_wrong')
|
||||
break
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (uppy && droppedFiles) {
|
||||
uppy.setOptions({
|
||||
autoProceed: false,
|
||||
})
|
||||
for (const file of droppedFiles.files) {
|
||||
const fileId = uppy.addFile({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
source: 'Local',
|
||||
isRemote: false,
|
||||
meta: {
|
||||
relativePath: (file as any).relativePath,
|
||||
targetFolderId: droppedFiles.targetFolderId,
|
||||
},
|
||||
})
|
||||
const uppyFile = uppy.getFile(fileId)
|
||||
uppy.setFileState(fileId, {
|
||||
xhrUpload: {
|
||||
...(uppyFile as any).xhrUpload,
|
||||
endpoint: buildEndpoint(projectId, droppedFiles.targetFolderId),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
setDroppedFiles(null)
|
||||
}
|
||||
}, [uppy, droppedFiles, setDroppedFiles, projectId])
|
||||
|
||||
// handle forced overwriting of conflicting files
|
||||
const handleOverwrite = useCallback(() => {
|
||||
setOverwrite(true)
|
||||
uppy.setOptions({
|
||||
onBeforeUpload() {
|
||||
// don't check for file conflicts
|
||||
return true
|
||||
},
|
||||
})
|
||||
uppy.upload()
|
||||
}, [uppy])
|
||||
|
||||
const showFolderUploadConflicts = !overwrite && folderConflicts.length > 0
|
||||
const showFileUploadConfilcts =
|
||||
!overwrite && !showFolderUploadConflicts && conflicts.length > 0
|
||||
const showDashboard = !showFileUploadConfilcts && !showFolderUploadConflicts
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<UploadErrorMessage error={error} maxNumberOfFiles={maxNumberOfFiles} />
|
||||
)}
|
||||
{showFolderUploadConflicts && (
|
||||
<FolderUploadConflicts
|
||||
cancel={cancel}
|
||||
conflicts={folderConflicts}
|
||||
handleOverwrite={handleOverwrite}
|
||||
setError={setError}
|
||||
/>
|
||||
)}
|
||||
{showFileUploadConfilcts && (
|
||||
<FileUploadConflicts
|
||||
cancel={cancel}
|
||||
conflicts={conflicts}
|
||||
handleOverwrite={handleOverwrite}
|
||||
/>
|
||||
)}
|
||||
{showDashboard && (
|
||||
<Dashboard
|
||||
uppy={uppy}
|
||||
showProgressDetails
|
||||
// note={`Up to ${maxNumberOfFiles} files, up to ${maxFileSize / (1024 * 1024)}MB`}
|
||||
height={400}
|
||||
width="100%"
|
||||
showLinkToFileUploadResult={false}
|
||||
proudlyDisplayPoweredByUppy={false}
|
||||
// allow files or folders to be selected
|
||||
fileManagerSelectionType="both"
|
||||
locale={{
|
||||
strings: {
|
||||
// Text to show on the droppable area.
|
||||
// `%{browse}` is replaced with a link that opens the system file selection dialog.
|
||||
// TODO: 'drag_here' or 'drop_files_here_to_upload'?
|
||||
// dropHereOr: `${t('drag_here')} ${t('or')} %{browse}`,
|
||||
dropPasteBoth: `Drop or paste your files, folder, or images here. %{browseFiles} or %{browseFolders} from your computer.`,
|
||||
// Used as the label for the link that opens the system file selection dialog.
|
||||
// browseFiles: t('select_from_your_computer')
|
||||
browseFiles: 'Select files',
|
||||
browseFolders: 'select a folder',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function UploadErrorMessage({
|
||||
error,
|
||||
maxNumberOfFiles,
|
||||
}: {
|
||||
error: string
|
||||
maxNumberOfFiles: number
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
switch (error) {
|
||||
case 'too-many-files':
|
||||
return (
|
||||
<>
|
||||
{t('maximum_files_uploaded_together', {
|
||||
max: maxNumberOfFiles,
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
default:
|
||||
return <ErrorMessage error={error} />
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user