fix(live-preview): client-side live preview failed to populate localized fields (#13794)

Fixes #13756 

The findByID endpoint, by default, expects the data for localized fields
to be an object, values mapped to the locale. This is not the case for
client-side live preview, as we send already-flattened data to the
findByID endpoint.

For localized fields where the value is an object (richText/json/group),
the afterRead hook handler would attempt to flatten the field value,
even though it was already flattened.

## Solution

The solution is to expose a `flattenLocales` arg to the findByID
endpoint (default: true) and pass `flattenLocales: false` from the
client-side live preview request handler.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211334752795627
This commit is contained in:
Alessio Gravili
2025-09-15 09:16:52 -07:00
committed by GitHub
parent 555228b712
commit b34e5eadf4
14 changed files with 262 additions and 131 deletions

View File

@@ -48,6 +48,8 @@ export const mergeData = async <T extends Record<string, any>>(args: {
data: { data: {
data: incomingData, data: incomingData,
depth, depth,
// The incoming data already has had its locales flattened
flattenLocales: false,
locale, locale,
}, },
endpoint: encodeURI( endpoint: encodeURI(

View File

@@ -15,6 +15,12 @@ export const findByIDHandler: PayloadHandler = async (req) => {
const { id, collection } = getRequestCollectionWithID(req) const { id, collection } = getRequestCollectionWithID(req)
const depth = data ? data.depth : searchParams.get('depth') const depth = data ? data.depth : searchParams.get('depth')
const trash = data ? data.trash : searchParams.get('trash') === 'true' const trash = data ? data.trash : searchParams.get('trash') === 'true'
const flattenLocales = data
? data.flattenLocales
: searchParams.has('flattenLocales')
? searchParams.get('flattenLocales') === 'true'
: // flattenLocales should be undefined if not provided, so that the default (true) is applied in the operation
undefined
const result = await findByIDOperation({ const result = await findByIDOperation({
id, id,
@@ -26,6 +32,7 @@ export const findByIDHandler: PayloadHandler = async (req) => {
: undefined, : undefined,
depth: isNumber(depth) ? Number(depth) : undefined, depth: isNumber(depth) ? Number(depth) : undefined,
draft: data ? data.draft : searchParams.get('draft') === 'true', draft: data ? data.draft : searchParams.get('draft') === 'true',
flattenLocales,
joins: sanitizeJoinParams(req.query.joins as JoinParams), joins: sanitizeJoinParams(req.query.joins as JoinParams),
populate: sanitizePopulateParam(req.query.populate), populate: sanitizePopulateParam(req.query.populate),
req, req,

View File

@@ -20,7 +20,7 @@ import { combineQueries } from '../../database/combineQueries.js'
import { sanitizeJoinQuery } from '../../database/sanitizeJoinQuery.js' import { sanitizeJoinQuery } from '../../database/sanitizeJoinQuery.js'
import { sanitizeWhereQuery } from '../../database/sanitizeWhereQuery.js' import { sanitizeWhereQuery } from '../../database/sanitizeWhereQuery.js'
import { NotFound } from '../../errors/index.js' import { NotFound } from '../../errors/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js' import { afterRead, type AfterReadArgs } from '../../fields/hooks/afterRead/index.js'
import { validateQueryPaths } from '../../index.js' import { validateQueryPaths } from '../../index.js'
import { lockedDocumentsCollectionSlug } from '../../locked-documents/config.js' import { lockedDocumentsCollectionSlug } from '../../locked-documents/config.js'
import { appendNonTrashedFilter } from '../../utilities/appendNonTrashedFilter.js' import { appendNonTrashedFilter } from '../../utilities/appendNonTrashedFilter.js'
@@ -29,7 +29,7 @@ import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { replaceWithDraftIfAvailable } from '../../versions/drafts/replaceWithDraftIfAvailable.js' import { replaceWithDraftIfAvailable } from '../../versions/drafts/replaceWithDraftIfAvailable.js'
import { buildAfterOperation } from './utils.js' import { buildAfterOperation } from './utils.js'
export type Arguments = { export type FindByIDArgs = {
collection: Collection collection: Collection
currentDepth?: number currentDepth?: number
/** /**
@@ -49,14 +49,14 @@ export type Arguments = {
select?: SelectType select?: SelectType
showHiddenFields?: boolean showHiddenFields?: boolean
trash?: boolean trash?: boolean
} } & Pick<AfterReadArgs<JsonObject>, 'flattenLocales'>
export const findByIDOperation = async < export const findByIDOperation = async <
TSlug extends CollectionSlug, TSlug extends CollectionSlug,
TDisableErrors extends boolean, TDisableErrors extends boolean,
TSelect extends SelectFromCollectionSlug<TSlug>, TSelect extends SelectFromCollectionSlug<TSlug>,
>( >(
incomingArgs: Arguments, incomingArgs: FindByIDArgs,
): Promise<ApplyDisableErrors<TransformCollectionWithSelect<TSlug, TSelect>, TDisableErrors>> => { ): Promise<ApplyDisableErrors<TransformCollectionWithSelect<TSlug, TSelect>, TDisableErrors>> => {
let args = incomingArgs let args = incomingArgs
@@ -85,6 +85,7 @@ export const findByIDOperation = async <
depth, depth,
disableErrors, disableErrors,
draft: draftEnabled = false, draft: draftEnabled = false,
flattenLocales,
includeLockStatus, includeLockStatus,
joins, joins,
overrideAccess = false, overrideAccess = false,
@@ -279,6 +280,7 @@ export const findByIDOperation = async <
doc: result, doc: result,
draft: draftEnabled, draft: draftEnabled,
fallbackLocale: fallbackLocale!, fallbackLocale: fallbackLocale!,
flattenLocales,
global: null, global: null,
locale: locale!, locale: locale!,
overrideAccess, overrideAccess,

View File

@@ -18,7 +18,7 @@ import type { SelectFromCollectionSlug } from '../../config/types.js'
import { APIError } from '../../../errors/index.js' import { APIError } from '../../../errors/index.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js' import { createLocalReq } from '../../../utilities/createLocalReq.js'
import { findByIDOperation } from '../findByID.js' import { type FindByIDArgs, findByIDOperation } from '../findByID.js'
export type Options< export type Options<
TSlug extends CollectionSlug, TSlug extends CollectionSlug,
@@ -117,7 +117,7 @@ export type Options<
* If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. * If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks.
*/ */
user?: Document user?: Document
} } & Pick<FindByIDArgs, 'flattenLocales'>
export async function findByIDLocal< export async function findByIDLocal<
TSlug extends CollectionSlug, TSlug extends CollectionSlug,
@@ -135,6 +135,7 @@ export async function findByIDLocal<
depth, depth,
disableErrors = false, disableErrors = false,
draft = false, draft = false,
flattenLocales,
includeLockStatus, includeLockStatus,
joins, joins,
overrideAccess = true, overrideAccess = true,
@@ -160,6 +161,7 @@ export async function findByIDLocal<
depth, depth,
disableErrors, disableErrors,
draft, draft,
flattenLocales,
includeLockStatus, includeLockStatus,
joins, joins,
overrideAccess, overrideAccess,

View File

@@ -6,7 +6,7 @@ import type { JsonObject, PayloadRequest, PopulateType, SelectType } from '../..
import { getSelectMode } from '../../../utilities/getSelectMode.js' import { getSelectMode } from '../../../utilities/getSelectMode.js'
import { traverseFields } from './traverseFields.js' import { traverseFields } from './traverseFields.js'
type Args<T extends JsonObject> = { export type AfterReadArgs<T extends JsonObject> = {
collection: null | SanitizedCollectionConfig collection: null | SanitizedCollectionConfig
context: RequestContext context: RequestContext
currentDepth?: number currentDepth?: number
@@ -15,6 +15,12 @@ type Args<T extends JsonObject> = {
draft: boolean draft: boolean
fallbackLocale: null | string fallbackLocale: null | string
findMany?: boolean findMany?: boolean
/**
* Controls whether locales should be flattened into the requested locale.
* E.g.: { [locale]: fields } -> fields
*
* @default true
*/
flattenLocales?: boolean flattenLocales?: boolean
global: null | SanitizedGlobalConfig global: null | SanitizedGlobalConfig
locale: string locale: string
@@ -35,7 +41,7 @@ type Args<T extends JsonObject> = {
* - Populate relationships * - Populate relationships
*/ */
export async function afterRead<T extends JsonObject>(args: Args<T>): Promise<T> { export async function afterRead<T extends JsonObject>(args: AfterReadArgs<T>): Promise<T> {
const { const {
collection, collection,
context, context,

View File

@@ -9,6 +9,7 @@ import type {
SelectType, SelectType,
} from '../../../types/index.js' } from '../../../types/index.js'
import type { Block, Field, TabAsField } from '../../config/types.js' import type { Block, Field, TabAsField } from '../../config/types.js'
import type { AfterReadArgs } from './index.js'
import { MissingEditorProp } from '../../../errors/index.js' import { MissingEditorProp } from '../../../errors/index.js'
import { type RequestContext } from '../../../index.js' import { type RequestContext } from '../../../index.js'
@@ -40,7 +41,6 @@ type Args = {
*/ */
fieldPromises: Promise<void>[] fieldPromises: Promise<void>[]
findMany: boolean findMany: boolean
flattenLocales: boolean
global: null | SanitizedGlobalConfig global: null | SanitizedGlobalConfig
locale: null | string locale: null | string
overrideAccess: boolean overrideAccess: boolean
@@ -61,7 +61,7 @@ type Args = {
siblingFields?: (Field | TabAsField)[] siblingFields?: (Field | TabAsField)[]
triggerAccessControl?: boolean triggerAccessControl?: boolean
triggerHooks?: boolean triggerHooks?: boolean
} } & Required<Pick<AfterReadArgs<JsonObject>, 'flattenLocales'>>
// This function is responsible for the following actions, in order: // This function is responsible for the following actions, in order:
// - Remove hidden fields from response // - Remove hidden fields from response
@@ -236,15 +236,23 @@ export const promise = async ({
} }
} }
// If locale is `all`, siblingDoc[field.name] will be an object mapping locales to values - locales won't be flattened.
// In this case, run the hook for each locale and value pair
const shouldRunHookOnAllLocales =
locale === 'all' &&
'name' in field &&
typeof field.name === 'string' &&
// If localized values were hoisted, siblingDoc[field.name] will not be an object mapping locales to values
// => Object.entries(siblingDoc[field.name]) will be the value of a single locale, not all locales
// => do not run the hook for each locale
!shouldHoistLocalizedValue &&
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
typeof siblingDoc[field.name] === 'object'
if (fieldAffectsDataResult) { if (fieldAffectsDataResult) {
// Execute hooks // Execute hooks
if (triggerHooks && 'hooks' in field && field.hooks?.afterRead) { if (triggerHooks && 'hooks' in field && field.hooks?.afterRead) {
for (const hook of field.hooks.afterRead) { for (const hook of field.hooks.afterRead) {
const shouldRunHookOnAllLocales =
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
if (shouldRunHookOnAllLocales) { if (shouldRunHookOnAllLocales) {
const localesAndValues = Object.entries(siblingDoc[field.name]) const localesAndValues = Object.entries(siblingDoc[field.name])
await Promise.all( await Promise.all(
@@ -711,11 +719,6 @@ export const promise = async ({
if (editor?.hooks?.afterRead?.length) { if (editor?.hooks?.afterRead?.length) {
for (const hook of editor.hooks.afterRead) { for (const hook of editor.hooks.afterRead) {
const shouldRunHookOnAllLocales =
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
if (shouldRunHookOnAllLocales) { if (shouldRunHookOnAllLocales) {
const localesAndValues = Object.entries(siblingDoc[field.name]) const localesAndValues = Object.entries(siblingDoc[field.name])

View File

@@ -13,6 +13,12 @@ export const findOneHandler: PayloadHandler = async (req) => {
const globalConfig = getRequestGlobal(req) const globalConfig = getRequestGlobal(req)
const { data, searchParams } = req const { data, searchParams } = req
const depth = data ? data.depth : searchParams.get('depth') const depth = data ? data.depth : searchParams.get('depth')
const flattenLocales = data
? data.flattenLocales
: searchParams.has('flattenLocales')
? searchParams.get('flattenLocales') === 'true'
: // flattenLocales should be undfined if not provided, so that the default (true) is applied in the operation
undefined
const result = await findOneOperation({ const result = await findOneOperation({
slug: globalConfig.slug, slug: globalConfig.slug,
@@ -23,6 +29,7 @@ export const findOneHandler: PayloadHandler = async (req) => {
: undefined, : undefined,
depth: isNumber(depth) ? Number(depth) : undefined, depth: isNumber(depth) ? Number(depth) : undefined,
draft: data ? data.draft : searchParams.get('draft') === 'true', draft: data ? data.draft : searchParams.get('draft') === 'true',
flattenLocales,
globalConfig, globalConfig,
populate: sanitizePopulateParam(req.query.populate), populate: sanitizePopulateParam(req.query.populate),
req, req,

View File

@@ -9,14 +9,14 @@ import type {
import type { SanitizedGlobalConfig } from '../config/types.js' import type { SanitizedGlobalConfig } from '../config/types.js'
import { executeAccess } from '../../auth/executeAccess.js' import { executeAccess } from '../../auth/executeAccess.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js' import { afterRead, type AfterReadArgs } from '../../fields/hooks/afterRead/index.js'
import { lockedDocumentsCollectionSlug } from '../../locked-documents/config.js' import { lockedDocumentsCollectionSlug } from '../../locked-documents/config.js'
import { getSelectMode } from '../../utilities/getSelectMode.js' import { getSelectMode } from '../../utilities/getSelectMode.js'
import { killTransaction } from '../../utilities/killTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js' import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { replaceWithDraftIfAvailable } from '../../versions/drafts/replaceWithDraftIfAvailable.js' import { replaceWithDraftIfAvailable } from '../../versions/drafts/replaceWithDraftIfAvailable.js'
type Args = { export type GlobalFindOneArgs = {
/** /**
* You may pass the document data directly which will skip the `db.findOne` database query. * You may pass the document data directly which will skip the `db.findOne` database query.
* This is useful if you want to use this endpoint solely for running hooks and populating data. * This is useful if you want to use this endpoint solely for running hooks and populating data.
@@ -32,15 +32,16 @@ type Args = {
select?: SelectType select?: SelectType
showHiddenFields?: boolean showHiddenFields?: boolean
slug: string slug: string
} } & Pick<AfterReadArgs<JsonObject>, 'flattenLocales'>
export const findOneOperation = async <T extends Record<string, unknown>>( export const findOneOperation = async <T extends Record<string, unknown>>(
args: Args, args: GlobalFindOneArgs,
): Promise<T> => { ): Promise<T> => {
const { const {
slug, slug,
depth, depth,
draft: draftEnabled = false, draft: draftEnabled = false,
flattenLocales,
globalConfig, globalConfig,
includeLockStatus, includeLockStatus,
overrideAccess = false, overrideAccess = false,
@@ -206,6 +207,7 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
doc, doc,
draft: draftEnabled, draft: draftEnabled,
fallbackLocale: fallbackLocale!, fallbackLocale: fallbackLocale!,
flattenLocales,
global: globalConfig, global: globalConfig,
locale: locale!, locale: locale!,
overrideAccess, overrideAccess,

View File

@@ -11,7 +11,7 @@ import type { SelectFromGlobalSlug } from '../../config/types.js'
import { APIError } from '../../../errors/index.js' import { APIError } from '../../../errors/index.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js' import { createLocalReq } from '../../../utilities/createLocalReq.js'
import { findOneOperation } from '../findOne.js' import { findOneOperation, type GlobalFindOneArgs } from '../findOne.js'
export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = { export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = {
/** /**
@@ -78,7 +78,7 @@ export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = {
* If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. * If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks.
*/ */
user?: Document user?: Document
} } & Pick<GlobalFindOneArgs, 'flattenLocales'>
export async function findOneGlobalLocal< export async function findOneGlobalLocal<
TSlug extends GlobalSlug, TSlug extends GlobalSlug,
@@ -92,6 +92,7 @@ export async function findOneGlobalLocal<
data, data,
depth, depth,
draft = false, draft = false,
flattenLocales,
includeLockStatus, includeLockStatus,
overrideAccess = true, overrideAccess = true,
populate, populate,
@@ -110,6 +111,7 @@ export async function findOneGlobalLocal<
data, data,
depth, depth,
draft, draft,
flattenLocales,
globalConfig, globalConfig,
includeLockStatus, includeLockStatus,
overrideAccess, overrideAccess,

View File

@@ -31,6 +31,12 @@ export const RelationshipsBlock: React.FC<RelationshipsBlockProps> = (props) =>
{data?.richTextLexical && ( {data?.richTextLexical && (
<RichText content={data.richTextLexical} renderUploadFilenameOnly /> <RichText content={data.richTextLexical} renderUploadFilenameOnly />
)} )}
<p>
<b>Rich Text Lexical Localized:</b>
</p>
{data?.richTextLexicalLocalized && (
<RichText content={data.richTextLexicalLocalized} renderUploadFilenameOnly />
)}
<p> <p>
<b>Upload:</b> <b>Upload:</b>
</p> </p>

View File

@@ -92,6 +92,18 @@ export const Pages: CollectionConfig = {
], ],
}), }),
}, },
{
label: 'Rich Text — Lexical — Localized',
type: 'richText',
name: 'richTextLexicalLocalized',
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({ blocks: ['mediaBlock'] }),
],
}),
},
{ {
name: 'relationshipAsUpload', name: 'relationshipAsUpload',
type: 'upload', type: 'upload',

View File

@@ -129,7 +129,13 @@ describe('Collections - Live Preview', () => {
} as MessageEvent as LivePreviewMessageEvent<Page>, } as MessageEvent as LivePreviewMessageEvent<Page>,
initialData: { initialData: {
title: 'Test Page', title: 'Test Page',
id: 1, id: 1 as number | string,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
hero: {
type: 'highImpact',
},
slug: 'testPage',
} as Page, } as Page,
}) })
@@ -177,6 +183,24 @@ describe('Collections - Live Preview', () => {
expect(mergedData.title).toEqual('Test Page (Changed)') expect(mergedData.title).toEqual('Test Page (Changed)')
}) })
it('— strings - merges localized data', async () => {
const initialData = await createPageWithInitialData({
title: 'Test Page',
})
const mergedData = await mergeData({
depth: 1,
incomingData: {
...initialData,
localizedTitle: 'Test Page (Changed)',
},
initialData,
collectionSlug: pagesSlug,
})
expect(mergedData.localizedTitle).toEqual('Test Page (Changed)')
})
it('— arrays - can clear all rows', async () => { it('— arrays - can clear all rows', async () => {
const initialData = await createPageWithInitialData({ const initialData = await createPageWithInitialData({
title: 'Test Page', title: 'Test Page',
@@ -751,7 +775,7 @@ describe('Collections - Live Preview', () => {
expect(merge2.richTextSlate[3].value).toMatchObject(media) expect(merge2.richTextSlate[3].value).toMatchObject(media)
}) })
it('— relationships - populates within Lexical rich text editor', async () => { async function lexicalTest(fieldName: string) {
const initialData = await createPageWithInitialData({ const initialData = await createPageWithInitialData({
title: 'Test Page', title: 'Test Page',
}) })
@@ -762,7 +786,7 @@ describe('Collections - Live Preview', () => {
collectionSlug: pagesSlug, collectionSlug: pagesSlug,
incomingData: { incomingData: {
...initialData, ...initialData,
richTextLexical: { [fieldName]: {
root: { root: {
type: 'root', type: 'root',
format: '', format: '',
@@ -804,12 +828,12 @@ describe('Collections - Live Preview', () => {
initialData, initialData,
}) })
expect(merge1.richTextLexical.root.children).toHaveLength(3) expect(merge1[fieldName].root.children).toHaveLength(3)
expect(merge1.richTextLexical.root.children[0].type).toEqual('relationship') expect(merge1[fieldName].root.children[0].type).toEqual('relationship')
expect(merge1.richTextLexical.root.children[0].value).toMatchObject(testPost) expect(merge1[fieldName].root.children[0].value).toMatchObject(testPost)
expect(merge1.richTextLexical.root.children[1].type).toEqual('paragraph') expect(merge1[fieldName].root.children[1].type).toEqual('paragraph')
expect(merge1.richTextLexical.root.children[2].type).toEqual('upload') expect(merge1[fieldName].root.children[2].type).toEqual('upload')
expect(merge1.richTextLexical.root.children[2].value).toMatchObject(media) expect(merge1[fieldName].root.children[2].value).toMatchObject(media)
// Add a node before the populated one // Add a node before the populated one
const merge2 = await mergeData({ const merge2 = await mergeData({
@@ -817,7 +841,7 @@ describe('Collections - Live Preview', () => {
collectionSlug: pagesSlug, collectionSlug: pagesSlug,
incomingData: { incomingData: {
...merge1, ...merge1,
richTextLexical: { [fieldName]: {
root: { root: {
type: 'root', type: 'root',
format: '', format: '',
@@ -867,13 +891,23 @@ describe('Collections - Live Preview', () => {
initialData: merge1, initialData: merge1,
}) })
expect(merge2.richTextLexical.root.children).toHaveLength(4) expect(merge2[fieldName].root.children).toHaveLength(4)
expect(merge2.richTextLexical.root.children[0].type).toEqual('relationship') expect(merge2[fieldName].root.children[0].type).toEqual('relationship')
expect(merge2.richTextLexical.root.children[0].value).toMatchObject(testPost) expect(merge2[fieldName].root.children[0].value).toMatchObject(testPost)
expect(merge2.richTextLexical.root.children[1].type).toEqual('paragraph') expect(merge2[fieldName].root.children[1].type).toEqual('paragraph')
expect(merge2.richTextLexical.root.children[2].type).toEqual('paragraph') expect(merge2[fieldName].root.children[2].type).toEqual('paragraph')
expect(merge2.richTextLexical.root.children[3].type).toEqual('upload') expect(merge2[fieldName].root.children[3].type).toEqual('upload')
expect(merge2.richTextLexical.root.children[3].value).toMatchObject(media) expect(merge2[fieldName].root.children[3].value).toMatchObject(media)
}
// eslint-disable-next-line jest/expect-expect
it('— relationships - populates within Lexical rich text editor', async () => {
await lexicalTest('richTextLexical')
})
// eslint-disable-next-line jest/expect-expect
it('— relationships - populates within Localized Lexical rich text editor', async () => {
await lexicalTest('richTextLexicalLocalized')
}) })
it('— relationships - re-populates externally updated relationships', async () => { it('— relationships - re-populates externally updated relationships', async () => {

View File

@@ -98,7 +98,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
}; };
db: { db: {
defaultIDType: number; defaultIDType: string;
}; };
globals: { globals: {
header: Header; header: Header;
@@ -142,7 +142,7 @@ export interface UserAuthOperations {
export interface MediaBlock { export interface MediaBlock {
invertBackground?: boolean | null; invertBackground?: boolean | null;
position?: ('default' | 'fullscreen') | null; position?: ('default' | 'fullscreen') | null;
media: number | Media; media: string | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'mediaBlock'; blockType: 'mediaBlock';
@@ -152,7 +152,7 @@ export interface MediaBlock {
* via the `definition` "media". * via the `definition` "media".
*/ */
export interface Media { export interface Media {
id: number; id: string;
alt: string; alt: string;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -171,7 +171,7 @@ export interface Media {
* via the `definition` "users". * via the `definition` "users".
*/ */
export interface User { export interface User {
id: number; id: string;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
email: string; email: string;
@@ -195,9 +195,9 @@ export interface User {
* via the `definition` "pages". * via the `definition` "pages".
*/ */
export interface Page { export interface Page {
id: number; id: string;
slug: string; slug: string;
tenant?: (number | null) | Tenant; tenant?: (string | null) | Tenant;
title: string; title: string;
hero: { hero: {
type: 'none' | 'highImpact' | 'lowImpact'; type: 'none' | 'highImpact' | 'lowImpact';
@@ -206,7 +206,7 @@ export interface Page {
[k: string]: unknown; [k: string]: unknown;
}[] }[]
| null; | null;
media?: (number | null) | Media; media?: (string | null) | Media;
}; };
layout?: layout?:
| ( | (
@@ -225,11 +225,11 @@ export interface Page {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -262,11 +262,11 @@ export interface Page {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -285,7 +285,7 @@ export interface Page {
| { | {
invertBackground?: boolean | null; invertBackground?: boolean | null;
position?: ('default' | 'fullscreen') | null; position?: ('default' | 'fullscreen') | null;
media: number | Media; media: string | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'mediaBlock'; blockType: 'mediaBlock';
@@ -298,12 +298,12 @@ export interface Page {
| null; | null;
populateBy?: ('collection' | 'selection') | null; populateBy?: ('collection' | 'selection') | null;
relationTo?: 'posts' | null; relationTo?: 'posts' | null;
categories?: (number | Category)[] | null; categories?: (string | Category)[] | null;
limit?: number | null; limit?: number | null;
selectedDocs?: selectedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
/** /**
@@ -312,7 +312,7 @@ export interface Page {
populatedDocs?: populatedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
/** /**
@@ -326,7 +326,7 @@ export interface Page {
)[] )[]
| null; | null;
localizedTitle?: string | null; localizedTitle?: string | null;
relationToLocalized?: (number | null) | Post; relationToLocalized?: (string | null) | Post;
richTextSlate?: richTextSlate?:
| { | {
[k: string]: unknown; [k: string]: unknown;
@@ -347,22 +347,37 @@ export interface Page {
}; };
[k: string]: unknown; [k: string]: unknown;
} | null; } | null;
relationshipAsUpload?: (number | null) | Media; richTextLexicalLocalized?: {
relationshipMonoHasOne?: (number | null) | Post; root: {
relationshipMonoHasMany?: (number | Post)[] | null; type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
relationshipAsUpload?: (string | null) | Media;
relationshipMonoHasOne?: (string | null) | Post;
relationshipMonoHasMany?: (string | Post)[] | null;
relationshipPolyHasOne?: { relationshipPolyHasOne?: {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null; } | null;
relationshipPolyHasMany?: relationshipPolyHasMany?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
arrayOfRelationships?: arrayOfRelationships?:
| { | {
uploadInArray?: (number | null) | Media; uploadInArray?: (string | null) | Media;
richTextInArray?: { richTextInArray?: {
root: { root: {
type: string; type: string;
@@ -378,28 +393,28 @@ export interface Page {
}; };
[k: string]: unknown; [k: string]: unknown;
} | null; } | null;
relationshipInArrayMonoHasOne?: (number | null) | Post; relationshipInArrayMonoHasOne?: (string | null) | Post;
relationshipInArrayMonoHasMany?: (number | Post)[] | null; relationshipInArrayMonoHasMany?: (string | Post)[] | null;
relationshipInArrayPolyHasOne?: { relationshipInArrayPolyHasOne?: {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null; } | null;
relationshipInArrayPolyHasMany?: relationshipInArrayPolyHasMany?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
tab?: { tab?: {
relationshipInTab?: (number | null) | Post; relationshipInTab?: (string | null) | Post;
}; };
meta?: { meta?: {
title?: string | null; title?: string | null;
description?: string | null; description?: string | null;
image?: (number | null) | Media; image?: (string | null) | Media;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -409,7 +424,7 @@ export interface Page {
* via the `definition` "tenants". * via the `definition` "tenants".
*/ */
export interface Tenant { export interface Tenant {
id: number; id: string;
title: string; title: string;
clientURL: string; clientURL: string;
updatedAt: string; updatedAt: string;
@@ -420,9 +435,9 @@ export interface Tenant {
* via the `definition` "posts". * via the `definition` "posts".
*/ */
export interface Post { export interface Post {
id: number; id: string;
slug: string; slug: string;
tenant?: (number | null) | Tenant; tenant?: (string | null) | Tenant;
title: string; title: string;
hero: { hero: {
type: 'none' | 'highImpact' | 'lowImpact'; type: 'none' | 'highImpact' | 'lowImpact';
@@ -431,7 +446,7 @@ export interface Post {
[k: string]: unknown; [k: string]: unknown;
}[] }[]
| null; | null;
media?: (number | null) | Media; media?: (string | null) | Media;
}; };
layout?: layout?:
| ( | (
@@ -450,11 +465,11 @@ export interface Post {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -487,11 +502,11 @@ export interface Post {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -510,7 +525,7 @@ export interface Post {
| { | {
invertBackground?: boolean | null; invertBackground?: boolean | null;
position?: ('default' | 'fullscreen') | null; position?: ('default' | 'fullscreen') | null;
media: number | Media; media: string | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'mediaBlock'; blockType: 'mediaBlock';
@@ -523,12 +538,12 @@ export interface Post {
| null; | null;
populateBy?: ('collection' | 'selection') | null; populateBy?: ('collection' | 'selection') | null;
relationTo?: 'posts' | null; relationTo?: 'posts' | null;
categories?: (number | Category)[] | null; categories?: (string | Category)[] | null;
limit?: number | null; limit?: number | null;
selectedDocs?: selectedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
/** /**
@@ -537,7 +552,7 @@ export interface Post {
populatedDocs?: populatedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
/** /**
@@ -550,12 +565,12 @@ export interface Post {
} }
)[] )[]
| null; | null;
relatedPosts?: (number | Post)[] | null; relatedPosts?: (string | Post)[] | null;
localizedTitle?: string | null; localizedTitle?: string | null;
meta?: { meta?: {
title?: string | null; title?: string | null;
description?: string | null; description?: string | null;
image?: (number | null) | Media; image?: (string | null) | Media;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -566,7 +581,7 @@ export interface Post {
* via the `definition` "categories". * via the `definition` "categories".
*/ */
export interface Category { export interface Category {
id: number; id: string;
title?: string | null; title?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -576,9 +591,9 @@ export interface Category {
* via the `definition` "ssr". * via the `definition` "ssr".
*/ */
export interface Ssr { export interface Ssr {
id: number; id: string;
slug: string; slug: string;
tenant?: (number | null) | Tenant; tenant?: (string | null) | Tenant;
title: string; title: string;
hero: { hero: {
type: 'none' | 'highImpact' | 'lowImpact'; type: 'none' | 'highImpact' | 'lowImpact';
@@ -587,7 +602,7 @@ export interface Ssr {
[k: string]: unknown; [k: string]: unknown;
}[] }[]
| null; | null;
media?: (number | null) | Media; media?: (string | null) | Media;
}; };
layout?: layout?:
| ( | (
@@ -606,11 +621,11 @@ export interface Ssr {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -643,11 +658,11 @@ export interface Ssr {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -666,7 +681,7 @@ export interface Ssr {
| { | {
invertBackground?: boolean | null; invertBackground?: boolean | null;
position?: ('default' | 'fullscreen') | null; position?: ('default' | 'fullscreen') | null;
media: number | Media; media: string | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'mediaBlock'; blockType: 'mediaBlock';
@@ -679,12 +694,12 @@ export interface Ssr {
| null; | null;
populateBy?: ('collection' | 'selection') | null; populateBy?: ('collection' | 'selection') | null;
relationTo?: 'posts' | null; relationTo?: 'posts' | null;
categories?: (number | Category)[] | null; categories?: (string | Category)[] | null;
limit?: number | null; limit?: number | null;
selectedDocs?: selectedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
/** /**
@@ -693,7 +708,7 @@ export interface Ssr {
populatedDocs?: populatedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
/** /**
@@ -709,7 +724,7 @@ export interface Ssr {
meta?: { meta?: {
title?: string | null; title?: string | null;
description?: string | null; description?: string | null;
image?: (number | null) | Media; image?: (string | null) | Media;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -719,9 +734,9 @@ export interface Ssr {
* via the `definition` "ssr-autosave". * via the `definition` "ssr-autosave".
*/ */
export interface SsrAutosave { export interface SsrAutosave {
id: number; id: string;
slug: string; slug: string;
tenant?: (number | null) | Tenant; tenant?: (string | null) | Tenant;
title: string; title: string;
hero: { hero: {
type: 'none' | 'highImpact' | 'lowImpact'; type: 'none' | 'highImpact' | 'lowImpact';
@@ -730,7 +745,7 @@ export interface SsrAutosave {
[k: string]: unknown; [k: string]: unknown;
}[] }[]
| null; | null;
media?: (number | null) | Media; media?: (string | null) | Media;
}; };
layout?: layout?:
| ( | (
@@ -749,11 +764,11 @@ export interface SsrAutosave {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -786,11 +801,11 @@ export interface SsrAutosave {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -809,7 +824,7 @@ export interface SsrAutosave {
| { | {
invertBackground?: boolean | null; invertBackground?: boolean | null;
position?: ('default' | 'fullscreen') | null; position?: ('default' | 'fullscreen') | null;
media: number | Media; media: string | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'mediaBlock'; blockType: 'mediaBlock';
@@ -822,12 +837,12 @@ export interface SsrAutosave {
| null; | null;
populateBy?: ('collection' | 'selection') | null; populateBy?: ('collection' | 'selection') | null;
relationTo?: 'posts' | null; relationTo?: 'posts' | null;
categories?: (number | Category)[] | null; categories?: (string | Category)[] | null;
limit?: number | null; limit?: number | null;
selectedDocs?: selectedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
/** /**
@@ -836,7 +851,7 @@ export interface SsrAutosave {
populatedDocs?: populatedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
}[] }[]
| null; | null;
/** /**
@@ -852,7 +867,7 @@ export interface SsrAutosave {
meta?: { meta?: {
title?: string | null; title?: string | null;
description?: string | null; description?: string | null;
image?: (number | null) | Media; image?: (string | null) | Media;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -865,7 +880,7 @@ export interface SsrAutosave {
* via the `definition` "collection-level-config". * via the `definition` "collection-level-config".
*/ */
export interface CollectionLevelConfig { export interface CollectionLevelConfig {
id: number; id: string;
title?: string | null; title?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -875,48 +890,48 @@ export interface CollectionLevelConfig {
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
*/ */
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: number; id: string;
document?: document?:
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'ssr'; relationTo: 'ssr';
value: number | Ssr; value: string | Ssr;
} | null) } | null)
| ({ | ({
relationTo: 'ssr-autosave'; relationTo: 'ssr-autosave';
value: number | SsrAutosave; value: string | SsrAutosave;
} | null) } | null)
| ({ | ({
relationTo: 'tenants'; relationTo: 'tenants';
value: number | Tenant; value: string | Tenant;
} | null) } | null)
| ({ | ({
relationTo: 'categories'; relationTo: 'categories';
value: number | Category; value: string | Category;
} | null) } | null)
| ({ | ({
relationTo: 'media'; relationTo: 'media';
value: number | Media; value: string | Media;
} | null) } | null)
| ({ | ({
relationTo: 'collection-level-config'; relationTo: 'collection-level-config';
value: number | CollectionLevelConfig; value: string | CollectionLevelConfig;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -926,10 +941,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".
*/ */
export interface PayloadPreference { export interface PayloadPreference {
id: number; id: string;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
}; };
key?: string | null; key?: string | null;
value?: value?:
@@ -949,7 +964,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations". * via the `definition` "payload-migrations".
*/ */
export interface PayloadMigration { export interface PayloadMigration {
id: number; id: string;
name?: string | null; name?: string | null;
batch?: number | null; batch?: number | null;
updatedAt: string; updatedAt: string;
@@ -1071,6 +1086,7 @@ export interface PagesSelect<T extends boolean = true> {
relationToLocalized?: T; relationToLocalized?: T;
richTextSlate?: T; richTextSlate?: T;
richTextLexical?: T; richTextLexical?: T;
richTextLexicalLocalized?: T;
relationshipAsUpload?: T; relationshipAsUpload?: T;
relationshipMonoHasOne?: T; relationshipMonoHasOne?: T;
relationshipMonoHasMany?: T; relationshipMonoHasMany?: T;
@@ -1489,7 +1505,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
* via the `definition` "header". * via the `definition` "header".
*/ */
export interface Header { export interface Header {
id: number; id: string;
navItems?: navItems?:
| { | {
link: { link: {
@@ -1498,11 +1514,11 @@ export interface Header {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -1522,7 +1538,7 @@ export interface Header {
* via the `definition` "footer". * via the `definition` "footer".
*/ */
export interface Footer { export interface Footer {
id: number; id: string;
navItems?: navItems?:
| { | {
link: { link: {
@@ -1531,11 +1547,11 @@ export interface Footer {
reference?: reference?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;

View File

@@ -9,6 +9,7 @@ export const home: Omit<Page, 'createdAt' | 'id' | 'updatedAt'> = {
description: 'This is an example of live preview on a page.', description: 'This is an example of live preview on a page.',
}, },
tenant: '{{TENANT_1_ID}}', tenant: '{{TENANT_1_ID}}',
localizedTitle: 'Localized Title',
hero: { hero: {
type: 'highImpact', type: 'highImpact',
richText: [ richText: [
@@ -169,6 +170,35 @@ export const home: Omit<Page, 'createdAt' | 'id' | 'updatedAt'> = {
direction: null, direction: null,
}, },
}, },
richTextLexicalLocalized: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'upload',
version: 1,
fields: null,
relationTo: 'media',
value: {
id: '{{MEDIA_ID}}',
},
},
],
direction: null,
},
},
relationshipMonoHasMany: ['{{POST_1_ID}}'], relationshipMonoHasMany: ['{{POST_1_ID}}'],
relationshipMonoHasOne: '{{POST_1_ID}}', relationshipMonoHasOne: '{{POST_1_ID}}',
relationshipPolyHasMany: [{ relationTo: 'posts', value: '{{POST_1_ID}}' }], relationshipPolyHasMany: [{ relationTo: 'posts', value: '{{POST_1_ID}}' }],