feat: adds admin.upload.collections[collection-name].fields to the RTE to save specific data on upload elements

This commit is contained in:
Jarrod Flesch
2022-01-19 11:21:08 -05:00
parent 7c7b546812
commit 3adf44a241
15 changed files with 655 additions and 204 deletions

View File

@@ -80,6 +80,7 @@ const RichText: React.FC<Props> = (props) => {
attributes={attributes}
element={element}
path={path}
fieldProps={props}
>
{children}
</Element>
@@ -87,7 +88,7 @@ const RichText: React.FC<Props> = (props) => {
}
return <div {...attributes}>{children}</div>;
}, [enabledElements, path]);
}, [enabledElements, path, props]);
const renderLeaf = useCallback(({ attributes, children, leaf }) => {
const matchedLeafName = Object.keys(enabledLeaves).find((leafName) => leaf[leafName]);
@@ -100,6 +101,7 @@ const RichText: React.FC<Props> = (props) => {
attributes={attributes}
leaf={leaf}
path={path}
fieldProps={props}
>
{children}
</Leaf>
@@ -109,7 +111,7 @@ const RichText: React.FC<Props> = (props) => {
return (
<span {...attributes}>{children}</span>
);
}, [enabledLeaves, path]);
}, [enabledLeaves, path, props]);
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });

View File

@@ -0,0 +1,55 @@
@import '../../../../../../../../scss/styles.scss';
.edit-upload-modal {
@include blur-bg;
display: flex;
align-items: center;
.template-minimal {
padding-top: base(4);
align-items: flex-start;
}
&__header {
margin-bottom: $baseline;
display: flex;
h1 {
margin: 0 auto 0 0;
}
.btn {
margin: 0 0 0 $baseline;
}
}
&__select-collection-wrap {
margin-bottom: base(1);
}
&__page-info {
margin-right: base(1);
margin-left: auto;
}
&__page-controls {
width: 100%;
display: flex;
align-items: center;
}
@include mid-break {
.paginator {
width: 100%;
margin-bottom: $baseline;
}
&__page-controls {
flex-wrap: wrap;
}
&__page-info {
margin-left: 0;
}
}
}

View File

@@ -0,0 +1,98 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Transforms, Element } from 'slate';
import { ReactEditor, useSlateStatic } from 'slate-react';
import { Modal } from '@faceless-ui/modal';
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
import buildStateFromSchema from '../../../../../../Form/buildStateFromSchema';
import MinimalTemplate from '../../../../../../../templates/Minimal';
import Button from '../../../../../../../elements/Button';
import RenderFields from '../../../../../../RenderFields';
import fieldTypes from '../../../../..';
import Form from '../../../../../../Form';
import reduceFieldsToValues from '../../../../../../Form/reduceFieldsToValues';
import Submit from '../../../../../../Submit';
import { Field } from '../../../../../../../../../fields/config/types';
import './index.scss';
const baseClass = 'edit-upload-modal';
type Props = {
slug: string
closeModal: () => void
relatedCollectionConfig: SanitizedCollectionConfig
fieldSchema: Field[]
element: Element & {
fields: Field[]
}
}
export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollectionConfig, fieldSchema, element }) => {
const editor = useSlateStatic();
const [initialState, setInitialState] = useState({});
const handleUpdateEditData = useCallback((fields) => {
const newNode = {
fields: reduceFieldsToValues(fields, true),
};
const elementPath = ReactEditor.findPath(editor, element);
Transforms.setNodes(
editor,
newNode,
{ at: elementPath },
);
closeModal();
}, [closeModal, editor, element]);
useEffect(() => {
const awaitInitialState = async () => {
const state = await buildStateFromSchema(fieldSchema, element?.fields);
setInitialState(state);
};
awaitInitialState();
}, [fieldSchema, element.fields]);
return (
<Modal
slug={slug}
className={baseClass}
>
<MinimalTemplate width="normal">
<header className={`${baseClass}__header`}>
<h1>
Edit
{' '}
{relatedCollectionConfig.labels.singular}
{' '}
data
</h1>
<Button
icon="x"
round
buttonStyle="icon-label"
onClick={closeModal}
/>
</header>
<div>
<Form
onSubmit={handleUpdateEditData}
initialState={initialState}
>
<RenderFields
readOnly={false}
fieldTypes={fieldTypes}
fieldSchema={fieldSchema}
/>
<Submit>
Save changes
</Submit>
</Form>
</div>
</MinimalTemplate>
</Modal>
);
};

