feat: dynamically populates richtext relationships
* feat: adds relationship field to test searchable input * fix: searching on relationship fields properly fetches results * chore: more dry relationship field * feat: sets default access control to requiring a user to be logged in * feat: dynamically populates richtext relationships * feat: allows depth param in graphql richText field * feat: ensures relationship input is initialized with up to 3 related collections
This commit is contained in:
@@ -246,19 +246,44 @@ const Relationship: React.FC<Props> = (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);
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
)}
|
||||
<Number
|
||||
required
|
||||
name="depth"
|
||||
label="Depth"
|
||||
min={0}
|
||||
max={maxDepth}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={baseClass}
|
||||
@@ -24,7 +34,7 @@ const Element = ({ attributes, children, element }) => {
|
||||
{' '}
|
||||
Relationship
|
||||
</div>
|
||||
<h5>{value[relatedCollection?.admin?.useAsTitle || 'id']}</h5>
|
||||
<h5>{data[relatedCollection?.admin?.useAsTitle || 'id']}</h5>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
3
src/auth/defaultAccess.ts
Normal file
3
src/auth/defaultAccess.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PayloadRequest } from '../express/types';
|
||||
|
||||
export default ({ req: { user } }: { req: PayloadRequest}): boolean => Boolean(user);
|
||||
@@ -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: {
|
||||
|
||||
@@ -57,6 +57,10 @@ async function findByID(incomingArgs: Arguments): Promise<Document> {
|
||||
// /////////////////////////////////////
|
||||
|
||||
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 } = {
|
||||
|
||||
@@ -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
|
||||
|
||||
134
src/fields/richTextRelationshipPromise.ts
Normal file
134
src/fields/richTextRelationshipPromise.ts
Normal file
@@ -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<void>[]
|
||||
}
|
||||
|
||||
const populate = async ({
|
||||
id,
|
||||
collection,
|
||||
data,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
}: 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,
|
||||
});
|
||||
|
||||
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<void> => {
|
||||
const promises = [];
|
||||
|
||||
recurseRichText({
|
||||
req,
|
||||
children: data[field.name],
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
field,
|
||||
promises,
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
export default richTextRelationshipPromise;
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user