diff --git a/package.json b/package.json index a120860292..5a2f7f69f6 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "mongoose-hidden": "^1.8.1", "mongoose-paginate-v2": "^1.3.6", "nodemailer": "^6.4.2", + "object-to-formdata": "^3.0.9", "passport": "^0.4.1", "passport-anonymous": "^1.0.1", "passport-headerapikey": "^1.2.1", diff --git a/src/client/components/elements/IconButton/index.js b/src/client/components/elements/IconButton/index.js deleted file mode 100644 index 7f9ab900d6..0000000000 --- a/src/client/components/elements/IconButton/index.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Button from '../Button'; -import Plus from '../../icons/Plus'; -import X from '../../icons/X'; -import Chevron from '../../icons/Chevron'; - -import './index.scss'; - -const baseClass = 'icon-button'; - -const IconButton = React.forwardRef(({ iconName, className, ...rest }, ref) => { - const classes = [ - baseClass, - className && className, - `${baseClass}--${iconName}`, - ].filter(Boolean).join(' '); - - const icons = { - Plus, - X, - Chevron, - }; - - const Icon = icons[iconName] || icons.arrow; - - return ( - - - - ); -}); - -IconButton.defaultProps = { - className: '', -}; - -IconButton.propTypes = { - iconName: PropTypes.oneOf(['Chevron', 'X', 'Plus']).isRequired, - className: PropTypes.string, -}; - -export default IconButton; diff --git a/src/client/components/elements/IconButton/index.scss b/src/client/components/elements/IconButton/index.scss deleted file mode 100644 index 5b8fcb9eb5..0000000000 --- a/src/client/components/elements/IconButton/index.scss +++ /dev/null @@ -1,24 +0,0 @@ -@import '../../../scss/styles.scss'; - -.icon-button { - line-height: 0; - background-color: transparent; - margin-top: 0; - @include color-svg($color-gray ); - - &:hover, - &:focus { - background-color: transparent; - @include color-svg($color-dark-gray); - } - - &.btn { - padding: 5px; - } - - &--crossOut { - svg { - transform: rotate(45deg); - } - } -} diff --git a/src/client/components/elements/Paginator/index.js b/src/client/components/elements/Paginator/index.js index 50b6eb32bd..34312601c7 100644 --- a/src/client/components/elements/Paginator/index.js +++ b/src/client/components/elements/Paginator/index.js @@ -29,15 +29,21 @@ const Pagination = (props) => { prevPage, nextPage, numberOfNeighbors, + disableHistoryChange, + onChange, } = props; if (!totalPages || totalPages <= 1) return null; // uses react router to set the current page const updatePage = (page) => { - const params = queryString.parse(location.search, { ignoreQueryPrefix: true }); - params.page = page; - history.push({ search: queryString.stringify(params, { addQueryPrefix: true }) }); + if (!disableHistoryChange) { + const params = queryString.parse(location.search, { ignoreQueryPrefix: true }); + params.page = page; + history.push({ search: queryString.stringify(params, { addQueryPrefix: true }) }); + } + + if (typeof onChange === 'function') onChange(page); }; // Create array of integers for each page @@ -139,6 +145,8 @@ Pagination.defaultProps = { prevPage: null, nextPage: null, numberOfNeighbors: 1, + disableHistoryChange: false, + onChange: undefined, }; Pagination.propTypes = { @@ -150,4 +158,6 @@ Pagination.propTypes = { prevPage: PropTypes.number, nextPage: PropTypes.number, numberOfNeighbors: PropTypes.number, + disableHistoryChange: PropTypes.bool, + onChange: PropTypes.func, }; diff --git a/src/client/components/elements/Thumbnail/index.js b/src/client/components/elements/Thumbnail/index.js index ebbdcaa8df..e8d1bb3dae 100644 --- a/src/client/components/elements/Thumbnail/index.js +++ b/src/client/components/elements/Thumbnail/index.js @@ -50,7 +50,7 @@ Thumbnail.propTypes = { adminThumbnail: PropTypes.string, mimeType: PropTypes.string, staticURL: PropTypes.string.isRequired, - size: PropTypes.oneOf(['small', 'medium', 'large']), + size: PropTypes.oneOf(['small', 'medium', 'large', 'expand']), }; export default Thumbnail; diff --git a/src/client/components/elements/Thumbnail/index.scss b/src/client/components/elements/Thumbnail/index.scss index 7fea756467..91fde3e3f9 100644 --- a/src/client/components/elements/Thumbnail/index.scss +++ b/src/client/components/elements/Thumbnail/index.scss @@ -12,13 +12,29 @@ object-fit: cover; } + &--size-expand { + max-height: 100%; + width: 100%; + padding-top: 100%; + position: relative; + + img, svg { + position: absolute; + } + } + + &--size-large { + max-height: base(9); + width: base(9); + } + &--size-medium { - max-height: base(6); + max-height: base(7); width: base(7); } &--size-small { - max-height: base(4); + max-height: base(5); width: base(5); } diff --git a/src/client/components/elements/UploadCard/index.js b/src/client/components/elements/UploadCard/index.js new file mode 100644 index 0000000000..0958bd1922 --- /dev/null +++ b/src/client/components/elements/UploadCard/index.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Thumbnail from '../Thumbnail'; + +import './index.scss'; + +const baseClass = 'upload-card'; + +const UploadCard = (props) => { + const { + onClick, + mimeType, + sizes, + filename, + collection: { + upload: { + adminThumbnail, + staticURL, + } = {}, + } = {}, + } = props; + + const classes = [ + baseClass, + typeof onClick === 'function' && `${baseClass}--has-on-click`, + ].filter(Boolean).join(' '); + + return ( +
+ +
+ {filename} +
+
+ ); +}; + +UploadCard.defaultProps = { + sizes: undefined, + onClick: undefined, +}; + +UploadCard.propTypes = { + collection: PropTypes.shape({ + labels: PropTypes.shape({ + singular: PropTypes.string, + }), + upload: PropTypes.shape({ + adminThumbnail: PropTypes.string, + staticURL: PropTypes.string, + }), + }).isRequired, + id: PropTypes.string.isRequired, + filename: PropTypes.string.isRequired, + mimeType: PropTypes.string.isRequired, + sizes: PropTypes.shape({}), + onClick: PropTypes.func, +}; + + +export default UploadCard; diff --git a/src/client/components/elements/UploadCard/index.scss b/src/client/components/elements/UploadCard/index.scss new file mode 100644 index 0000000000..c7bc2f41a5 --- /dev/null +++ b/src/client/components/elements/UploadCard/index.scss @@ -0,0 +1,19 @@ +@import '../../../scss/styles.scss'; + +.upload-card { + @include shadow; + background: white; + max-width: base(9); + margin-bottom: base(.5); + + &__filename { + padding: base(.5); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &--has-on-click { + cursor: pointer; + } +} diff --git a/src/client/components/elements/UploadGallery/index.js b/src/client/components/elements/UploadGallery/index.js new file mode 100644 index 0000000000..88e5bb9bb7 --- /dev/null +++ b/src/client/components/elements/UploadGallery/index.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import UploadCard from '../UploadCard'; + +import './index.scss'; + +const baseClass = 'upload-gallery'; + +const UploadGallery = (props) => { + const { docs, onCardClick, collection } = props; + + + if (docs && docs.length > 0) { + return ( + + ); + } + + return null; +}; + +UploadGallery.defaultProps = { + docs: undefined, +}; + +UploadGallery.propTypes = { + docs: PropTypes.arrayOf( + PropTypes.shape({}), + ), + collection: PropTypes.shape({}).isRequired, + onCardClick: PropTypes.func.isRequired, +}; + +export default UploadGallery; diff --git a/src/client/components/elements/UploadGallery/index.scss b/src/client/components/elements/UploadGallery/index.scss new file mode 100644 index 0000000000..3eef52cc6b --- /dev/null +++ b/src/client/components/elements/UploadGallery/index.scss @@ -0,0 +1,12 @@ +@import '../../../scss/styles.scss'; + +.upload-gallery { + list-style: none; + padding: 0; + margin: base(2) 0; + display: flex; + + .upload-card { + margin-right: base(.5); + } +} diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index c1e3a30909..394a5dcc7f 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -1,6 +1,7 @@ import React, { useState, useReducer, useCallback, useEffect, } from 'react'; +import { objectToFormData } from 'object-to-formdata'; import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; import { unflatten } from 'flatley'; @@ -121,14 +122,8 @@ const Form = (props) => { }, [fields]); const createFormData = useCallback(() => { - const formData = new FormData(); const data = reduceFieldsToValues(fields); - - Object.entries(data).forEach(([key, value]) => { - formData.append(key, value); - }); - - return formData; + return objectToFormData(data, { indices: true }); }, [fields]); const submit = useCallback((e) => { diff --git a/src/client/components/forms/field-types/Upload/SelectExisting/index.js b/src/client/components/forms/field-types/Upload/SelectExisting/index.js index 6e34a49924..0ab6eebba7 100644 --- a/src/client/components/forms/field-types/Upload/SelectExisting/index.js +++ b/src/client/components/forms/field-types/Upload/SelectExisting/index.js @@ -9,6 +9,7 @@ import formatFields from '../../../../views/collections/List/formatFields'; import usePayloadAPI from '../../../../../hooks/usePayloadAPI'; import ListControls from '../../../../elements/ListControls'; import Paginator from '../../../../elements/Paginator'; +import UploadGallery from '../../../../elements/UploadGallery'; import './index.scss'; @@ -18,22 +19,17 @@ const baseClass = 'select-existing-upload-modal'; const SelectExistingUploadModal = (props) => { const { + setValue, collection, collection: { slug: collectionSlug, - labels: { - plural: pluralLabel, - singular: singularLabel, - }, } = {}, slug: modalSlug, - addModalSlug, } = props; - const { closeAll, toggle } = useModal(); + const { closeAll } = useModal(); const [fields, setFields] = useState(collection.fields); const [listControls, setListControls] = useState({}); - const [sort, setSort] = useState(null); const [page, setPage] = useState(null); const classes = [ @@ -52,11 +48,10 @@ const SelectExistingUploadModal = (props) => { const params = {}; if (page) params.page = page; - if (sort) params.sort = sort; if (listControls?.where) params.where = listControls.where; setParams(params); - }, [setParams, page, sort, listControls]); + }, [setParams, page, listControls]); return ( { >
-
+

{' '} Select existing @@ -87,41 +82,14 @@ const SelectExistingUploadModal = (props) => { fields, }} /> - {(data.docs && data.docs.length > 0) && ( -
    - {data.docs.map((doc, i) => { - return ( -
  • - doc.id -
  • - ); - })} -
- )} - {data.docs && data.docs.length === 0 && ( -
-

- No - {' '} - {pluralLabel} - {' '} - found. Either no - {' '} - {pluralLabel} - {' '} - exist yet or none match the filters you've specified above. -

- -
- )} + { + setValue(doc); + closeAll(); + }} + />
{ prevPage={data.prevPage} nextPage={data.nextPage} numberOfNeighbors={1} + onChange={setPage} + disableHistoryChange /> {data?.totalDocs > 0 && (
@@ -152,13 +122,16 @@ const SelectExistingUploadModal = (props) => { }; SelectExistingUploadModal.propTypes = { + setValue: PropTypes.func.isRequired, collection: PropTypes.shape({ labels: PropTypes.shape({ singular: PropTypes.string, }), + fields: PropTypes.arrayOf( + PropTypes.shape({}), + ), }).isRequired, slug: PropTypes.string.isRequired, - addModalSlug: PropTypes.string.isRequired, }; export default SelectExistingUploadModal; diff --git a/src/client/components/forms/field-types/Upload/SelectExisting/index.scss b/src/client/components/forms/field-types/Upload/SelectExisting/index.scss index 087a6b1bda..1a346cd53b 100644 --- a/src/client/components/forms/field-types/Upload/SelectExisting/index.scss +++ b/src/client/components/forms/field-types/Upload/SelectExisting/index.scss @@ -6,7 +6,12 @@ align-items: center; height: 100%; - header { + .template-minimal { + padding-top: base(6); + align-items: flex-start; + } + + &__header { display: flex; margin-bottom: $baseline; diff --git a/src/client/components/forms/field-types/Upload/Selected/index.js b/src/client/components/forms/field-types/Upload/Selected/index.js deleted file mode 100644 index 5960949fe6..0000000000 --- a/src/client/components/forms/field-types/Upload/Selected/index.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import './index.scss'; - -const baseClass = 'selected-upload'; - -const SelectedUpload = () => { - const { id, collection } = props; - - return ( -
- {id} -
- ); -}; - -SelectedUpload.propTypes = { - collection: PropTypes.shape({ - labels: PropTypes.shape({ - singular: PropTypes.string, - }), - }).isRequired, - id: PropTypes.string.isRequired, -}; - - -export default SelectedUpload; diff --git a/src/client/components/forms/field-types/Upload/Selected/index.scss b/src/client/components/forms/field-types/Upload/Selected/index.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/client/components/forms/field-types/Upload/index.js b/src/client/components/forms/field-types/Upload/index.js index 281502eec8..146ae2016f 100644 --- a/src/client/components/forms/field-types/Upload/index.js +++ b/src/client/components/forms/field-types/Upload/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useModal } from '@trbl/react-modal'; import config from '../../../../config'; @@ -8,7 +8,7 @@ import Button from '../../../elements/Button'; import Label from '../../Label'; import Error from '../../Error'; import { upload } from '../../../../../fields/validations'; -import SelectedUpload from './Selected'; +import FileDetails from '../../../elements/FileDetails'; import AddModal from './Add'; import SelectExistingModal from './SelectExisting'; @@ -19,7 +19,8 @@ const { collections } = config; const baseClass = 'upload'; const Upload = (props) => { - const { closeAll, toggle } = useModal(); + const { toggle } = useModal(); + const [internalValue, setInternalValue] = useState(undefined); const { path: pathFromProps, @@ -63,6 +64,12 @@ const Upload = (props) => { readOnly && 'read-only', ].filter(Boolean).join(' '); + useEffect(() => { + if (initialData) { + setInternalValue(initialData); + } + }, [initialData]); + return (
{ label={label} required={required} /> - {collection && ( + {collection?.upload && ( <> - {value && ( - { + setInternalValue(undefined); + setValue(null); + }} /> )} {!value && ( @@ -111,11 +122,22 @@ const Upload = (props) => {
)} { + setValue(val.id); + setInternalValue(val); + }, }} /> { + setValue(val.id); + setInternalValue(val); + }, + addModalSlug, }} /> @@ -142,7 +164,7 @@ Upload.propTypes = { required: PropTypes.bool, readOnly: PropTypes.bool, defaultValue: PropTypes.string, - initialData: PropTypes.string, + initialData: PropTypes.shape({}), validate: PropTypes.func, width: PropTypes.string, style: PropTypes.shape({}), diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 04c4ed2441..1c7a908d4f 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -50,6 +50,17 @@ module.exports = async (config, operation) => { const traverseFields = (fields, data = {}, path) => { fields.forEach((field) => { + const dataCopy = data; + + if (field.type === 'upload') { + if (data[field.name] === '') dataCopy[field.name] = null; + } + + if (field.type === 'checkbox') { + if (data[field.name] === 'true') dataCopy[field.name] = true; + if (data[field.name] === 'false') dataCopy[field.name] = false; + } + policyPromises.push(createPolicyPromise(data, field)); hookPromises.push(createHookPromise(data, field)); diff --git a/yarn.lock b/yarn.lock index abe8208431..c21247169f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7797,6 +7797,11 @@ object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== +object-to-formdata@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/object-to-formdata/-/object-to-formdata-3.0.9.tgz#40e3314522345789259738811c0222b7d6e556e9" + integrity sha512-1JDHRFSpk6wzhPBAAqdVncdvbjZ+bjB0tioruNdKn8UyudBBXiBxRa7PJyvYqp4ioEKX98dEOX9Fw1RxA+vLzg== + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"