View File

@@ -0,0 +1,55 @@
@import '../../../../../../../../scss/styles.scss';
.swap-upload-modal {
@include blur-bg;
display: flex;
align-items: center;
.template-minimal {
padding-top: base(4);
align-items: flex-start;
}
&__header {
margin-bottom: $baseline;
display: flex;
h1 {
margin: 0 auto 0 0;
}
.btn {
margin: 0 0 0 $baseline;
}
}
&__select-collection-wrap {
margin-bottom: base(1);
}
&__page-info {
margin-right: base(1);
margin-left: auto;
}
&__page-controls {
width: 100%;
display: flex;
align-items: center;
}
@include mid-break {
.paginator {
width: 100%;
margin-bottom: $baseline;
}
&__page-controls {
flex-wrap: wrap;
}
&__page-info {
margin-left: 0;
}
}
}

View File

@@ -0,0 +1,184 @@
import * as React from 'react';
import { Modal } from '@faceless-ui/modal';
import { useConfig } from '@payloadcms/config-provider';
import { Element, Transforms } from 'slate';
import { ReactEditor, useSlateStatic } from 'slate-react';
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
import usePayloadAPI from '../../../../../../../../hooks/usePayloadAPI';
import MinimalTemplate from '../../../../../../../templates/Minimal';
import Button from '../../../../../../../elements/Button';
import Label from '../../../../../../Label';
import ReactSelect from '../../../../../../../elements/ReactSelect';
import ListControls from '../../../../../../../elements/ListControls';
import UploadGallery from '../../../../../../../elements/UploadGallery';
import Paginator from '../../../../../../../elements/Paginator';
import PerPage from '../../../../../../../elements/PerPage';
import formatFields from '../../../../../../../views/collections/List/formatFields';
import './index.scss';
const baseClass = 'swap-upload-modal';
type Props = {
slug: string
element: Element
closeModal: () => void
setRelatedCollectionConfig: (collectionConfig: SanitizedCollectionConfig) => void
relatedCollectionConfig: SanitizedCollectionConfig
}
export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelatedCollectionConfig, relatedCollectionConfig, slug }) => {
const { collections, serverURL, routes: { api } } = useConfig();
const editor = useSlateStatic();
const [modalCollection, setModalCollection] = React.useState(relatedCollectionConfig);
const [modalCollectionOption, setModalCollectionOption] = React.useState<{ label: string, value: string }>({ label: relatedCollectionConfig.labels.singular, value: relatedCollectionConfig.slug });
const [availableCollections] = React.useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
const [fields, setFields] = React.useState(() => formatFields(modalCollection));
const [limit, setLimit] = React.useState<number>();
const [sort, setSort] = React.useState(null);
const [where, setWhere] = React.useState(null);
const [page, setPage] = React.useState(null);
const moreThanOneAvailableCollection = availableCollections.length > 1;
const apiURL = `${serverURL}${api}/${modalCollection.slug}`;
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
const handleUpdateUpload = React.useCallback((doc) => {
const newNode = {
type: 'upload',
value: { id: doc.id },
relationTo: modalCollection.slug,
children: [
{ text: ' ' },
],
};
const elementPath = ReactEditor.findPath(editor, element);
Transforms.setNodes(
editor,
newNode,
{ at: elementPath },
);
closeModal();
}, [closeModal, editor, element, modalCollection]);
React.useEffect(() => {
const params: {
page?: number
sort?: string
where?: unknown
limit?: number
} = {};
if (page) params.page = page;
if (where) params.where = where;
if (sort) params.sort = sort;
if (limit) params.limit = limit;
setParams(params);
}, [setParams, page, sort, where, limit]);
React.useEffect(() => {
setFields(formatFields(modalCollection));
setLimit(modalCollection.admin.pagination.defaultLimit);
}, [modalCollection]);
React.useEffect(() => {
setModalCollection(collections.find(({ slug: collectionSlug }) => modalCollectionOption.value === collectionSlug));
}, [modalCollectionOption, collections]);
return (
<Modal
className={baseClass}
slug={slug}
>
<MinimalTemplate width="wide">
<header className={`${baseClass}__header`}>
<h1>
Choose
{' '}
{modalCollection.labels.singular}
</h1>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={closeModal}
/>
</header>
{
moreThanOneAvailableCollection && (
<div className={`${baseClass}__select-collection-wrap`}>
<Label label="Select a Collection to Browse" />
<ReactSelect
className={`${baseClass}__select-collection`}
value={modalCollectionOption}
onChange={setModalCollectionOption}
options={availableCollections.map((coll) => ({ label: coll.labels.singular, value: coll.slug }))}
/>
</div>
)
}
<ListControls
collection={
{
...modalCollection,
fields,
}
}
enableColumns={false}
enableSort
modifySearchQuery={false}
handleSortChange={setSort}
handleWhereChange={setWhere}
/>
<UploadGallery
docs={data?.docs}
collection={modalCollection}
onCardClick={(doc) => {
handleUpdateUpload(doc);
setRelatedCollectionConfig(modalCollection);
closeModal();
}}
/>
<div className={`${baseClass}__page-controls`}>
<Paginator
limit={data.limit}
totalPages={data.totalPages}
page={data.page}
hasPrevPage={data.hasPrevPage}
hasNextPage={data.hasNextPage}
prevPage={data.prevPage}
nextPage={data.nextPage}
numberOfNeighbors={1}
onChange={setPage}
disableHistoryChange
/>
{data?.totalDocs > 0 && (
<React.Fragment>
<div className={`${baseClass}__page-info`}>
{data.page}
-
{data.totalPages > 1 ? data.limit : data.totalDocs}
{' '}
of
{' '}
{data.totalDocs}
</div>
<PerPage
collection={modalCollection}
limit={limit}
modifySearchParams={false}
handleChange={setLimit}
/>
</React.Fragment>
)}
</div>
</MinimalTemplate>
</Modal>
);
};

