feat: enhances rich text upload with custom field API
* feat: adds admin.upload.collections[collection-name].fields to the RTE to save specific data on upload elements * chore: renames flatten to unflatten in reduceFieldsToValues, disables automatic arrow function return in eslint * docs: adds documentation for upload.collections[collection-name].fields feature * feat: adds recursion to richText field to populate relationship and upload nested fields * chore: removes unused css * fix: import path for createRichTextRelationshipPromise * docs: updates docs to include images for the RTE upload docs
This commit is contained in:
@@ -20,6 +20,26 @@
|
||||
@include color-svg(currentColor);
|
||||
}
|
||||
|
||||
&--has-tooltip {
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
.btn__tooltip {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translate(-50%, -10px);
|
||||
}
|
||||
|
||||
.btn__content {
|
||||
&:hover {
|
||||
.btn__tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--icon-style-without-border {
|
||||
.btn__icon {
|
||||
border: none;
|
||||
|
||||
@@ -6,6 +6,8 @@ import plus from '../../icons/Plus';
|
||||
import x from '../../icons/X';
|
||||
import chevron from '../../icons/Chevron';
|
||||
import edit from '../../icons/Edit';
|
||||
import swap from '../../icons/Swap';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -14,15 +16,21 @@ const icons = {
|
||||
x,
|
||||
chevron,
|
||||
edit,
|
||||
swap,
|
||||
};
|
||||
|
||||
const baseClass = 'btn';
|
||||
|
||||
const ButtonContents = ({ children, icon }) => {
|
||||
const ButtonContents = ({ children, icon, tooltip }) => {
|
||||
const BuiltInIcon = icons[icon];
|
||||
|
||||
return (
|
||||
<span className={`${baseClass}__content`}>
|
||||
{tooltip && (
|
||||
<Tooltip className={`${baseClass}__tooltip`}>
|
||||
{tooltip}
|
||||
</Tooltip>
|
||||
)}
|
||||
{children && (
|
||||
<span className={`${baseClass}__label`}>
|
||||
{children}
|
||||
@@ -55,6 +63,7 @@ const Button: React.FC<Props> = (props) => {
|
||||
size = 'medium',
|
||||
iconPosition = 'right',
|
||||
newTab,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
const classes = [
|
||||
@@ -68,6 +77,7 @@ const Button: React.FC<Props> = (props) => {
|
||||
round && `${baseClass}--round`,
|
||||
size && `${baseClass}--size-${size}`,
|
||||
iconPosition && `${baseClass}--icon-position-${iconPosition}`,
|
||||
tooltip && `${baseClass}--has-tooltip`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
function handleClick(event) {
|
||||
@@ -90,7 +100,10 @@ const Button: React.FC<Props> = (props) => {
|
||||
{...buttonProps}
|
||||
to={to || url}
|
||||
>
|
||||
<ButtonContents icon={icon}>
|
||||
<ButtonContents
|
||||
icon={icon}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
{children}
|
||||
</ButtonContents>
|
||||
</Link>
|
||||
@@ -102,7 +115,10 @@ const Button: React.FC<Props> = (props) => {
|
||||
{...buttonProps}
|
||||
href={url}
|
||||
>
|
||||
<ButtonContents icon={icon}>
|
||||
<ButtonContents
|
||||
icon={icon}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
{children}
|
||||
</ButtonContents>
|
||||
</a>
|
||||
@@ -114,7 +130,10 @@ const Button: React.FC<Props> = (props) => {
|
||||
type="submit"
|
||||
{...buttonProps}
|
||||
>
|
||||
<ButtonContents icon={icon}>
|
||||
<ButtonContents
|
||||
icon={icon}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
{children}
|
||||
</ButtonContents>
|
||||
</button>
|
||||
|
||||
@@ -16,4 +16,5 @@ export type Props = {
|
||||
size?: 'small' | 'medium',
|
||||
iconPosition?: 'left' | 'right',
|
||||
newTab?: boolean
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
line-height: base(.75);
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
border-radius: 2px;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { unflatten } from 'flatley';
|
||||
import { unflatten as flatleyUnflatten } from 'flatley';
|
||||
import { Fields, Data } from './types';
|
||||
|
||||
const reduceFieldsToValues = (fields: Fields, flatten?: boolean): Data => {
|
||||
const reduceFieldsToValues = (fields: Fields, unflatten?: boolean): Data => {
|
||||
const data = {};
|
||||
|
||||
Object.keys(fields).forEach((key) => {
|
||||
@@ -14,8 +14,8 @@ const reduceFieldsToValues = (fields: Fields, flatten?: boolean): Data => {
|
||||
}
|
||||
});
|
||||
|
||||
if (flatten) {
|
||||
const unflattened = unflatten(data, { safe: true });
|
||||
if (unflatten) {
|
||||
const unflattened = flatleyUnflatten(data, { safe: true });
|
||||
return unflattened;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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="Upload Fields"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -107,6 +107,10 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m $color-green;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin soft-shadow-bottom {
|
||||
box-shadow: 0 7px 14px 0px rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// STYLE MIXINS
|
||||
//////////////////////////////
|
||||
|
||||
@@ -263,6 +263,11 @@ export const richText = baseField.keys({
|
||||
),
|
||||
),
|
||||
hideGutter: joi.boolean().default(false),
|
||||
upload: joi.object({
|
||||
collections: joi.object().pattern(joi.string(), joi.object().keys({
|
||||
fields: joi.array().items(joi.link('#field')),
|
||||
})),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -223,6 +223,13 @@ export type RichTextField = FieldBase & {
|
||||
elements?: RichTextElement[];
|
||||
leaves?: RichTextLeaf[];
|
||||
hideGutter?: boolean;
|
||||
upload?: {
|
||||
collections: {
|
||||
[collection: string]: {
|
||||
fields: Field[];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
src/fields/richText/populate.ts
Normal file
54
src/fields/richText/populate.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { Collection } from '../../collections/config/types';
|
||||
import { Payload } from '../..';
|
||||
import { RichTextField, Field } from '../config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
|
||||
type Arguments = {
|
||||
data: unknown
|
||||
overrideAccess?: boolean
|
||||
depth: number
|
||||
currentDepth?: number
|
||||
payload: Payload
|
||||
field: RichTextField
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
export const populate = async ({
|
||||
id,
|
||||
collection,
|
||||
data,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}: Omit<Arguments, 'field'> & {
|
||||
id: string,
|
||||
field: Field
|
||||
collection: Collection
|
||||
}): Promise<void> => {
|
||||
let dataRef = data as Record<string, unknown>;
|
||||
|
||||
const doc = await payload.operations.collections.findByID({
|
||||
req: {
|
||||
...req,
|
||||
payloadAPI: 'local',
|
||||
},
|
||||
collection,
|
||||
id,
|
||||
currentDepth: currentDepth + 1,
|
||||
overrideAccess,
|
||||
disableErrors: true,
|
||||
depth,
|
||||
showHiddenFields,
|
||||
});
|
||||
|
||||
if (doc) {
|
||||
dataRef = doc;
|
||||
} else {
|
||||
dataRef = null;
|
||||
}
|
||||
};
|
||||
183
src/fields/richText/recurseNestedFields.ts
Normal file
183
src/fields/richText/recurseNestedFields.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { Payload } from '../..';
|
||||
import { Field, fieldHasSubFields, fieldIsArrayType, fieldAffectsData } from '../config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { populate } from './populate';
|
||||
import { recurseRichText } from './relationshipPromise';
|
||||
|
||||
type NestedRichTextFieldsArgs = {
|
||||
promises: Promise<void>[]
|
||||
data: unknown
|
||||
fields: Field[]
|
||||
req: PayloadRequest
|
||||
payload: Payload
|
||||
overrideAccess: boolean
|
||||
depth: number
|
||||
currentDepth?: number
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
export const recurseNestedFields = ({
|
||||
promises,
|
||||
data,
|
||||
fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess = false,
|
||||
depth,
|
||||
currentDepth = 0,
|
||||
showHiddenFields,
|
||||
}: NestedRichTextFieldsArgs): void => {
|
||||
fields.forEach((field) => {
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
if (field.type === 'relationship') {
|
||||
if (field.hasMany && Array.isArray(data[field.name])) {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
data[field.name].forEach(({ relationTo, value }, i) => {
|
||||
const collection = payload.collections[relationTo];
|
||||
if (collection) {
|
||||
promises.push(populate({
|
||||
id: value,
|
||||
field,
|
||||
collection,
|
||||
data: data[field.name][i],
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
data[field.name].forEach((id, i) => {
|
||||
const collection = payload.collections[field.relationTo as string];
|
||||
if (collection) {
|
||||
promises.push(populate({
|
||||
id,
|
||||
field,
|
||||
collection,
|
||||
data: data[field.name][i],
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (Array.isArray(field.relationTo) && data[field.name]?.value && data[field.name]?.relationTo) {
|
||||
const collection = payload.collections[data[field.name].relationTo];
|
||||
promises.push(populate({
|
||||
id: data[field.name].value,
|
||||
field,
|
||||
collection,
|
||||
data: data[field.name].value,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
} else if (typeof data[field.name] !== undefined) {
|
||||
const collection = payload.collections[field.relationTo];
|
||||
promises.push(populate({
|
||||
id: data[field.name],
|
||||
field,
|
||||
collection,
|
||||
data: data[field.name],
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
} else if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
|
||||
if (fieldAffectsData(field) && typeof data[field.name] === 'object') {
|
||||
recurseNestedFields({
|
||||
promises,
|
||||
data: data[field.name],
|
||||
fields: field.fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
showHiddenFields,
|
||||
});
|
||||
} else {
|
||||
recurseNestedFields({
|
||||
promises,
|
||||
data,
|
||||
fields: field.fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
if (field.type === 'blocks') {
|
||||
data[field.name].forEach((row, i) => {
|
||||
const block = field.blocks.find(({ slug }) => slug === row?.blockType);
|
||||
if (block) {
|
||||
recurseNestedFields({
|
||||
promises,
|
||||
data: data[field.name][i],
|
||||
fields: block.fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (field.type === 'array') {
|
||||
data[field.name].forEach((_, i) => {
|
||||
recurseNestedFields({
|
||||
promises,
|
||||
data: data[field.name][i],
|
||||
fields: field.fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
showHiddenFields,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'richText' && Array.isArray(data[field.name])) {
|
||||
data[field.name].forEach((node) => {
|
||||
if (Array.isArray(node.children)) {
|
||||
recurseRichText({
|
||||
req,
|
||||
children: node.children,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
field,
|
||||
promises,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Collection } from '../collections/config/types';
|
||||
import { Payload } from '..';
|
||||
import { RichTextField } from './config/types';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import { Payload } from '../..';
|
||||
import { RichTextField } from '../config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { recurseNestedFields } from './recurseNestedFields';
|
||||
import { populate } from './populate';
|
||||
|
||||
type Arguments = {
|
||||
data: unknown
|
||||
@@ -26,44 +27,7 @@ type RecurseRichTextArgs = {
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
const populate = async ({
|
||||
id,
|
||||
collection,
|
||||
data,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}: Arguments & {
|
||||
id: string,
|
||||
collection: Collection
|
||||
}) => {
|
||||
const dataRef = data as Record<string, unknown>;
|
||||
|
||||
const doc = await payload.operations.collections.findByID({
|
||||
req: {
|
||||
...req,
|
||||
payloadAPI: 'local',
|
||||
},
|
||||
collection,
|
||||
id,
|
||||
currentDepth: currentDepth + 1,
|
||||
overrideAccess,
|
||||
disableErrors: true,
|
||||
depth,
|
||||
showHiddenFields,
|
||||
});
|
||||
|
||||
if (doc) {
|
||||
dataRef.value = doc;
|
||||
} else {
|
||||
dataRef.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const recurseRichText = ({
|
||||
export const recurseRichText = ({
|
||||
req,
|
||||
children,
|
||||
payload,
|
||||
@@ -73,7 +37,7 @@ const recurseRichText = ({
|
||||
field,
|
||||
promises,
|
||||
showHiddenFields,
|
||||
}: RecurseRichTextArgs) => {
|
||||
}: RecurseRichTextArgs): void => {
|
||||
if (Array.isArray(children)) {
|
||||
(children as any[]).forEach((element) => {
|
||||
const collection = payload.collections[element?.relationTo];
|
||||
@@ -82,10 +46,23 @@ const recurseRichText = ({
|
||||
&& element?.value?.id
|
||||
&& collection
|
||||
&& (depth && currentDepth <= depth)) {
|
||||
if (element.type === 'upload' && Array.isArray(field.admin?.upload?.collections?.[element?.relationTo]?.fields)) {
|
||||
recurseNestedFields({
|
||||
promises,
|
||||
data: element.fields || {},
|
||||
fields: field.admin.upload.collections[element.relationTo].fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
promises.push(populate({
|
||||
req,
|
||||
id: element.value.id,
|
||||
data: element,
|
||||
data: element.value,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
@@ -5,7 +5,7 @@ import { Field, fieldHasSubFields, fieldIsArrayType, fieldIsBlockType, fieldAffe
|
||||
import { Operation } from '../types';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import { Payload } from '..';
|
||||
import richTextRelationshipPromise from './richTextRelationshipPromise';
|
||||
import richTextRelationshipPromise from './richText/relationshipPromise';
|
||||
|
||||
type Arguments = {
|
||||
fields: Field[]
|
||||
@@ -28,7 +28,7 @@ type Arguments = {
|
||||
fullOriginalDoc: Record<string, any>
|
||||
fullData: Record<string, any>
|
||||
validationPromises: (() => Promise<string | boolean>)[]
|
||||
errors: {message: string, field: string}[]
|
||||
errors: { message: string, field: string }[]
|
||||
payload: Payload
|
||||
showHiddenFields: boolean
|
||||
unflattenLocales: boolean
|
||||
@@ -96,7 +96,7 @@ const traverseFields = (args: Arguments): void => {
|
||||
}
|
||||
|
||||
if ((field.type === 'upload' || field.type === 'relationship')
|
||||
&& (data[field.name] === '' || data[field.name] === 'none' || data[field.name] === 'null')) {
|
||||
&& (data[field.name] === '' || data[field.name] === 'none' || data[field.name] === 'null')) {
|
||||
if (field.type === 'relationship' && field.hasMany === true) {
|
||||
dataCopy[field.name] = [];
|
||||
} else {
|
||||
@@ -304,7 +304,7 @@ const traverseFields = (args: Arguments): void => {
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (Array.isArray(dataCopy[field.name])) {
|
||||
dataCopy[field.name].forEach((relatedDoc: {value: unknown, relationTo: string}, i) => {
|
||||
dataCopy[field.name].forEach((relatedDoc: { value: unknown, relationTo: string }, i) => {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === relatedDoc.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
|
||||
@@ -20,7 +20,7 @@ import combineParentName from '../utilities/combineParentName';
|
||||
import withNullableType from './withNullableType';
|
||||
import { BaseFields } from '../../collections/graphql/types';
|
||||
import { toWords } from '../../utilities/formatLabels';
|
||||
import createRichTextRelationshipPromise from '../../fields/richTextRelationshipPromise';
|
||||
import createRichTextRelationshipPromise from '../../fields/richText/relationshipPromise';
|
||||
|
||||
type LocaleInputType = {
|
||||
locale: {
|
||||
|
||||
Reference in New Issue
Block a user