From bdc376af25af18ba67b1739c68fb2f9b46b92710 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 7 Sep 2025 19:41:26 +0100 Subject: [PATCH] Added file support --- .../Dashboard/Management/Files/FileInfo.jsx | 27 +++- src/components/Dashboard/common/FileList.jsx | 147 ++++++++++++++++++ .../Dashboard/common/FilePreview.jsx | 88 +++++++++++ .../Dashboard/common/FileUpload.jsx | 119 ++++++++++++++ src/components/Icons/UploadIcon.jsx | 6 + src/database/models/File.js | 44 +++--- 6 files changed, 408 insertions(+), 23 deletions(-) create mode 100644 src/components/Dashboard/common/FileList.jsx create mode 100644 src/components/Dashboard/common/FilePreview.jsx create mode 100644 src/components/Dashboard/common/FileUpload.jsx create mode 100644 src/components/Icons/UploadIcon.jsx 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 && ( +
+ ) +} + +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 ? ( + + + + + + + ) : 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 } ]