Added file support

This commit is contained in:
Tom Butcher 2025-09-07 19:41:26 +01:00
parent 8993caeac5
commit bdc376af25
6 changed files with 408 additions and 23 deletions

View File

@ -19,6 +19,9 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import FileIcon from '../../../Icons/FileIcon.jsx'
import FilePreview from '../../common/FilePreview.jsx'
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
const log = loglevel.getLogger('FileInfo')
log.setLevel(config.logLevel)
@ -39,7 +42,9 @@ const FileInfo = () => {
formValid: false,
lock: null,
loading: false,
objectData: {}
objectData: {
_id: fileId
}
})
const actions = {
@ -85,6 +90,7 @@ const FileInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'File Information' },
{ key: 'preview', label: 'Preview' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@ -138,6 +144,7 @@ const FileInfo = () => {
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
console.log(state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
@ -152,6 +159,24 @@ const FileInfo = () => {
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Preview'
icon={<FileIcon />}
active={collapseState.preview}
onToggle={(expanded) => updateCollapseState('preview', expanded)}
collapseKey='preview'
>
{objectFormState?.objectData?._id ? (
<Card>
<FilePreview
file={objectFormState?.objectData}
style={{ width: '100%', height: '100%' }}
/>
</Card>
) : (
<MissingPlaceholder message={'No file.'} />
)}
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}

View File

@ -0,0 +1,147 @@
import { Card, Flex, Typography, Button, Tag, Divider } from 'antd'
import PropTypes from 'prop-types'
import FileIcon from '../../Icons/FileIcon'
import BinIcon from '../../Icons/BinIcon'
import EyeIcon from '../../Icons/EyeIcon'
import DownloadIcon from '../../Icons/DownloadIcon'
import { useContext, useState } from 'react'
import { ApiServerContext } from '../context/ApiServerContext'
import FilePreview from './FilePreview'
import EyeSlashIcon from '../../Icons/EyeSlashIcon'
import { getModelByName } from '../../../database/ObjectModels'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import { useNavigate } from 'react-router-dom'
const { Text } = Typography
const FileList = ({
files,
onChange,
multiple = true,
editing = false,
showPreview = true,
showInfo = true,
showDownload = true,
defaultPreviewOpen = false
}) => {
const { fetchFileContent } = useContext(ApiServerContext)
const navigate = useNavigate()
const [previewOpen, setPreviewOpen] = useState(defaultPreviewOpen)
const infoAction = getModelByName('file').actions.filter(
(action) => action.name == 'info'
)[0]
// Check if there are no items in the list
const hasNoItems = multiple
? !files || !Array.isArray(files) || files.length === 0
: !files
if (hasNoItems) {
return null
}
const handleRemove = (fileToRemove) => {
if (multiple) {
const currentFiles = Array.isArray(files) ? files : []
const updatedFiles = currentFiles.filter((file) => {
const fileUid = file._id || file.id
const removeUid = fileToRemove._id || fileToRemove.id
return fileUid !== removeUid
})
onChange(updatedFiles)
} else {
onChange(null)
}
}
const handleDownload = async (file) => {
await fetchFileContent(file, true)
}
const filesToRender = multiple ? files : [files]
return (
<div style={{ width: '100%' }}>
{filesToRender.map((file, index) => (
<Card
styles={{ body: { padding: '10px' } }}
key={file._id || file.id || index}
style={{
marginTop: index > 0 && multiple ? '4px' : undefined
}}
>
<Flex vertical gap='10px'>
<Flex justify='space-between'>
<Flex gap={'small'} align='center'>
<FileIcon style={{ margin: 0, fontSize: '24px' }} />
<Text>{file.name || file.filename || 'Unknown file'}</Text>
<Tag>{file.extension}</Tag>
</Flex>
<Flex gap={'small'} align='center'>
{showDownload && (
<Button
icon={<DownloadIcon />}
size='small'
type='text'
onClick={() => handleDownload(file)}
/>
)}
{showPreview && (
<Button
icon={previewOpen ? <EyeSlashIcon /> : <EyeIcon />}
size='small'
type='text'
onClick={() => {
if (previewOpen == true) {
setPreviewOpen(false)
} else {
setPreviewOpen(true)
}
}}
/>
)}
{showInfo && (
<Button
icon={<InfoCircleIcon />}
size='small'
type='text'
onClick={() => {
navigate(infoAction.url(file._id))
}}
/>
)}
{editing && (
<Button
icon={<BinIcon />}
size='small'
type='text'
onClick={() => handleRemove(file)}
/>
)}
</Flex>
</Flex>
{previewOpen ? (
<>
<Divider style={{ margin: 0 }} />
<FilePreview file={file} style={{ width: '100%' }} />
</>
) : null}
</Flex>
</Card>
))}
</div>
)
}
FileList.propTypes = {
files: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
onChange: PropTypes.func,
multiple: PropTypes.bool,
editing: PropTypes.bool,
showPreview: PropTypes.string,
showInfo: PropTypes.bool,
showDownload: PropTypes.bool,
defaultPreviewOpen: PropTypes.bool
}
export default FileList

View File

@ -0,0 +1,88 @@
import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext'
import { useCallback, useContext, useEffect, useState, memo } from 'react'
import LoadingPlaceholder from './LoadingPlaceholder'
import GCodePreview from './GCodePreview'
import ThreeDPreview from './ThreeDPreview'
import { AuthContext } from '../context/AuthContext'
const FilePreview = ({ file, style = {} }) => {
const { token } = useContext(AuthContext)
const { fetchFileContent } = useContext(ApiServerContext)
const [fileObjectUrl, setFileObjectUrl] = useState(null)
const [loading, setLoading] = useState(true)
const fetchPreview = useCallback(async () => {
setLoading(true)
const objectUrl = await fetchFileContent(file, false)
setFileObjectUrl(objectUrl)
setLoading(false)
}, [file._id, fetchFileContent])
useEffect(() => {
if (file?.type && token != null) {
fetchPreview()
}
}, [file._id, file?.type, fetchPreview, token])
console.log('file', file)
if (loading == true || !file?.type) {
return <LoadingPlaceholder message={'Loading file preview...'} />
}
const isGcode = ['.g', '.gcode'].includes(
(file?.extension || '').toLowerCase()
)
const is3DModel = ['.stl', '.3mf'].includes(
(file?.extension || '').toLowerCase()
)
const isImage = file?.type.startsWith('image/')
if (isGcode && fileObjectUrl) {
return (
<GCodePreview
src={fileObjectUrl}
topLayerColor={'#ff9800'}
lastSegmentColor={'#e91e63'}
startLayer={0}
endLayer={undefined}
lineWidth={1}
style={style}
/>
)
}
if (is3DModel && fileObjectUrl) {
return (
<ThreeDPreview
src={fileObjectUrl}
style={style}
extension={file.extension}
/>
)
}
if (isImage && fileObjectUrl) {
return <img src={fileObjectUrl} style={style}></img>
}
return null
}
FilePreview.propTypes = {
file: PropTypes.object.isRequired,
style: PropTypes.object
}
// Custom comparison function to only re-render when file._id changes
const areEqual = (prevProps, nextProps) => {
return (
prevProps.file?._id === nextProps.file?._id &&
JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style)
)
}
export default memo(FilePreview, areEqual)

View File

@ -0,0 +1,119 @@
import { Upload, Button, Flex, Typography, Space } from 'antd'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext'
import UploadIcon from '../../Icons/UploadIcon'
import { useContext, useState, useEffect } from 'react'
import ObjectSelect from './ObjectSelect'
import FileList from './FileList'
import PlusIcon from '../../Icons/PlusIcon'
const { Text } = Typography
const FileUpload = ({
value,
onChange,
multiple = true,
defaultPreviewOpen = false,
showPreview = true
}) => {
const { uploadFile } = useContext(ApiServerContext)
// Track current files using useState
const [currentFiles, setCurrentFiles] = useState(() => {
if (multiple) {
return Array.isArray(value) ? value : []
} else {
return value || null
}
})
// Update currentFiles when value prop changes
useEffect(() => {
if (multiple) {
setCurrentFiles(Array.isArray(value) ? value : [])
} else {
setCurrentFiles(value || null)
}
}, [value, multiple])
// Track if there are no items in the list
const [hasNoItems, setHasNoItems] = useState(false)
// Update hasNoItems when currentFiles changes
useEffect(() => {
const noItems = multiple
? !currentFiles ||
!Array.isArray(currentFiles) ||
currentFiles.length === 0
: !currentFiles
setHasNoItems(noItems)
console.log('No items', noItems)
}, [currentFiles, multiple])
const handleFileUpload = async (file) => {
try {
const uploadedFile = await uploadFile(file)
if (uploadedFile) {
if (multiple) {
// For multiple files, add to existing array
const newFiles = [...currentFiles, uploadedFile]
setCurrentFiles(newFiles)
onChange(newFiles)
} else {
// For single file, replace the value
setCurrentFiles(uploadedFile)
onChange(uploadedFile)
}
}
} catch (error) {
console.error('File upload failed:', error)
}
return false // Prevent default upload behavior
}
return (
<Flex gap={'small'} vertical>
{hasNoItems ? (
<Flex gap={'small'} align='center'>
<Space.Compact style={{ flexGrow: 1 }}>
<ObjectSelect type={'file'} />
<Button icon={<PlusIcon />} />
</Space.Compact>
<Text style={{ whiteSpace: 'nowrap' }} type='secondary'>
or
</Text>
<Upload
beforeUpload={handleFileUpload}
showUploadList={false}
multiple={multiple}
>
<Button style={{ width: '100%' }} icon={<UploadIcon />}>
Upload
</Button>
</Upload>
</Flex>
) : null}
<FileList
files={currentFiles}
multiple={multiple}
editing={true}
showInfo={false}
showPreview={showPreview}
defaultPreviewOpen={defaultPreviewOpen}
onChange={(updatedFiles) => {
setCurrentFiles(updatedFiles)
}}
/>
</Flex>
)
}
FileUpload.propTypes = {
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
onChange: PropTypes.func,
multiple: PropTypes.bool,
showPreview: PropTypes.string,
defaultPreviewOpen: PropTypes.bool
}
export default FileUpload

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/uploadicon.svg?react'
const UploadIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default UploadIcon

View File

@ -7,7 +7,7 @@ import BinIcon from '../../components/Icons/BinIcon'
export const File = {
name: 'file',
label: 'File',
prefix: 'VEN',
prefix: 'FLE',
icon: FileIcon,
actions: [
{
@ -88,31 +88,31 @@ export const File = {
type: 'number',
readOnly: true,
required: true,
suffix: () => {
return 'gb'
suffix: (objectData) => {
const size = objectData?.size || 0
if (size === 0) return ' B'
if (size < 1024) return ' B'
if (size < 1024 * 1024) return ' KB'
if (size < 1024 * 1024 * 1024) return ' MB'
if (size < 1024 * 1024 * 1024 * 1024) return ' GB'
return ' TB'
},
value: (objectData) => {
const size = objectData?.size || 0
if (size === 0) return 0
if (size < 1024) return size
if (size < 1024 * 1024) return size / 1024
if (size < 1024 * 1024 * 1024) return size / (1024 * 1024)
if (size < 1024 * 1024 * 1024 * 1024) return size / (1024 * 1024 * 1024)
return size / (1024 * 1024 * 1024 * 1024)
}
},
{
name: 'email',
label: 'Email',
name: 'metaData',
label: 'Meta Data',
columnWidth: 300,
type: 'email',
readOnly: false,
required: false
},
{
name: 'phone',
label: 'Phone',
type: 'phone',
readOnly: false,
required: false
},
{
name: 'website',
label: 'Website',
columnWidth: 300,
type: 'url',
readOnly: false,
type: 'data',
readOnly: true,
required: false
}
]