View File

@@ -4,14 +4,68 @@
max-width: base(15);
display: flex;
align-items: center;
background: $color-background-gray;
background: white;
position: relative;
&__button {
&__card {
@include soft-shadow-bottom;
display: flex;
flex-direction: column;
width: 100%;
}
&__topRow {
display: flex;
}
&__thumbnail {
width: base(3.25);
height: auto;
position: relative;
img, svg {
position: absolute;
object-fit: cover;
width: 100%;
height: 100%;
background-color: $color-dark-gray;
}
}
&__topRowRightPanel {
flex-grow: 1;
display: flex;
align-items: center;
padding: base(.75) base(1);
justify-content: space-between;
}
&__actions {
display: flex;
align-items: center;
}
&__actionButton {
margin: 0;
position: absolute;
top: base(.5);
right: base(.5);
margin-right: base(.5);
border-radius: 0;
line {
stroke-width: $style-stroke-width-m;
}
&:last-child {
margin-right: 0;
}
}
&__collectionLabel {
margin-right: base(3);
}
&__bottomRow {
padding: base(.5);
border-top: 1px solid $color-background-gray;
}
h5 {
@@ -20,17 +74,6 @@
overflow: hidden;
}
&__thumbnail {
width: base(4);
max-height: base(4);
img, svg {
object-fit: cover;
width: 100%;
height: 100%;
}
}
&__wrap {
padding: base(.5) base(.5) base(.5) base(1);
text-align: left;

View File

@@ -1,55 +1,36 @@
import React, { Fragment, useState, useEffect, useCallback } from 'react';
import { Modal, useModal } from '@faceless-ui/modal';
import React, { useState, useEffect, useCallback } from 'react';
import { useModal } from '@faceless-ui/modal';
import { Transforms } from 'slate';
import { ReactEditor, useSlateStatic, useFocused, useSelected } from 'slate-react';
import { useConfig } from '@payloadcms/config-provider';
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
import FileGraphic from '../../../../../../graphics/File';
import useThumbnail from '../../../../../../../hooks/useThumbnail';
import MinimalTemplate from '../../../../../../templates/Minimal';
import UploadGallery from '../../../../../../elements/UploadGallery';
import ListControls from '../../../../../../elements/ListControls';
import Button from '../../../../../../elements/Button';
import ReactSelect from '../../../../../../elements/ReactSelect';
import Paginator from '../../../../../../elements/Paginator';
import formatFields from '../../../../../../views/collections/List/formatFields';
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
import PerPage from '../../../../../../elements/PerPage';
import Label from '../../../../../Label';
import { SwapUploadModal } from './SwapUploadModal';
import './index.scss';
import '../modal.scss';
import { EditModal } from './EditModal';
const baseClass = 'rich-text-upload';
const baseModalClass = 'rich-text-upload-modal';
const initialParams = {
depth: 0,
};
const Element = ({ attributes, children, element, path }) => {
const Element = ({ attributes, children, element, path, fieldProps }) => {
const { relationTo, value } = element;
const { closeAll, currentModal, open } = useModal();
const { closeAll, open } = useModal();
const { collections, serverURL, routes: { api } } = useConfig();
const [availableCollections] = useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
const [renderModal, setRenderModal] = useState(false);
const [modalToRender, setModalToRender] = useState(undefined);
const [relatedCollection, setRelatedCollection] = useState<SanitizedCollectionConfig>(() => collections.find((coll) => coll.slug === relationTo));
const [modalCollectionOption, setModalCollectionOption] = useState<{ label: string, value: string}>({ label: relatedCollection.labels.singular, value: relatedCollection.slug });
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>(relatedCollection);
const [fields, setFields] = useState(() => formatFields(modalCollection));
const [limit, setLimit] = useState<number>();
const [sort, setSort] = useState(null);
const [where, setWhere] = useState(null);
const [page, setPage] = useState(null);
const editor = useSlateStatic();
const selected = useSelected();
const focused = useFocused();
const modalSlug = `${path}-edit-upload`;
const isOpen = currentModal === modalSlug;
const moreThanOneAvailableCollection = availableCollections.length > 1;
const modalSlug = `${path}-edit-upload-${modalToRender}`;
// Get the referenced document
const [{ data: upload }] = usePayloadAPI(
@@ -57,62 +38,29 @@ const Element = ({ attributes, children, element, path }) => {
{ initialParams },
);
// If modal is open, get active page of upload gallery
const apiURL = isOpen ? `${serverURL}${api}/${modalCollection.slug}` : null;
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
const thumbnailSRC = useThumbnail(relatedCollection, upload);
const handleUpdateUpload = useCallback((doc) => {
const newNode = {
type: 'upload',
value: { id: doc.id },
relationTo: modalCollection.slug,
children: [
{ text: ' ' },
],
};
const removeUpload = useCallback(() => {
const elementPath = ReactEditor.findPath(editor, element);
Transforms.setNodes(
Transforms.removeNodes(
editor,
newNode,
{ at: elementPath },
);
}, [editor, element]);
const closeModal = useCallback(() => {
closeAll();
}, [closeAll, editor, element, modalCollection]);
setModalToRender(null);
}, [closeAll]);
useEffect(() => {
setFields(formatFields(modalCollection));
setLimit(modalCollection.admin.pagination.defaultLimit);
}, [modalCollection]);
useEffect(() => {
if (renderModal && modalSlug) {
open(modalSlug);
if (modalToRender && modalSlug) {
open(`${modalSlug}`);
}
}, [renderModal, open, modalSlug]);
}, [modalToRender, open, modalSlug]);
useEffect(() => {
const params: {
page?: number
sort?: string
where?: unknown
limit?: number
} = {};
if (page) params.page = page;
if (where) params.where = where;
if (sort) params.sort = sort;
if (limit) params.limit = limit;
setParams(params);
}, [setParams, page, sort, where, limit]);
useEffect(() => {
setModalCollection(collections.find(({ slug }) => modalCollectionOption.value === slug));
}, [modalCollectionOption, collections]);
const fieldSchema = fieldProps?.admin?.upload?.collections?.[relatedCollection.slug]?.fields;
return (
<div
@@ -123,127 +71,86 @@ const Element = ({ attributes, children, element, path }) => {
contentEditable={false}
{...attributes}
>
<div className={`${baseClass}__thumbnail`}>
{thumbnailSRC && (
<img
src={thumbnailSRC}
alt={upload?.filename}
/>
)}
{!thumbnailSRC && (
<FileGraphic />
)}
</div>
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__label`}>
{relatedCollection.labels.singular}
</div>
<h5>{upload?.filename}</h5>
</div>
<Button
icon="edit"
round
buttonStyle="icon-label"
iconStyle="with-border"
className={`${baseClass}__button`}
onClick={(e) => {
e.preventDefault();
setRenderModal(true);
}}
/>
{children}
{renderModal && (
<Modal
className={baseModalClass}
slug={modalSlug}
>
{isOpen && (
<MinimalTemplate width="wide">
<header className={`${baseModalClass}__header`}>
<h1>
Choose
{' '}
{modalCollection.labels.singular}
</h1>
<div className={`${baseClass}__card`}>
<div className={`${baseClass}__topRow`}>
<div className={`${baseClass}__thumbnail`}>
{thumbnailSRC ? (
<img
src={thumbnailSRC}
alt={upload?.filename}
/>
) : (
<FileGraphic />
)}
</div>
<div className={`${baseClass}__topRowRightPanel`}>
<div className={`${baseClass}__collectionLabel`}>
{relatedCollection.labels.singular}
</div>
<div className={`${baseClass}__actions`}>
{fieldSchema && (
<Button
icon="x"
icon="edit"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={() => {
closeAll();
setRenderModal(false);
className={`${baseClass}__actionButton`}
onClick={(e) => {
e.preventDefault();
setModalToRender('edit');
}}
tooltip="Edit Upload"
/>
</header>
{moreThanOneAvailableCollection && (
<div className={`${baseModalClass}__select-collection-wrap`}>
<Label label="Select a Collection to Browse" />
<ReactSelect
className={`${baseClass}__select-collection`}
value={modalCollectionOption}
onChange={setModalCollectionOption}
options={availableCollections.map((coll) => ({ label: coll.labels.singular, value: coll.slug }))}
/>
</div>
)}
<ListControls
collection={{
...modalCollection,
fields,
<Button
icon="swap"
round
buttonStyle="icon-label"
className={`${baseClass}__actionButton`}
onClick={(e) => {
e.preventDefault();
setModalToRender('swap');
}}
enableColumns={false}
enableSort
modifySearchQuery={false}
handleSortChange={setSort}
handleWhereChange={setWhere}
tooltip="Swap Upload"
/>
<UploadGallery
docs={data?.docs}
collection={modalCollection}
onCardClick={(doc) => {
handleUpdateUpload(doc);
setRelatedCollection(modalCollection);
setRenderModal(false);
closeAll();
<Button
icon="x"
round
buttonStyle="icon-label"
className={`${baseClass}__actionButton`}
onClick={(e) => {
e.preventDefault();
removeUpload();
}}
tooltip="Remove Upload"
/>
<div className={`${baseModalClass}__page-controls`}>
<Paginator
limit={data.limit}
totalPages={data.totalPages}
page={data.page}
hasPrevPage={data.hasPrevPage}
hasNextPage={data.hasNextPage}
prevPage={data.prevPage}
nextPage={data.nextPage}
numberOfNeighbors={1}
onChange={setPage}
disableHistoryChange
/>
{data?.totalDocs > 0 && (
<Fragment>
<div className={`${baseModalClass}__page-info`}>
{data.page}
-
{data.totalPages > 1 ? data.limit : data.totalDocs}
{' '}
of
{' '}
{data.totalDocs}
</div>
<PerPage
collection={modalCollection}
limit={limit}
modifySearchParams={false}
handleChange={setLimit}
/>
</Fragment>
)}
</div>
</MinimalTemplate>
)}
</Modal>
</div>
</div>
</div>
<div className={`${baseClass}__bottomRow`}>
<h5>{upload?.filename}</h5>
</div>
</div>
{children}
{modalToRender === 'swap' && (
<SwapUploadModal
slug={modalSlug}
element={element}
closeModal={closeModal}
setRelatedCollectionConfig={setRelatedCollection}
relatedCollectionConfig={relatedCollection}
/>
)}
{(modalToRender === 'edit' && fieldSchema) && (
<EditModal
slug={modalSlug}
closeModal={closeModal}
relatedCollectionConfig={relatedCollection}
fieldSchema={fieldSchema}
element={element}
/>
)}
</div>
);