implements upload field type with ability to select existing upload

This commit is contained in:
James
2020-06-27 16:35:21 -04:00
parent e7eca5b30c
commit df930ae97e
18 changed files with 255 additions and 172 deletions

View File

@@ -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 (
<span ref={ref}>
<Button
className={classes}
{...rest}
>
<Icon />
</Button>
</span>
);
});
IconButton.defaultProps = {
className: '',
};
IconButton.propTypes = {
iconName: PropTypes.oneOf(['Chevron', 'X', 'Plus']).isRequired,
className: PropTypes.string,
};
export default IconButton;

View File

@@ -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);
}
}
}

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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 (
<div
className={classes}
onClick={typeof onClick === 'function' ? onClick : undefined}
>
<Thumbnail
size="large"
{...{
mimeType, adminThumbnail, sizes, staticURL, filename,
}}
/>
<div className={`${baseClass}__filename`}>
{filename}
</div>
</div>
);
};
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;

View File

@@ -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;
}
}

View File

@@ -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 (
<ul className={baseClass}>
{docs.map((doc, i) => {
return (
<li key={i}>
<UploadCard
{...doc}
{...{ collection }}
onClick={() => onCardClick(doc)}
/>
</li>
);
})}
</ul>
);
}
return null;
};
UploadGallery.defaultProps = {
docs: undefined,
};
UploadGallery.propTypes = {
docs: PropTypes.arrayOf(
PropTypes.shape({}),
),
collection: PropTypes.shape({}).isRequired,
onCardClick: PropTypes.func.isRequired,
};
export default UploadGallery;

View File

@@ -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);
}
}

View File

@@ -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) => {

View File

@@ -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 (
<Modal
@@ -65,7 +60,7 @@ const SelectExistingUploadModal = (props) => {
>
<MinimalTemplate width="wide">
<Form>
<header>
<header className={`${baseClass}__header`}>
<h1>
{' '}
Select existing
@@ -87,41 +82,14 @@ const SelectExistingUploadModal = (props) => {
fields,
}}
/>
{(data.docs && data.docs.length > 0) && (
<ul>
{data.docs.map((doc, i) => {
return (
<li key={i}>
doc.id
</li>
);
})}
</ul>
)}
{data.docs && data.docs.length === 0 && (
<div className={`${baseClass}__no-results`}>
<p>
No
{' '}
{pluralLabel}
{' '}
found. Either no
{' '}
{pluralLabel}
{' '}
exist yet or none match the filters you&apos;ve specified above.
</p>
<Button
onClick={() => {
toggle(addModalSlug);
}}
>
Upload new
{' '}
{singularLabel}
</Button>
</div>
)}
<UploadGallery
docs={data?.docs}
collection={collection}
onCardClick={(doc) => {
setValue(doc);
closeAll();
}}
/>
<div className={`${baseClass}__page-controls`}>
<Paginator
limit={data.limit}
@@ -132,6 +100,8 @@ const SelectExistingUploadModal = (props) => {
prevPage={data.prevPage}
nextPage={data.nextPage}
numberOfNeighbors={1}
onChange={setPage}
disableHistoryChange
/>
{data?.totalDocs > 0 && (
<div className={`${baseClass}__page-info`}>
@@ -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;

View File

@@ -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;

View File

@@ -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 (
<div className={baseClass}>
{id}
</div>
);
};
SelectedUpload.propTypes = {
collection: PropTypes.shape({
labels: PropTypes.shape({
singular: PropTypes.string,
}),
}).isRequired,
id: PropTypes.string.isRequired,
};
export default SelectedUpload;

View File

@@ -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 (
<div
className={classes}
@@ -80,12 +87,16 @@ const Upload = (props) => {
label={label}
required={required}
/>
{collection && (
{collection?.upload && (
<>
{value && (
<SelectedUpload
collection={collection}
value={value}
{internalValue && (
<FileDetails
{...collection.upload}
{...internalValue}
handleRemove={() => {
setInternalValue(undefined);
setValue(null);
}}
/>
)}
{!value && (
@@ -111,11 +122,22 @@ const Upload = (props) => {
</div>
)}
<AddModal {...{
collection, slug: addModalSlug, setValue,
collection,
slug: addModalSlug,
setValue: (val) => {
setValue(val.id);
setInternalValue(val);
},
}}
/>
<SelectExistingModal {...{
collection, slug: selectExistingModalSlug, setValue, addModalSlug,
collection,
slug: selectExistingModalSlug,
setValue: (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({}),