diff --git a/demo/collections/AutoLabel.ts b/demo/collections/AutoLabel.ts
index ffd4ca99df..23ad49fae5 100644
--- a/demo/collections/AutoLabel.ts
+++ b/demo/collections/AutoLabel.ts
@@ -1,7 +1,12 @@
import { PayloadCollectionConfig } from '../../src/collections/config/types';
+import { relationship } from '../../src/fields/config/schema';
const AutoLabel: PayloadCollectionConfig = {
slug: 'auto-label',
+ admin: {
+ useAsTitle: 'autoLabelField',
+ enableRichTextRelationship: true,
+ },
fields: [
{
name: 'autoLabelField',
@@ -17,6 +22,11 @@ const AutoLabel: PayloadCollectionConfig = {
type: 'text',
label: 'Custom Label',
},
+ {
+ name: 'testRelationship',
+ type: 'relationship',
+ relationTo: 'all-fields',
+ },
{
name: 'specialBlock',
type: 'blocks',
diff --git a/demo/collections/RelationshipB.ts b/demo/collections/RelationshipB.ts
index faa780fd0e..f8203f349b 100644
--- a/demo/collections/RelationshipB.ts
+++ b/demo/collections/RelationshipB.ts
@@ -22,7 +22,7 @@ const RelationshipB: PayloadCollectionConfig = {
name: 'postManyRelationships',
label: 'Post Many Relationships',
type: 'relationship',
- relationTo: ['relationship-a'],
+ relationTo: ['relationship-a', 'media'],
localized: true,
hasMany: false,
},
diff --git a/demo/collections/RichText.ts b/demo/collections/RichText.ts
index 1405bd7d87..632e8bbef0 100644
--- a/demo/collections/RichText.ts
+++ b/demo/collections/RichText.ts
@@ -8,6 +8,9 @@ const RichText: PayloadCollectionConfig = {
singular: 'Rich Text',
plural: 'Rich Texts',
},
+ access: {
+ read: () => true,
+ },
fields: [
{
name: 'defaultRichText',
diff --git a/docs/fields/rich-text.mdx b/docs/fields/rich-text.mdx
index f2a3d499eb..008f705027 100644
--- a/docs/fields/rich-text.mdx
+++ b/docs/fields/rich-text.mdx
@@ -80,6 +80,8 @@ The built-in `relationship` element is a powerful way to reference other Documen
To enable collections to be selected within the Rich Text relationship, you need to enable the collection admin option of enableRichTextRelationship.
+Relationships are populated dynamically into your Rich Text field' content. Within the REST and Local APIs, any present RichText `relationship` elements will respect the `depth` option that you pass, and will be populated accordingly. In GraphQL, each `richText` field accepts an argument of `depth` for you to utilize.
+
### Specifying which elements and leaves to allow
To specify which default elements or leaves should be allowed to be used for this field, define arrays that contain string names for each element or leaf you wish to enable. To specify a custom element or leaf, pass an object with all corresponding properties as outlined below. View the [example](#example) to reference how this all works.
diff --git a/src/admin/components/forms/field-types/Relationship/index.tsx b/src/admin/components/forms/field-types/Relationship/index.tsx
index 163ab80c3a..5b9dc567f3 100644
--- a/src/admin/components/forms/field-types/Relationship/index.tsx
+++ b/src/admin/components/forms/field-types/Relationship/index.tsx
@@ -246,19 +246,44 @@ const Relationship: React.FC = (props) => {
useEffect(() => {
const getFirstResults = async () => {
- const relation = relations[0];
- const res = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&depth=0`);
+ const res = await fetch(`${serverURL}${api}/${relations[0]}?limit=${maxResultsPerRequest}&depth=0`);
if (res.ok) {
const data: PaginatedDocs = await res.json();
- addOptions(data, relation);
+ addOptions(data, relations[0]);
if (!data.hasNextPage) {
- setLastFullyLoadedRelation(relations.indexOf(relation));
- } else {
- setLastLoadedPage(2);
+ setLastFullyLoadedRelation(0);
+
+ if (relations[1]) {
+ const secondRes = await fetch(`${serverURL}${api}/${relations[1]}?limit=${maxResultsPerRequest}&depth=0`);
+
+ if (res.ok) {
+ const secondData: PaginatedDocs = await secondRes.json();
+
+ addOptions(secondData, relations[1]);
+
+ if (!secondData.hasNextPage) {
+ setLastFullyLoadedRelation(1);
+
+ if (relations[2]) {
+ const thirdRes = await fetch(`${serverURL}${api}/${relations[2]}?limit=${maxResultsPerRequest}&depth=0`);
+
+ if (res.ok) {
+ const thirdData: PaginatedDocs = await thirdRes.json();
+
+ addOptions(thirdData, relations[2]);
+
+ if (!thirdData.hasNextPage) {
+ setLastFullyLoadedRelation(2);
+ }
+ }
+ }
+ }
+ }
+ }
}
setHasLoadedFirstOptions(true);
diff --git a/src/admin/components/forms/field-types/RichText/elements/relationship/Button/Fields/index.tsx b/src/admin/components/forms/field-types/RichText/elements/relationship/Button/Fields/index.tsx
index 95a9b6b978..6e0ca0fb6f 100644
--- a/src/admin/components/forms/field-types/RichText/elements/relationship/Button/Fields/index.tsx
+++ b/src/admin/components/forms/field-types/RichText/elements/relationship/Button/Fields/index.tsx
@@ -2,10 +2,8 @@ import React, { Fragment, useState, useEffect } from 'react';
import { useConfig, useAuth } from '@payloadcms/config-provider';
import { useWatchForm } from '../../../../../../Form/context';
import Relationship from '../../../../../Relationship';
-import Number from '../../../../../Number';
import Select from '../../../../../Select';
-
const createOptions = (collections, permissions) => collections.reduce((options, collection) => {
if (permissions?.collections?.[collection.slug]?.read?.permission && collection?.admin?.enableRichTextRelationship) {
return [
@@ -21,7 +19,7 @@ const createOptions = (collections, permissions) => collections.reduce((options,
}, []);
const RelationshipFields = () => {
- const { collections, maxDepth } = useConfig();
+ const { collections } = useConfig();
const { permissions } = useAuth();
const [options, setOptions] = useState(() => createOptions(collections, permissions));
@@ -49,13 +47,6 @@ const RelationshipFields = () => {
required
/>
)}
-
);
};
diff --git a/src/admin/components/forms/field-types/RichText/elements/relationship/Button/index.tsx b/src/admin/components/forms/field-types/RichText/elements/relationship/Button/index.tsx
index 53e7b60eee..c5f532a1fb 100644
--- a/src/admin/components/forms/field-types/RichText/elements/relationship/Button/index.tsx
+++ b/src/admin/components/forms/field-types/RichText/elements/relationship/Button/index.tsx
@@ -15,19 +15,16 @@ import { requests } from '../../../../../../../api';
import './index.scss';
-const initialFormData = {
- depth: 0,
-};
+const initialFormData = {};
const baseClass = 'relationship-rich-text-button';
-const insertRelationship = (editor, { value, relationTo, depth }) => {
+const insertRelationship = (editor, { value, relationTo }) => {
const text = { text: ' ' };
const relationship = {
type: 'relationship',
value,
- depth,
relationTo,
children: [
text,
@@ -58,13 +55,13 @@ const RelationshipButton: React.FC<{path: string}> = ({ path }) => {
const [hasEnabledCollections] = useState(() => collections.find(({ admin: { enableRichTextRelationship } }) => enableRichTextRelationship));
const modalSlug = `${path}-add-relationship`;
- const handleAddRelationship = useCallback(async (_, { relationTo, value, depth }) => {
+ const handleAddRelationship = useCallback(async (_, { relationTo, value }) => {
setLoading(true);
- const res = await requests.get(`${serverURL}${api}/${relationTo}/${value}?depth=${depth}`);
+ const res = await requests.get(`${serverURL}${api}/${relationTo}/${value}?depth=0`);
const json = await res.json();
- insertRelationship(editor, { value: json, depth, relationTo });
+ insertRelationship(editor, { value: { id: json.id }, relationTo });
closeAll();
setRenderModal(false);
setLoading(false);
diff --git a/src/admin/components/forms/field-types/RichText/elements/relationship/Element/index.scss b/src/admin/components/forms/field-types/RichText/elements/relationship/Element/index.scss
index a00cbdf938..3da21a911a 100644
--- a/src/admin/components/forms/field-types/RichText/elements/relationship/Element/index.scss
+++ b/src/admin/components/forms/field-types/RichText/elements/relationship/Element/index.scss
@@ -6,7 +6,7 @@
align-items: flex-start;
background: $color-background-gray;
max-width: base(15);
- margin-bottom: $baseline;
+ margin-bottom: base(.5);
svg {
width: base(1.25);
diff --git a/src/admin/components/forms/field-types/RichText/elements/relationship/Element/index.tsx b/src/admin/components/forms/field-types/RichText/elements/relationship/Element/index.tsx
index 558fe66f13..b118e76da1 100644
--- a/src/admin/components/forms/field-types/RichText/elements/relationship/Element/index.tsx
+++ b/src/admin/components/forms/field-types/RichText/elements/relationship/Element/index.tsx
@@ -1,16 +1,26 @@
import React, { useState } from 'react';
import { useConfig } from '@payloadcms/config-provider';
import RelationshipIcon from '../../../../../../icons/Relationship';
+import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
import './index.scss';
const baseClass = 'rich-text-relationship';
+const initialParams = {
+ depth: 0,
+};
+
const Element = ({ attributes, children, element }) => {
const { relationTo, value } = element;
- const { collections } = useConfig();
+ const { collections, serverURL, routes: { api } } = useConfig();
const [relatedCollection] = useState(() => collections.find((coll) => coll.slug === relationTo));
+ const [{ data }] = usePayloadAPI(
+ `${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
+ { initialParams },
+ );
+
return (
{
{' '}
Relationship
- {value[relatedCollection?.admin?.useAsTitle || 'id']}
+ {data[relatedCollection?.admin?.useAsTitle || 'id']}
{children}
diff --git a/src/auth/defaultAccess.ts b/src/auth/defaultAccess.ts
new file mode 100644
index 0000000000..59ae04c959
--- /dev/null
+++ b/src/auth/defaultAccess.ts
@@ -0,0 +1,3 @@
+import { PayloadRequest } from '../express/types';
+
+export default ({ req: { user } }: { req: PayloadRequest}): boolean => Boolean(user);
diff --git a/src/collections/config/defaults.ts b/src/collections/config/defaults.ts
index 757dbe921f..f8ed9fc6e4 100644
--- a/src/collections/config/defaults.ts
+++ b/src/collections/config/defaults.ts
@@ -1,8 +1,12 @@
-import { PayloadRequest } from '../../express/types';
+import defaultAccess from '../../auth/defaultAccess';
export const defaults = {
access: {
- unlock: ({ req: { user } }: { req: PayloadRequest}): boolean => Boolean(user),
+ create: defaultAccess,
+ read: defaultAccess,
+ update: defaultAccess,
+ delete: defaultAccess,
+ unlock: defaultAccess,
},
timestamps: true,
admin: {
diff --git a/src/collections/operations/findByID.ts b/src/collections/operations/findByID.ts
index ec3203378a..94a1759acc 100644
--- a/src/collections/operations/findByID.ts
+++ b/src/collections/operations/findByID.ts
@@ -57,6 +57,10 @@ async function findByID(incomingArgs: Arguments): Promise {
// /////////////////////////////////////
const accessResults = !overrideAccess ? await executeAccess({ req, disableErrors, id }, collectionConfig.access.read) : true;
+
+ // If errors are disabled, and access returns false, return null
+ if (accessResults === false) return null;
+
const hasWhereAccess = typeof accessResults === 'object';
const queryToBuild: { where: Where } = {
diff --git a/src/fields/relationshipPopulationPromise.ts b/src/fields/relationshipPopulationPromise.ts
index 4537d6986c..450c913af5 100644
--- a/src/fields/relationshipPopulationPromise.ts
+++ b/src/fields/relationshipPopulationPromise.ts
@@ -3,7 +3,6 @@ import executeAccess from '../auth/executeAccess';
import { Field, RelationshipField, fieldSupportsMany } from './config/types';
import { Payload } from '..';
-
type PopulateArgs = {
depth: number
currentDepth: number
diff --git a/src/fields/richTextRelationshipPromise.ts b/src/fields/richTextRelationshipPromise.ts
new file mode 100644
index 0000000000..aa213b7194
--- /dev/null
+++ b/src/fields/richTextRelationshipPromise.ts
@@ -0,0 +1,134 @@
+import { Collection } from '../collections/config/types';
+import { Payload } from '..';
+import { RichTextField } from './config/types';
+import { PayloadRequest } from '../express/types';
+
+type Arguments = {
+ data: unknown
+ overrideAccess?: boolean
+ depth: number
+ currentDepth?: number
+ payload: Payload
+ field: RichTextField
+ req: PayloadRequest
+}
+
+type RecurseRichTextArgs = {
+ children: unknown[]
+ overrideAccess: boolean
+ depth: number
+ currentDepth: number
+ payload: Payload
+ field: RichTextField
+ req: PayloadRequest
+ promises: Promise[]
+}
+
+const populate = async ({
+ id,
+ collection,
+ data,
+ overrideAccess,
+ depth,
+ currentDepth,
+ payload,
+ req,
+}: Arguments & {
+ id: string,
+ collection: Collection
+}) => {
+ const dataRef = data as Record;
+
+ const doc = await payload.operations.collections.findByID({
+ req: {
+ ...req,
+ payloadAPI: 'local',
+ },
+ collection,
+ id,
+ currentDepth: currentDepth + 1,
+ overrideAccess,
+ disableErrors: true,
+ depth,
+ });
+
+ if (doc) {
+ dataRef.value = doc;
+ } else {
+ dataRef.value = null;
+ }
+};
+
+const recurseRichText = ({
+ req,
+ children,
+ payload,
+ overrideAccess = false,
+ depth,
+ currentDepth = 0,
+ field,
+ promises,
+}: RecurseRichTextArgs) => {
+ if (Array.isArray(children)) {
+ (children as any[]).forEach((element) => {
+ const collection = payload.collections[element?.relationTo];
+
+ if (element.type === 'relationship'
+ && element?.value?.id
+ && collection
+ && (depth && currentDepth <= depth)) {
+ promises.push(populate({
+ req,
+ id: element.value.id,
+ data: element,
+ overrideAccess,
+ depth,
+ currentDepth,
+ payload,
+ field,
+ collection,
+ }));
+ }
+
+ if (element?.children) {
+ recurseRichText({
+ req,
+ children: element.children,
+ payload,
+ overrideAccess,
+ depth,
+ currentDepth,
+ field,
+ promises,
+ });
+ }
+ });
+ }
+};
+
+const richTextRelationshipPromise = ({
+ req,
+ data,
+ payload,
+ overrideAccess,
+ depth,
+ currentDepth,
+ field,
+}: Arguments) => async (): Promise => {
+ const promises = [];
+
+ recurseRichText({
+ req,
+ children: data[field.name],
+ payload,
+ overrideAccess,
+ depth,
+ currentDepth,
+ field,
+ promises,
+ });
+
+ await Promise.all(promises);
+};
+
+export default richTextRelationshipPromise;
diff --git a/src/fields/traverseFields.ts b/src/fields/traverseFields.ts
index ec2375889d..608088577f 100644
--- a/src/fields/traverseFields.ts
+++ b/src/fields/traverseFields.ts
@@ -5,6 +5,7 @@ import { Field, fieldHasSubFields, fieldIsArrayType, fieldIsBlockType, HookName
import { Operation } from '../types';
import { PayloadRequest } from '../express/types';
import { Payload } from '..';
+import richTextRelationshipPromise from './richTextRelationshipPromise';
type Arguments = {
fields: Field[]
@@ -91,8 +92,22 @@ const traverseFields = (args: Arguments): void => {
if (data[field.name] === '') dataCopy[field.name] = false;
}
- if (field.type === 'richText' && typeof data[field.name] === 'string') {
- dataCopy[field.name] = JSON.parse(data[field.name] as string);
+ if (field.type === 'richText') {
+ if (typeof data[field.name] === 'string') {
+ dataCopy[field.name] = JSON.parse(data[field.name] as string);
+ }
+
+ if ((field.admin?.elements?.includes('relationship') || !field?.admin?.elements) && hook === 'afterRead') {
+ relationshipPopulations.push(richTextRelationshipPromise({
+ req,
+ data,
+ payload,
+ overrideAccess,
+ depth,
+ field,
+ currentDepth,
+ }));
+ }
}
const hasLocalizedValue = (typeof data?.[field.name] === 'object' && data?.[field.name] !== null)
diff --git a/src/globals/config/sanitize.ts b/src/globals/config/sanitize.ts
index d3fdb518ff..eb0d286d2e 100644
--- a/src/globals/config/sanitize.ts
+++ b/src/globals/config/sanitize.ts
@@ -2,6 +2,7 @@ import { toWords } from '../../utilities/formatLabels';
import { PayloadCollectionConfig } from '../../collections/config/types';
import sanitizeFields from '../../fields/config/sanitize';
import { PayloadGlobalConfig, GlobalConfig } from './types';
+import defaultAccess from '../../auth/defaultAccess';
const sanitizeGlobals = (collections: PayloadCollectionConfig[], globals: PayloadGlobalConfig[]): GlobalConfig[] => {
const sanitizedGlobals = globals.map((global) => {
@@ -17,6 +18,9 @@ const sanitizeGlobals = (collections: PayloadCollectionConfig[], globals: Payloa
if (!sanitizedGlobal.access) sanitizedGlobal.access = {};
if (!sanitizedGlobal.admin) sanitizedGlobal.admin = {};
+ if (!sanitizedGlobal.access.read) sanitizedGlobal.access.read = defaultAccess;
+ if (!sanitizedGlobal.access.update) sanitizedGlobal.access.update = defaultAccess;
+
if (!sanitizedGlobal.hooks.beforeValidate) sanitizedGlobal.hooks.beforeValidate = [];
if (!sanitizedGlobal.hooks.beforeChange) sanitizedGlobal.hooks.beforeChange = [];
if (!sanitizedGlobal.hooks.afterChange) sanitizedGlobal.hooks.afterChange = [];
diff --git a/src/graphql/schema/buildObjectType.ts b/src/graphql/schema/buildObjectType.ts
index f39786b5a7..a9db52ed35 100644
--- a/src/graphql/schema/buildObjectType.ts
+++ b/src/graphql/schema/buildObjectType.ts
@@ -13,12 +13,13 @@ import {
GraphQLUnionType,
} from 'graphql';
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars';
-import { Field, RadioField, RelationshipField, SelectField, UploadField, optionIsObject, ArrayField, GroupField, BlockField, RowField } from '../../fields/config/types';
+import { Field, RadioField, RelationshipField, SelectField, UploadField, optionIsObject, ArrayField, GroupField, RichTextField } from '../../fields/config/types';
import formatName from '../utilities/formatName';
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';
type LocaleInputType = {
locale: {
@@ -40,9 +41,31 @@ function buildObjectType(name: string, fields: Field[], parentName: string, base
text: (field: Field) => ({ type: withNullableType(field, GraphQLString) }),
email: (field: Field) => ({ type: withNullableType(field, EmailAddressResolver) }),
textarea: (field: Field) => ({ type: withNullableType(field, GraphQLString) }),
- richText: (field: Field) => ({ type: withNullableType(field, GraphQLJSON) }),
code: (field: Field) => ({ type: withNullableType(field, GraphQLString) }),
date: (field: Field) => ({ type: withNullableType(field, DateTimeResolver) }),
+ richText: (field: RichTextField) => ({
+ type: withNullableType(field, GraphQLJSON),
+ async resolve(parent, args, context) {
+ if (args.depth > 0) {
+ const richTextRelationshipPromise = createRichTextRelationshipPromise({
+ req: context.req,
+ data: parent,
+ payload: context.req.payload,
+ depth: args.depth,
+ field,
+ });
+
+ await richTextRelationshipPromise();
+ }
+
+ return parent[field.name];
+ },
+ args: {
+ depth: {
+ type: GraphQLInt,
+ },
+ },
+ }),
upload: (field: UploadField) => {
const { relationTo, label } = field;