diff --git a/src/components/Dashboard/Management/Files/FileInfo.jsx b/src/components/Dashboard/Management/Files/FileInfo.jsx
index f527794..eaa9ebd 100644
--- a/src/components/Dashboard/Management/Files/FileInfo.jsx
+++ b/src/components/Dashboard/Management/Files/FileInfo.jsx
@@ -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 = () => {
+ }
+ active={collapseState.preview}
+ onToggle={(expanded) => updateCollapseState('preview', expanded)}
+ collapseKey='preview'
+ >
+ {objectFormState?.objectData?._id ? (
+
+
+
+ ) : (
+
+ )}
+
}
diff --git a/src/components/Dashboard/common/FileList.jsx b/src/components/Dashboard/common/FileList.jsx
new file mode 100644
index 0000000..d843806
--- /dev/null
+++ b/src/components/Dashboard/common/FileList.jsx
@@ -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 (
+
+ {filesToRender.map((file, index) => (
+
0 && multiple ? '4px' : undefined
+ }}
+ >
+
+
+
+
+
+ {file.name || file.filename || 'Unknown file'}
+ {file.extension}
+
+
+ {showDownload && (
+ }
+ size='small'
+ type='text'
+ onClick={() => handleDownload(file)}
+ />
+ )}
+ {showPreview && (
+ : }
+ size='small'
+ type='text'
+ onClick={() => {
+ if (previewOpen == true) {
+ setPreviewOpen(false)
+ } else {
+ setPreviewOpen(true)
+ }
+ }}
+ />
+ )}
+ {showInfo && (
+ }
+ size='small'
+ type='text'
+ onClick={() => {
+ navigate(infoAction.url(file._id))
+ }}
+ />
+ )}
+ {editing && (
+ }
+ size='small'
+ type='text'
+ onClick={() => handleRemove(file)}
+ />
+ )}
+
+
+ {previewOpen ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+ ))}
+
+ )
+}
+
+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
diff --git a/src/components/Dashboard/common/FilePreview.jsx b/src/components/Dashboard/common/FilePreview.jsx
new file mode 100644
index 0000000..2596f1b
--- /dev/null
+++ b/src/components/Dashboard/common/FilePreview.jsx
@@ -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
+ }
+
+ 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 (
+
+ )
+ }
+
+ if (is3DModel && fileObjectUrl) {
+ return (
+
+ )
+ }
+
+ if (isImage && fileObjectUrl) {
+ return
+ }
+ 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)
diff --git a/src/components/Dashboard/common/FileUpload.jsx b/src/components/Dashboard/common/FileUpload.jsx
new file mode 100644
index 0000000..4bce154
--- /dev/null
+++ b/src/components/Dashboard/common/FileUpload.jsx
@@ -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 (
+
+ {hasNoItems ? (
+
+
+
+ } />
+
+
+ or
+
+
+ }>
+ Upload
+
+
+
+ ) : null}
+ {
+ setCurrentFiles(updatedFiles)
+ }}
+ />
+
+ )
+}
+
+FileUpload.propTypes = {
+ value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
+ onChange: PropTypes.func,
+ multiple: PropTypes.bool,
+ showPreview: PropTypes.string,
+ defaultPreviewOpen: PropTypes.bool
+}
+
+export default FileUpload
diff --git a/src/components/Icons/UploadIcon.jsx b/src/components/Icons/UploadIcon.jsx
new file mode 100644
index 0000000..014dd21
--- /dev/null
+++ b/src/components/Icons/UploadIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/uploadicon.svg?react'
+
+const UploadIcon = (props) =>
+
+export default UploadIcon
diff --git a/src/database/models/File.js b/src/database/models/File.js
index cb4dbcc..6d15955 100644
--- a/src/database/models/File.js
+++ b/src/database/models/File.js
@@ -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
}
]