implements upload field type with ability to select existing upload
This commit is contained in:
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
69
src/client/components/elements/UploadCard/index.js
Normal file
69
src/client/components/elements/UploadCard/index.js
Normal 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;
|
||||
19
src/client/components/elements/UploadCard/index.scss
Normal file
19
src/client/components/elements/UploadCard/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
46
src/client/components/elements/UploadGallery/index.js
Normal file
46
src/client/components/elements/UploadGallery/index.js
Normal 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;
|
||||
12
src/client/components/elements/UploadGallery/index.scss
Normal file
12
src/client/components/elements/UploadGallery/index.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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'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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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({}),
|
||||
|
||||
Reference in New Issue
Block a user