diff --git a/demo/collections/File.js b/demo/collections/File.js index d3b1891a43..b3c02f19d9 100644 --- a/demo/collections/File.js +++ b/demo/collections/File.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { slug: 'files', labels: { @@ -6,7 +8,7 @@ module.exports = { }, upload: { staticURL: '/files', - staticDir: 'demo/files', + staticDir: path.resolve(__dirname, '../files'), }, useAsTitle: 'filename', fields: [ diff --git a/demo/collections/Media.js b/demo/collections/Media.js index bde1d14cba..dfa4d53716 100644 --- a/demo/collections/Media.js +++ b/demo/collections/Media.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { slug: 'media', labels: { @@ -10,7 +12,7 @@ module.exports = { }, upload: { staticURL: '/media', - staticDir: 'demo/media', + staticDir: path.resolve(__dirname, '../media'), adminThumbnail: 'mobile', imageSizes: [ { diff --git a/src/client/components/elements/FileDetails/index.js b/src/client/components/elements/FileDetails/index.js index f537f0efb1..359cc8b4e9 100644 --- a/src/client/components/elements/FileDetails/index.js +++ b/src/client/components/elements/FileDetails/index.js @@ -1,30 +1,93 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import FileGraphic from '../../graphics/File'; +import config from '../../../config'; +import getThumbnail from '../../../../uploads/getThumbnail'; +import Button from '../Button'; +import formatFilesize from '../../../../uploads/formatFilesize'; import './index.scss'; +const { serverURL } = config; + const baseClass = 'file-details'; const FileDetails = (props) => { - const { filename, mimeType, filesize } = props; + const { + filename, mimeType, filesize, staticURL, adminThumbnail, sizes, handleRemove, width, height, + } = props; + + const [moreInfoOpen, setMoreInfoOpen] = useState(false); + + const thumbnail = getThumbnail(mimeType, staticURL, filename, sizes, adminThumbnail); return (
- +
+ {!thumbnail && ( + + )} + {thumbnail && ( +
+ {filename} +
+ )} +
+ +
+ {formatFilesize(filesize)} + {(width && height) && ( + <> +  -  + {width} + x + {height} + + )} + {mimeType && ( + <> +  -  + {mimeType} + + )} +
+
+
+
+ test +
); }; FileDetails.defaultProps = { - sizes: null, + adminThumbnail: undefined, }; FileDetails.propTypes = { filename: PropTypes.string.isRequired, mimeType: PropTypes.string.isRequired, filesize: PropTypes.number.isRequired, - sizes: PropTypes.shape({}), + staticURL: PropTypes.string.isRequired, + adminThumbnail: PropTypes.string, }; export default FileDetails; diff --git a/src/client/components/elements/FileDetails/index.scss b/src/client/components/elements/FileDetails/index.scss index e69de29bb2..908d7378a1 100644 --- a/src/client/components/elements/FileDetails/index.scss +++ b/src/client/components/elements/FileDetails/index.scss @@ -0,0 +1,29 @@ +@import '../../../scss/styles.scss'; + +.file-details { + header { + display: flex; + align-items: flex-start; + + .btn { + margin: $baseline; + } + } + + &__thumbnail { + width: base(6); + height: base(4.8); + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + &__main-detail { + padding: $baseline; + width: auto; + flex-grow: 1; + } +} diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index 2ce01f3345..c1e3a30909 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -17,7 +17,7 @@ import './index.scss'; const baseClass = 'form'; -const reduceFieldsToValues = (fields) => { +const reduceFieldsToValues = (fields, flatten) => { const data = {}; Object.keys(fields).forEach((key) => { @@ -26,8 +26,11 @@ const reduceFieldsToValues = (fields) => { } }); - const unflattened = unflatten(data, { safe: true }); - return unflattened; + if (flatten) { + return unflatten(data, { safe: true }); + } + + return data; }; const Form = (props) => { @@ -62,7 +65,7 @@ const Form = (props) => { }, [fields]); const getData = useCallback(() => { - return reduceFieldsToValues(fields); + return reduceFieldsToValues(fields, true); }, [fields]); const getSiblingData = useCallback((path) => { @@ -84,7 +87,7 @@ const Form = (props) => { }, {}); } - return reduceFieldsToValues(siblingFields); + return reduceFieldsToValues(siblingFields, true); }, [fields]); const getDataByPath = useCallback((path) => { @@ -102,7 +105,7 @@ const Form = (props) => { return matchedData; }, {}); - const values = reduceFieldsToValues(data); + const values = reduceFieldsToValues(data, true); const unflattenedData = unflatten(values); return unflattenedData?.[name]; }, [fields]); diff --git a/src/client/components/forms/field-types/File/index.js b/src/client/components/forms/field-types/File/index.js index 045ccb4911..3601b2b50e 100644 --- a/src/client/components/forms/field-types/File/index.js +++ b/src/client/components/forms/field-types/File/index.js @@ -30,7 +30,10 @@ const File = (props) => { const [selectingFile, setSelectingFile] = useState(false); const [dragging, setDragging] = useState(false); const [dragCounter, setDragCounter] = useState(0); - const { initialData = {} } = props; + const { + initialData = {}, adminThumbnail, imageSizes, staticURL, + } = props; + const { filename } = initialData; const { @@ -129,7 +132,12 @@ const File = (props) => { return (
{filename && ( - + )} {!filename && (
@@ -178,6 +186,7 @@ const File = (props) => { File.defaultProps = { initialData: undefined, + adminThumbnail: undefined, }; File.propTypes = { @@ -187,6 +196,8 @@ File.propTypes = { mimeType: PropTypes.string, filesize: PropTypes.number, }), + staticURL: PropTypes.string.isRequired, + adminThumbnail: PropTypes.string, }; export default File; diff --git a/src/client/components/views/collections/Edit/formatFields.js b/src/client/components/views/collections/Edit/formatFields.js index 0ef19ea95e..43ec0e3e53 100644 --- a/src/client/components/views/collections/Edit/formatFields.js +++ b/src/client/components/views/collections/Edit/formatFields.js @@ -17,6 +17,7 @@ const formatFields = (config, isEditing) => { const uploadFields = [ { type: 'file', + ...config.upload, }, ]; diff --git a/src/collections/operations/create.js b/src/collections/operations/create.js index e2901cc56d..fe8ddc8260 100644 --- a/src/collections/operations/create.js +++ b/src/collections/operations/create.js @@ -5,6 +5,8 @@ const executePolicy = require('../../auth/executePolicy'); const { MissingFile } = require('../../errors'); const resizeAndSave = require('../../uploads/imageResizer'); const getSafeFilename = require('../../uploads/getSafeFilename'); +const getImageSize = require('../../uploads/getImageSize'); +const imageMIMETypes = require('../../uploads/imageMIMETypes'); const performFieldOperations = require('../../fields/performFieldOperations'); @@ -39,7 +41,7 @@ const create = async (args) => { // ///////////////////////////////////// if (args.config.upload) { - const { staticDir, imageSizes } = options.req.collection.config.upload; + const { staticDir } = options.req.collection.config.upload; const fileData = {}; @@ -53,14 +55,18 @@ const create = async (args) => { await options.req.files.file.mv(`${staticDir}/${fsSafeName}`); + if (imageMIMETypes.indexOf(options.req.files.file.mimetype) > -1) { + const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`); + fileData.width = dimensions.width; + fileData.height = dimensions.height; + + fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); + } + fileData.filename = fsSafeName; fileData.filesize = options.req.files.file.size; fileData.mimeType = options.req.files.file.mimetype; - if (imageSizes) { - fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); - } - options.data = { ...options.data, ...fileData, diff --git a/src/collections/operations/update.js b/src/collections/operations/update.js index de03a21732..eb84532067 100644 --- a/src/collections/operations/update.js +++ b/src/collections/operations/update.js @@ -3,6 +3,9 @@ const overwriteMerge = require('../../utilities/overwriteMerge'); const executePolicy = require('../../auth/executePolicy'); const { NotFound, Forbidden } = require('../../errors'); const performFieldOperations = require('../../fields/performFieldOperations'); +const imageMIMETypes = require('../../uploads/imageMIMETypes'); +const getImageSize = require('../../uploads/getImageSize'); +const getSafeFilename = require('../../uploads/getSafeFilename'); const resizeAndSave = require('../../uploads/imageResizer'); @@ -79,15 +82,21 @@ const update = async (args) => { if (args.config.upload) { const fileData = {}; - const { staticDir, imageSizes } = args.config.upload; + const { staticDir } = args.config.upload; if (args.req.files || args.req.files.file) { - await options.req.files.file.mv(`${staticDir}/${options.req.files.file.name}`); + const fsSafeName = await getSafeFilename(staticDir, options.req.files.file.name); - fileData.filename = options.req.files.file.name; + await options.req.files.file.mv(`${staticDir}/${fsSafeName}`); - if (imageSizes) { - fileData.sizes = await resizeAndSave(options.config, options.req.files.file.name); + fileData.filename = fsSafeName; + + if (imageMIMETypes.indexOf(options.req.files.file.mimetype) > -1) { + const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`); + fileData.width = dimensions.width; + fileData.height = dimensions.height; + + fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); } options.data = { diff --git a/src/express/static.js b/src/express/static.js index 9f6925ea9c..bd336e3e2a 100644 --- a/src/express/static.js +++ b/src/express/static.js @@ -3,7 +3,7 @@ const express = require('express'); function initStatic() { this.config.collections.forEach((collection) => { if (collection.upload) { - this.express.use(`${collection.upload.staticURL}*`, express.static(collection.upload.staticDir)); + this.express.use(`${collection.upload.staticURL}`, express.static(collection.upload.staticDir)); } }); } diff --git a/src/uploads/formatFilesize.js b/src/uploads/formatFilesize.js new file mode 100644 index 0000000000..1b2d92c174 --- /dev/null +++ b/src/uploads/formatFilesize.js @@ -0,0 +1,13 @@ +function formatBytes(bytes, decimals = 0) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / (k ** i)).toFixed(dm))} ${sizes[i]}`; +} + +module.exports = formatBytes; diff --git a/src/uploads/getImageSize.js b/src/uploads/getImageSize.js new file mode 100644 index 0000000000..c19b7ada75 --- /dev/null +++ b/src/uploads/getImageSize.js @@ -0,0 +1,6 @@ +const imageSize = require('image-size'); +const { promisify } = require('util'); + +const getImageSize = promisify(imageSize); + +module.exports = getImageSize; diff --git a/src/uploads/getThumbnail.js b/src/uploads/getThumbnail.js new file mode 100644 index 0000000000..61b9285760 --- /dev/null +++ b/src/uploads/getThumbnail.js @@ -0,0 +1,15 @@ +const imageMIMETypes = require('./imageMIMETypes'); + +const getThumbnail = (mimeType, staticURL, filename, sizes, adminThumbnail) => { + if (imageMIMETypes.indexOf(mimeType) > -1) { + if (sizes?.[adminThumbnail]?.filename) { + return `${staticURL}/${sizes[adminThumbnail].filename}`; + } + + return `${staticURL}/${filename}`; + } + + return false; +}; + +module.exports = getThumbnail; diff --git a/src/uploads/imageMIMETypes.js b/src/uploads/imageMIMETypes.js new file mode 100644 index 0000000000..192a293c43 --- /dev/null +++ b/src/uploads/imageMIMETypes.js @@ -0,0 +1,3 @@ +const types = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']; + +module.exports = types; diff --git a/src/uploads/imageResizer.js b/src/uploads/imageResizer.js index 7d05277842..b94b62e413 100644 --- a/src/uploads/imageResizer.js +++ b/src/uploads/imageResizer.js @@ -1,12 +1,9 @@ const fs = require('fs'); const sharp = require('sharp'); const sanitize = require('sanitize-filename'); -const { promisify } = require('util'); -const imageSize = require('image-size'); +const getImageSize = require('./getImageSize'); const fileExists = require('./fileExists'); -const sizeOf = promisify(imageSize); - function getOutputImage(sourceImage, size) { const extension = sourceImage.split('.').pop(); const name = sanitize(sourceImage.substr(0, sourceImage.lastIndexOf('.')) || sourceImage); @@ -33,7 +30,7 @@ module.exports = async function resizeAndSave(config, savedFilename, mimeType) { const sourceImage = `${staticDir}/${savedFilename}`; let sizes; try { - const dimensions = await sizeOf(sourceImage); + const dimensions = await getImageSize(sourceImage); sizes = imageSizes .filter(desiredSize => desiredSize.width < dimensions.width) .map(async (desiredSize) => {