perf: faster page navigation by speeding up createClientConfig, speed up version fetching, speed up lexical init. Up to 100x faster (#9457)
If you had a lot of fields and collections, createClientConfig would be extremely slow, as it was copying a lot of memory. In my test config with a lot of fields and collections, it took 4 seconds(!!). And not only that, it also ran between every single page navigation. This PR significantly speeds up the createClientConfig function. In my test config, its execution speed went from 4 seconds to 50 ms. Additionally, createClientConfig is now properly cached in both dev & prod. It no longer runs between every single page navigation. Even if you trigger a full page reload, createClientConfig will be cached and not run again. Despite that, HMR remains fully-functional. This will make payload feel noticeably faster for large configs - especially if it contains a lot of richtext fields, as it was previously deep-copying the relatively large richText editor configs over and over again. ## Before - 40 sec navigation speed https://github.com/user-attachments/assets/fe6b707a-459b-44c6-982a-b277f6cbb73f ## After - 1 sec navigation speed https://github.com/user-attachments/assets/384fba63-dc32-4396-b3c2-0353fcac6639 ## Todo - [x] Implement ClientSchemaMap and cache it, to remove createClientField call in our form state endpoint - [x] Enable schemaMap caching for dev - [x] Cache lexical clientField generation, or add it to the parent clientConfig ## Lexical changes Red: old / removed Green: new  ### Speed up version queries This PR comes with performance optimizations for fetching versions before a document is loaded. Not only does it use the new select API to limit the fields it queries, it also completely skips a database query if the current document is published. ### Speed up lexical init Removes a bunch of unnecessary deep copying of lexical objects which caused higher memory usage and slower load times. Additionally, the lexical default config sanitization now happens less often.
This commit is contained in:
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -296,6 +296,7 @@ jobs:
|
||||
- admin__e2e__3
|
||||
- admin-root
|
||||
- auth
|
||||
- auth-basic
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
- fields
|
||||
|
||||
@@ -17,7 +17,7 @@ const customReactVersionParser: CustomVersionParser = (version) => {
|
||||
|
||||
let checkedDependencies = false
|
||||
|
||||
export const checkDependencies = async () => {
|
||||
export const checkDependencies = () => {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' &&
|
||||
@@ -26,7 +26,7 @@ export const checkDependencies = async () => {
|
||||
checkedDependencies = true
|
||||
|
||||
// First check if there are mismatching dependency versions of next / react packages
|
||||
await payloadCheckDependencies({
|
||||
void payloadCheckDependencies({
|
||||
dependencyGroups: [
|
||||
{
|
||||
name: 'react',
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'
|
||||
|
||||
import { rtlLanguages } from '@payloadcms/translations'
|
||||
import { RootProvider } from '@payloadcms/ui'
|
||||
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
|
||||
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||
import { getPayload, parseCookies } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js'
|
||||
import { getClientConfig } from '../../utilities/getClientConfig.js'
|
||||
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
|
||||
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
|
||||
import { initReq } from '../../utilities/initReq.js'
|
||||
@@ -33,7 +33,7 @@ export const RootLayout = async ({
|
||||
readonly importMap: ImportMap
|
||||
readonly serverFunction: ServerFunctionClient
|
||||
}) => {
|
||||
await checkDependencies()
|
||||
checkDependencies()
|
||||
|
||||
const config = await configPromise
|
||||
|
||||
@@ -54,7 +54,7 @@ export const RootLayout = async ({
|
||||
|
||||
const payload = await getPayload({ config, importMap })
|
||||
|
||||
const { i18n, permissions, req, user } = await initReq(config)
|
||||
const { i18n, permissions, user } = await initReq(config)
|
||||
|
||||
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
||||
? 'RTL'
|
||||
@@ -86,7 +86,7 @@ export const RootLayout = async ({
|
||||
|
||||
const navPrefs = await getNavPrefs({ payload, user })
|
||||
|
||||
const clientConfig = await getClientConfig({
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
importMap,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientConfig, ImportMap, SanitizedConfig } from 'payload'
|
||||
|
||||
import { createClientConfig } from 'payload'
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getClientConfig = cache(
|
||||
async (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): Promise<ClientConfig> => {
|
||||
const { config, i18n, importMap } = args
|
||||
|
||||
const clientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
return Promise.resolve(clientConfig)
|
||||
},
|
||||
)
|
||||
@@ -102,6 +102,7 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
await getVersions({
|
||||
id: user.id,
|
||||
collectionConfig,
|
||||
doc: data,
|
||||
docPermissions,
|
||||
locale: locale?.code,
|
||||
payload,
|
||||
|
||||
@@ -10,6 +10,13 @@ import { sanitizeID } from '@payloadcms/ui/shared'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
/**
|
||||
* Optional - performance optimization.
|
||||
* If a document has been fetched before fetching versions, pass it here.
|
||||
* If this document is set to published, we can skip the query to find out if a published document exists,
|
||||
* as the passed in document is proof of its existence.
|
||||
*/
|
||||
doc?: Record<string, any>
|
||||
docPermissions: SanitizedDocumentPermissions
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
@@ -27,9 +34,11 @@ type Result = Promise<{
|
||||
|
||||
// TODO: in the future, we can parallelize some of these queries
|
||||
// this will speed up the API by ~30-100ms or so
|
||||
// Note from the future: I have attempted parallelizing these queries, but it made this function almost 2x slower.
|
||||
export const getVersions = async ({
|
||||
id: idArg,
|
||||
collectionConfig,
|
||||
doc,
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
locale,
|
||||
@@ -37,7 +46,7 @@ export const getVersions = async ({
|
||||
user,
|
||||
}: Args): Result => {
|
||||
const id = sanitizeID(idArg)
|
||||
let publishedQuery
|
||||
let publishedDoc
|
||||
let hasPublishedDoc = false
|
||||
let mostRecentVersionIsAutosaved = false
|
||||
let unpublishedVersionCount = 0
|
||||
@@ -70,10 +79,20 @@ export const getVersions = async ({
|
||||
}
|
||||
|
||||
if (versionsConfig?.drafts) {
|
||||
publishedQuery = await payload.find({
|
||||
// Find out if a published document exists
|
||||
if (doc?._status === 'published') {
|
||||
publishedDoc = doc
|
||||
} else {
|
||||
publishedDoc = (
|
||||
await payload.find({
|
||||
collection: collectionConfig.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
locale: locale || undefined,
|
||||
pagination: false,
|
||||
select: {
|
||||
updatedAt: true,
|
||||
},
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
@@ -99,8 +118,10 @@ export const getVersions = async ({
|
||||
],
|
||||
},
|
||||
})
|
||||
)?.docs?.[0]
|
||||
}
|
||||
|
||||
if (publishedQuery.docs?.[0]) {
|
||||
if (publishedDoc) {
|
||||
hasPublishedDoc = true
|
||||
}
|
||||
|
||||
@@ -109,6 +130,9 @@ export const getVersions = async ({
|
||||
collection: collectionConfig.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
select: {
|
||||
autosave: true,
|
||||
},
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
@@ -130,7 +154,7 @@ export const getVersions = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (publishedQuery.docs?.[0]?.updatedAt) {
|
||||
if (publishedDoc?.updatedAt) {
|
||||
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
|
||||
collection: collectionConfig.slug,
|
||||
user,
|
||||
@@ -148,7 +172,7 @@ export const getVersions = async ({
|
||||
},
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: publishedQuery.docs[0].updatedAt,
|
||||
greater_than: publishedDoc.updatedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -159,6 +183,7 @@ export const getVersions = async ({
|
||||
|
||||
;({ totalDocs: versionCount } = await payload.countVersions({
|
||||
collection: collectionConfig.slug,
|
||||
depth: 0,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
@@ -173,15 +198,23 @@ export const getVersions = async ({
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
// Find out if a published document exists
|
||||
if (versionsConfig?.drafts) {
|
||||
publishedQuery = await payload.findGlobal({
|
||||
if (doc?._status === 'published') {
|
||||
publishedDoc = doc
|
||||
} else {
|
||||
publishedDoc = await payload.findGlobal({
|
||||
slug: globalConfig.slug,
|
||||
depth: 0,
|
||||
locale,
|
||||
select: {
|
||||
updatedAt: true,
|
||||
},
|
||||
user,
|
||||
})
|
||||
}
|
||||
|
||||
if (publishedQuery?._status === 'published') {
|
||||
if (publishedDoc?._status === 'published') {
|
||||
hasPublishedDoc = true
|
||||
}
|
||||
|
||||
@@ -204,7 +237,7 @@ export const getVersions = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (publishedQuery?.updatedAt) {
|
||||
if (publishedDoc?.updatedAt) {
|
||||
;({ totalDocs: unpublishedVersionCount } = await payload.countGlobalVersions({
|
||||
depth: 0,
|
||||
global: globalConfig.slug,
|
||||
@@ -218,7 +251,7 @@ export const getVersions = async ({
|
||||
},
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: publishedQuery.updatedAt,
|
||||
greater_than: publishedDoc.updatedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,46 +1,11 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
ClientConfig,
|
||||
Data,
|
||||
DocumentPreferences,
|
||||
FormState,
|
||||
ImportMap,
|
||||
PayloadRequest,
|
||||
SanitizedConfig,
|
||||
VisibleEntities,
|
||||
} from 'payload'
|
||||
import type { Data, DocumentPreferences, FormState, PayloadRequest, VisibleEntities } from 'payload'
|
||||
|
||||
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
|
||||
import { getAccessResults, isEntityHidden, parseCookies } from 'payload'
|
||||
|
||||
import { renderDocument } from './index.js'
|
||||
|
||||
let cachedClientConfig = global._payload_clientConfig
|
||||
|
||||
if (!cachedClientConfig) {
|
||||
cachedClientConfig = global._payload_clientConfig = null
|
||||
}
|
||||
|
||||
export const getClientConfig = (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientConfig => {
|
||||
const { config, i18n, importMap } = args
|
||||
|
||||
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
cachedClientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
type RenderDocumentResult = {
|
||||
data: any
|
||||
Document: React.ReactNode
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import type {
|
||||
AdminViewProps,
|
||||
Data,
|
||||
PayloadComponent,
|
||||
ServerProps,
|
||||
ServerSideEditViewProps,
|
||||
} from 'payload'
|
||||
import type { AdminViewProps, Data, PayloadComponent, ServerSideEditViewProps } from 'payload'
|
||||
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
@@ -137,6 +131,7 @@ export const renderDocument = async ({
|
||||
getVersions({
|
||||
id: idFromArgs,
|
||||
collectionConfig,
|
||||
doc,
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
locale: locale?.code,
|
||||
|
||||
@@ -1,45 +1,12 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ListPreferences } from '@payloadcms/ui'
|
||||
import type {
|
||||
ClientConfig,
|
||||
ImportMap,
|
||||
ListQuery,
|
||||
PayloadRequest,
|
||||
SanitizedConfig,
|
||||
VisibleEntities,
|
||||
} from 'payload'
|
||||
import type { ListQuery, PayloadRequest, VisibleEntities } from 'payload'
|
||||
|
||||
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
|
||||
import { getAccessResults, isEntityHidden, parseCookies } from 'payload'
|
||||
|
||||
import { renderListView } from './index.js'
|
||||
|
||||
let cachedClientConfig = global._payload_clientConfig
|
||||
|
||||
if (!cachedClientConfig) {
|
||||
cachedClientConfig = global._payload_clientConfig = null
|
||||
}
|
||||
|
||||
export const getClientConfig = (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientConfig => {
|
||||
const { config, i18n, importMap } = args
|
||||
|
||||
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
cachedClientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
type RenderListResult = {
|
||||
List: React.ReactNode
|
||||
preferences: ListPreferences
|
||||
|
||||
@@ -4,12 +4,12 @@ import type { ImportMap, SanitizedConfig } from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { getClientConfig } from '../../utilities/getClientConfig.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getViewFromConfig } from './getViewFromConfig.js'
|
||||
|
||||
@@ -115,7 +115,7 @@ export const RootPage = async ({
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
const clientConfig = await getClientConfig({
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ClientBlock, ClientField, Field } from '../../fields/config/types.
|
||||
import type { DocumentPreferences } from '../../preferences/types.js'
|
||||
import type { Operation, Payload, PayloadRequest } from '../../types/index.js'
|
||||
import type {
|
||||
ClientFieldSchemaMap,
|
||||
ClientTab,
|
||||
Data,
|
||||
FieldSchemaMap,
|
||||
@@ -67,6 +68,7 @@ export type FieldPaths = {
|
||||
|
||||
export type ServerComponentProps = {
|
||||
clientField: ClientFieldWithOptionalType
|
||||
clientFieldSchemaMap: ClientFieldSchemaMap
|
||||
collectionSlug: string
|
||||
data: Data
|
||||
field: Field
|
||||
|
||||
@@ -3,8 +3,16 @@ import type React from 'react'
|
||||
|
||||
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Block, Field, FieldTypes, Tab } from '../fields/config/types.js'
|
||||
import type {
|
||||
Block,
|
||||
ClientBlock,
|
||||
ClientField,
|
||||
Field,
|
||||
FieldTypes,
|
||||
Tab,
|
||||
} from '../fields/config/types.js'
|
||||
import type { JsonObject } from '../types/index.js'
|
||||
import type { ClientTab } from './fields/Tabs.js'
|
||||
import type {
|
||||
BuildFormStateArgs,
|
||||
Data,
|
||||
@@ -489,3 +497,13 @@ export type FieldSchemaMap = Map<
|
||||
| Field
|
||||
| Tab
|
||||
>
|
||||
|
||||
export type ClientFieldSchemaMap = Map<
|
||||
SchemaPath,
|
||||
| {
|
||||
fields: ClientField[]
|
||||
}
|
||||
| ClientBlock
|
||||
| ClientField
|
||||
| ClientTab
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { checkDependencies } from './utilities/dependencies/dependencyChecker.js'
|
||||
import { PAYLOAD_PACKAGE_LIST } from './versions/payloadPackageList.js'
|
||||
|
||||
export async function checkPayloadDependencies() {
|
||||
export function checkPayloadDependencies() {
|
||||
const dependencies = [...PAYLOAD_PACKAGE_LIST]
|
||||
|
||||
if (process.env.PAYLOAD_CI_DEPENDENCY_CHECKER !== 'true') {
|
||||
@@ -9,7 +9,7 @@ export async function checkPayloadDependencies() {
|
||||
}
|
||||
|
||||
// First load. First check if there are mismatching dependency versions of payload packages
|
||||
await checkDependencies({
|
||||
void checkDependencies({
|
||||
dependencyGroups: [
|
||||
{
|
||||
name: 'payload',
|
||||
|
||||
@@ -9,10 +9,10 @@ import type {
|
||||
} from '../../config/types.js'
|
||||
import type { ClientField } from '../../fields/config/client.js'
|
||||
import type { Payload } from '../../types/index.js'
|
||||
import type { SanitizedUploadConfig } from '../../uploads/types.js'
|
||||
import type { SanitizedCollectionConfig } from './types.js'
|
||||
|
||||
import { createClientFields } from '../../fields/config/client.js'
|
||||
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
|
||||
|
||||
export type ServerOnlyCollectionProperties = keyof Pick<
|
||||
SanitizedCollectionConfig,
|
||||
@@ -47,12 +47,19 @@ export type ClientCollectionConfig = {
|
||||
| 'preview'
|
||||
| ServerOnlyCollectionAdminProperties
|
||||
>
|
||||
auth?: { verify?: true } & Omit<
|
||||
SanitizedCollectionConfig['auth'],
|
||||
'forgotPassword' | 'strategies' | 'verify'
|
||||
>
|
||||
fields: ClientField[]
|
||||
labels: {
|
||||
plural: StaticLabel
|
||||
singular: StaticLabel
|
||||
}
|
||||
} & Omit<SanitizedCollectionConfig, 'admin' | 'fields' | 'labels' | ServerOnlyCollectionProperties>
|
||||
} & Omit<
|
||||
SanitizedCollectionConfig,
|
||||
'admin' | 'auth' | 'fields' | 'labels' | ServerOnlyCollectionProperties
|
||||
>
|
||||
|
||||
const serverOnlyCollectionProperties: Partial<ServerOnlyCollectionProperties>[] = [
|
||||
'hooks',
|
||||
@@ -93,97 +100,147 @@ export const createClientCollectionConfig = ({
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientCollectionConfig => {
|
||||
const clientCollection = deepCopyObjectSimple(
|
||||
collection,
|
||||
true,
|
||||
) as unknown as ClientCollectionConfig
|
||||
const clientCollection = {} as Partial<ClientCollectionConfig>
|
||||
|
||||
for (const key in collection) {
|
||||
if (serverOnlyCollectionProperties.includes(key as any)) {
|
||||
continue
|
||||
}
|
||||
switch (key) {
|
||||
case 'admin':
|
||||
if (!collection.admin) {
|
||||
break
|
||||
}
|
||||
clientCollection.admin = {} as ClientCollectionConfig['admin']
|
||||
for (const adminKey in collection.admin) {
|
||||
if (serverOnlyCollectionAdminProperties.includes(adminKey as any)) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch (adminKey) {
|
||||
case 'description':
|
||||
if (
|
||||
typeof collection.admin.description === 'string' ||
|
||||
typeof collection.admin.description === 'object'
|
||||
) {
|
||||
if (collection.admin.description) {
|
||||
clientCollection.admin.description = collection.admin.description
|
||||
}
|
||||
} else if (typeof collection.admin.description === 'function') {
|
||||
const description = collection.admin.description({ t: i18n.t })
|
||||
if (description) {
|
||||
clientCollection.admin.description = description
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'livePreview':
|
||||
clientCollection.admin.livePreview =
|
||||
{} as ClientCollectionConfig['admin']['livePreview']
|
||||
if (collection.admin.livePreview.breakpoints) {
|
||||
clientCollection.admin.livePreview.breakpoints =
|
||||
collection.admin.livePreview.breakpoints
|
||||
}
|
||||
break
|
||||
case 'preview':
|
||||
if (collection.admin.preview) {
|
||||
clientCollection.admin.preview = true
|
||||
}
|
||||
break
|
||||
default:
|
||||
clientCollection.admin[adminKey] = collection.admin[adminKey]
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'auth':
|
||||
if (!collection.auth) {
|
||||
break
|
||||
}
|
||||
clientCollection.auth = {} as { verify?: true } & SanitizedCollectionConfig['auth']
|
||||
if (collection.auth.cookies) {
|
||||
clientCollection.auth.cookies = collection.auth.cookies
|
||||
}
|
||||
if (collection.auth.depth !== undefined) {
|
||||
// Check for undefined as it can be a number (0)
|
||||
clientCollection.auth.depth = collection.auth.depth
|
||||
}
|
||||
if (collection.auth.disableLocalStrategy) {
|
||||
clientCollection.auth.disableLocalStrategy = collection.auth.disableLocalStrategy
|
||||
}
|
||||
if (collection.auth.lockTime !== undefined) {
|
||||
// Check for undefined as it can be a number (0)
|
||||
clientCollection.auth.lockTime = collection.auth.lockTime
|
||||
}
|
||||
if (collection.auth.loginWithUsername) {
|
||||
clientCollection.auth.loginWithUsername = collection.auth.loginWithUsername
|
||||
}
|
||||
if (collection.auth.maxLoginAttempts !== undefined) {
|
||||
// Check for undefined as it can be a number (0)
|
||||
clientCollection.auth.maxLoginAttempts = collection.auth.maxLoginAttempts
|
||||
}
|
||||
if (collection.auth.removeTokenFromResponses) {
|
||||
clientCollection.auth.removeTokenFromResponses = collection.auth.removeTokenFromResponses
|
||||
}
|
||||
|
||||
if (collection.auth.useAPIKey) {
|
||||
clientCollection.auth.useAPIKey = collection.auth.useAPIKey
|
||||
}
|
||||
if (collection.auth.tokenExpiration) {
|
||||
clientCollection.auth.tokenExpiration = collection.auth.tokenExpiration
|
||||
}
|
||||
if (collection.auth.verify) {
|
||||
clientCollection.auth.verify = true
|
||||
}
|
||||
break
|
||||
case 'fields':
|
||||
clientCollection.fields = createClientFields({
|
||||
clientFields: clientCollection?.fields || [],
|
||||
defaultIDType,
|
||||
fields: collection.fields,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
serverOnlyCollectionProperties.forEach((key) => {
|
||||
if (key in clientCollection) {
|
||||
delete clientCollection[key]
|
||||
break
|
||||
case 'labels':
|
||||
clientCollection.labels = {
|
||||
plural:
|
||||
typeof collection.labels.plural === 'function'
|
||||
? collection.labels.plural({ t: i18n.t })
|
||||
: collection.labels.plural,
|
||||
singular:
|
||||
typeof collection.labels.singular === 'function'
|
||||
? collection.labels.singular({ t: i18n.t })
|
||||
: collection.labels.singular,
|
||||
}
|
||||
})
|
||||
|
||||
if ('upload' in clientCollection && typeof clientCollection.upload === 'object') {
|
||||
serverOnlyUploadProperties.forEach((key) => {
|
||||
if (key in clientCollection.upload) {
|
||||
delete clientCollection.upload[key]
|
||||
break
|
||||
case 'upload':
|
||||
if (!collection.upload) {
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
if ('imageSizes' in clientCollection.upload && clientCollection.upload.imageSizes.length) {
|
||||
clientCollection.upload.imageSizes = clientCollection.upload.imageSizes.map((size) => {
|
||||
clientCollection.upload = {} as SanitizedUploadConfig
|
||||
for (const uploadKey in collection.upload) {
|
||||
if (serverOnlyUploadProperties.includes(uploadKey as any)) {
|
||||
continue
|
||||
}
|
||||
if (uploadKey === 'imageSizes') {
|
||||
clientCollection.upload.imageSizes = collection.upload.imageSizes.map((size) => {
|
||||
const sanitizedSize = { ...size }
|
||||
if ('generateImageName' in sanitizedSize) {
|
||||
delete sanitizedSize.generateImageName
|
||||
}
|
||||
return sanitizedSize
|
||||
})
|
||||
} else {
|
||||
clientCollection.upload[uploadKey] = collection.upload[uploadKey]
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
break
|
||||
default:
|
||||
clientCollection[key] = collection[key]
|
||||
}
|
||||
}
|
||||
|
||||
if ('auth' in clientCollection && typeof clientCollection.auth === 'object') {
|
||||
delete clientCollection.auth.strategies
|
||||
delete clientCollection.auth.forgotPassword
|
||||
delete clientCollection.auth.verify
|
||||
}
|
||||
|
||||
if (collection.labels) {
|
||||
Object.entries(collection.labels).forEach(([labelType, collectionLabel]) => {
|
||||
if (typeof collectionLabel === 'function') {
|
||||
clientCollection.labels[labelType] = collectionLabel({ t: i18n.t })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!clientCollection.admin) {
|
||||
clientCollection.admin = {} as ClientCollectionConfig['admin']
|
||||
}
|
||||
|
||||
serverOnlyCollectionAdminProperties.forEach((key) => {
|
||||
if (key in clientCollection.admin) {
|
||||
delete clientCollection.admin[key]
|
||||
}
|
||||
})
|
||||
|
||||
if (collection.admin.preview) {
|
||||
clientCollection.admin.preview = true
|
||||
}
|
||||
|
||||
let description = undefined
|
||||
|
||||
if (collection.admin?.description) {
|
||||
if (
|
||||
typeof collection.admin?.description === 'string' ||
|
||||
typeof collection.admin?.description === 'object'
|
||||
) {
|
||||
description = collection.admin.description
|
||||
} else if (typeof collection.admin?.description === 'function') {
|
||||
description = collection.admin?.description({ t: i18n.t })
|
||||
}
|
||||
}
|
||||
|
||||
if (description) {
|
||||
clientCollection.admin.description = description
|
||||
}
|
||||
|
||||
if (
|
||||
'livePreview' in clientCollection.admin &&
|
||||
clientCollection.admin.livePreview &&
|
||||
'url' in clientCollection.admin.livePreview
|
||||
) {
|
||||
delete clientCollection.admin.livePreview.url
|
||||
}
|
||||
|
||||
return clientCollection
|
||||
return clientCollection as ClientCollectionConfig
|
||||
}
|
||||
|
||||
export const createClientCollectionConfigs = ({
|
||||
|
||||
@@ -91,6 +91,11 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
|
||||
return null
|
||||
}
|
||||
|
||||
if (!result.version) {
|
||||
// Fallback if not selected
|
||||
;(result as any).version = {}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeRead - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -93,6 +93,10 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
|
||||
docs: await Promise.all(
|
||||
paginatedDocs.docs.map(async (doc) => {
|
||||
const docRef = doc
|
||||
// Fallback if not selected
|
||||
if (!docRef.version) {
|
||||
;(docRef as any).version = {}
|
||||
}
|
||||
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
|
||||
await priorHook
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { DeepPartial } from 'ts-essentials'
|
||||
|
||||
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
||||
import type {
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
createClientCollectionConfigs,
|
||||
} from '../collections/config/client.js'
|
||||
import { type ClientGlobalConfig, createClientGlobalConfigs } from '../globals/config/client.js'
|
||||
import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
|
||||
|
||||
export type ServerOnlyRootProperties = keyof Pick<
|
||||
SanitizedConfig,
|
||||
@@ -39,7 +39,6 @@ export type ServerOnlyRootAdminProperties = keyof Pick<SanitizedConfig['admin'],
|
||||
|
||||
export type ClientConfig = {
|
||||
admin: {
|
||||
components: null
|
||||
dependencies?: Record<string, React.ReactNode>
|
||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
|
||||
@@ -81,56 +80,95 @@ export const createClientConfig = ({
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientConfig => {
|
||||
// We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client
|
||||
const clientConfig = deepCopyObjectSimple(config, true) as unknown as ClientConfig
|
||||
const clientConfig = {} as DeepPartial<ClientConfig>
|
||||
|
||||
for (const key of serverOnlyConfigProperties) {
|
||||
if (key in clientConfig) {
|
||||
delete clientConfig[key]
|
||||
for (const key in config) {
|
||||
if (serverOnlyConfigProperties.includes(key as any)) {
|
||||
continue
|
||||
}
|
||||
switch (key) {
|
||||
case 'admin':
|
||||
clientConfig.admin = {
|
||||
autoLogin: config.admin.autoLogin,
|
||||
avatar: config.admin.avatar,
|
||||
custom: config.admin.custom,
|
||||
dateFormat: config.admin.dateFormat,
|
||||
dependencies: config.admin.dependencies,
|
||||
disable: config.admin.disable,
|
||||
importMap: config.admin.importMap,
|
||||
meta: config.admin.meta,
|
||||
routes: config.admin.routes,
|
||||
theme: config.admin.theme,
|
||||
user: config.admin.user,
|
||||
}
|
||||
if (config.admin.livePreview) {
|
||||
clientConfig.admin.livePreview = {}
|
||||
|
||||
if (config.admin.livePreview.breakpoints) {
|
||||
clientConfig.admin.livePreview.breakpoints = config.admin.livePreview.breakpoints
|
||||
}
|
||||
}
|
||||
|
||||
if ('localization' in clientConfig && clientConfig.localization) {
|
||||
for (const locale of clientConfig.localization.locales) {
|
||||
delete locale.toString
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
'i18n' in clientConfig &&
|
||||
'supportedLanguages' in clientConfig.i18n &&
|
||||
clientConfig.i18n.supportedLanguages
|
||||
) {
|
||||
delete clientConfig.i18n.supportedLanguages
|
||||
}
|
||||
|
||||
if (!clientConfig.admin) {
|
||||
clientConfig.admin = {} as ClientConfig['admin']
|
||||
}
|
||||
|
||||
clientConfig.admin.components = null
|
||||
|
||||
if (
|
||||
'livePreview' in clientConfig.admin &&
|
||||
clientConfig.admin.livePreview &&
|
||||
'url' in clientConfig.admin.livePreview
|
||||
) {
|
||||
delete clientConfig.admin.livePreview.url
|
||||
}
|
||||
|
||||
clientConfig.collections = createClientCollectionConfigs({
|
||||
break
|
||||
case 'collections':
|
||||
;(clientConfig.collections as ClientCollectionConfig[]) = createClientCollectionConfigs({
|
||||
collections: config.collections,
|
||||
defaultIDType: config.db.defaultIDType,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
clientConfig.globals = createClientGlobalConfigs({
|
||||
break
|
||||
case 'globals':
|
||||
;(clientConfig.globals as ClientGlobalConfig[]) = createClientGlobalConfigs({
|
||||
defaultIDType: config.db.defaultIDType,
|
||||
globals: config.globals,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
return clientConfig
|
||||
break
|
||||
case 'i18n':
|
||||
clientConfig.i18n = {
|
||||
fallbackLanguage: config.i18n.fallbackLanguage,
|
||||
translations: config.i18n.translations,
|
||||
}
|
||||
break
|
||||
case 'localization':
|
||||
if (typeof config.localization === 'object' && config.localization) {
|
||||
clientConfig.localization = {}
|
||||
if (config.localization.defaultLocale) {
|
||||
clientConfig.localization.defaultLocale = config.localization.defaultLocale
|
||||
}
|
||||
if (config.localization.fallback) {
|
||||
clientConfig.localization.fallback = config.localization.fallback
|
||||
}
|
||||
if (config.localization.localeCodes) {
|
||||
clientConfig.localization.localeCodes = config.localization.localeCodes
|
||||
}
|
||||
if (config.localization.locales) {
|
||||
clientConfig.localization.locales = []
|
||||
for (const locale of config.localization.locales) {
|
||||
if (locale) {
|
||||
const clientLocale: Partial<(typeof config.localization.locales)[0]> = {}
|
||||
if (locale.code) {
|
||||
clientLocale.code = locale.code
|
||||
}
|
||||
if (locale.fallbackLocale) {
|
||||
clientLocale.fallbackLocale = locale.fallbackLocale
|
||||
}
|
||||
if (locale.label) {
|
||||
clientLocale.label = locale.label
|
||||
}
|
||||
if (locale.rtl) {
|
||||
clientLocale.rtl = locale.rtl
|
||||
}
|
||||
clientConfig.localization.locales.push(clientLocale)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
clientConfig[key] = config[key]
|
||||
}
|
||||
}
|
||||
return clientConfig as ClientConfig
|
||||
}
|
||||
|
||||
@@ -39,19 +39,6 @@ export type ServerOnlyFieldProperties =
|
||||
|
||||
export type ServerOnlyFieldAdminProperties = keyof Pick<FieldBase['admin'], 'condition'>
|
||||
|
||||
export const createClientField = ({
|
||||
clientField = {} as ClientField,
|
||||
defaultIDType,
|
||||
field: incomingField,
|
||||
i18n,
|
||||
importMap,
|
||||
}: {
|
||||
clientField?: ClientField
|
||||
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||
field: Field
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientField => {
|
||||
const serverOnlyFieldProperties: Partial<ServerOnlyFieldProperties>[] = [
|
||||
'hooks',
|
||||
'access',
|
||||
@@ -70,30 +57,79 @@ export const createClientField = ({
|
||||
// `tabs`
|
||||
// `admin`
|
||||
]
|
||||
const serverOnlyFieldAdminProperties: Partial<ServerOnlyFieldAdminProperties>[] = ['condition']
|
||||
type FieldWithDescription = {
|
||||
admin: AdminClient
|
||||
} & ClientField
|
||||
|
||||
clientField.admin = clientField.admin || {}
|
||||
// clientField.admin.readOnly = true
|
||||
|
||||
serverOnlyFieldProperties.forEach((key) => {
|
||||
if (key in clientField) {
|
||||
delete clientField[key]
|
||||
}
|
||||
})
|
||||
export const createClientField = ({
|
||||
defaultIDType,
|
||||
field: incomingField,
|
||||
i18n,
|
||||
importMap,
|
||||
}: {
|
||||
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||
field: Field
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientField => {
|
||||
const clientField: ClientField = {} as ClientField
|
||||
|
||||
const isHidden = 'hidden' in incomingField && incomingField?.hidden
|
||||
const disabledFromAdmin =
|
||||
incomingField?.admin && 'disabled' in incomingField.admin && incomingField.admin.disabled
|
||||
|
||||
if (fieldAffectsData(clientField) && (isHidden || disabledFromAdmin)) {
|
||||
if (fieldAffectsData(incomingField) && (isHidden || disabledFromAdmin)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
'label' in clientField &&
|
||||
'label' in incomingField &&
|
||||
typeof incomingField.label === 'function'
|
||||
) {
|
||||
for (const key in incomingField) {
|
||||
if (serverOnlyFieldProperties.includes(key as any)) {
|
||||
continue
|
||||
}
|
||||
switch (key) {
|
||||
case 'admin':
|
||||
if (!incomingField.admin) {
|
||||
break
|
||||
}
|
||||
clientField.admin = {} as AdminClient
|
||||
for (const adminKey in incomingField.admin) {
|
||||
if (serverOnlyFieldAdminProperties.includes(adminKey as any)) {
|
||||
continue
|
||||
}
|
||||
switch (adminKey) {
|
||||
case 'description':
|
||||
if ('description' in incomingField.admin) {
|
||||
if (typeof incomingField.admin?.description !== 'function') {
|
||||
;(clientField as FieldWithDescription).admin.description =
|
||||
incomingField.admin.description
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
default:
|
||||
clientField.admin[adminKey] = incomingField.admin[adminKey]
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'blocks':
|
||||
case 'fields':
|
||||
case 'tabs':
|
||||
// Skip - we handle sub-fields in the switch below
|
||||
break
|
||||
case 'label':
|
||||
//@ts-expect-error - would need to type narrow
|
||||
if (typeof incomingField.label === 'function') {
|
||||
//@ts-expect-error - would need to type narrow
|
||||
clientField.label = incomingField.label({ t: i18n.t })
|
||||
} else {
|
||||
//@ts-expect-error - would need to type narrow
|
||||
clientField.label = incomingField.label
|
||||
}
|
||||
break
|
||||
default:
|
||||
clientField[key] = incomingField[key]
|
||||
}
|
||||
}
|
||||
|
||||
switch (incomingField.type) {
|
||||
@@ -108,7 +144,6 @@ export const createClientField = ({
|
||||
}
|
||||
|
||||
field.fields = createClientFields({
|
||||
clientFields: field.fields,
|
||||
defaultIDType,
|
||||
disableAddingID: incomingField.type !== 'array',
|
||||
fields: incomingField.fields,
|
||||
@@ -167,7 +202,6 @@ export const createClientField = ({
|
||||
}
|
||||
|
||||
clientBlock.fields = createClientFields({
|
||||
clientFields: clientBlock.fields,
|
||||
defaultIDType,
|
||||
fields: block.fields,
|
||||
i18n,
|
||||
@@ -225,24 +259,29 @@ export const createClientField = ({
|
||||
const field = clientField as unknown as TabsFieldClient
|
||||
|
||||
if (incomingField.tabs?.length) {
|
||||
field.tabs = []
|
||||
|
||||
for (let i = 0; i < incomingField.tabs.length; i++) {
|
||||
const tab = incomingField.tabs[i]
|
||||
const clientTab = field.tabs[i]
|
||||
const clientTab = {} as unknown as TabsFieldClient['tabs'][0]
|
||||
|
||||
serverOnlyFieldProperties.forEach((key) => {
|
||||
if (key in clientTab) {
|
||||
delete clientTab[key]
|
||||
for (const key in tab) {
|
||||
if (serverOnlyFieldProperties.includes(key as any)) {
|
||||
continue
|
||||
}
|
||||
})
|
||||
|
||||
if (key === 'fields') {
|
||||
clientTab.fields = createClientFields({
|
||||
clientFields: clientTab.fields,
|
||||
defaultIDType,
|
||||
disableAddingID: true,
|
||||
fields: tab.fields,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
} else {
|
||||
clientTab[key] = tab[key]
|
||||
}
|
||||
}
|
||||
field.tabs[i] = clientTab
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,70 +292,43 @@ export const createClientField = ({
|
||||
break
|
||||
}
|
||||
|
||||
const serverOnlyFieldAdminProperties: Partial<ServerOnlyFieldAdminProperties>[] = ['condition']
|
||||
|
||||
if (!clientField.admin) {
|
||||
clientField.admin = {} as AdminClient
|
||||
}
|
||||
|
||||
serverOnlyFieldAdminProperties.forEach((key) => {
|
||||
if (key in clientField.admin) {
|
||||
delete clientField.admin[key]
|
||||
}
|
||||
})
|
||||
|
||||
type FieldWithDescription = {
|
||||
admin: AdminClient
|
||||
} & ClientField
|
||||
|
||||
if (incomingField.admin && 'description' in incomingField.admin) {
|
||||
if (typeof incomingField.admin?.description === 'function') {
|
||||
delete (clientField as FieldWithDescription).admin.description
|
||||
} else {
|
||||
;(clientField as FieldWithDescription).admin.description = incomingField.admin.description
|
||||
}
|
||||
}
|
||||
|
||||
return clientField
|
||||
}
|
||||
|
||||
export const createClientFields = ({
|
||||
clientFields,
|
||||
defaultIDType,
|
||||
disableAddingID,
|
||||
fields,
|
||||
i18n,
|
||||
importMap,
|
||||
}: {
|
||||
clientFields: ClientField[]
|
||||
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||
disableAddingID?: boolean
|
||||
fields: Field[]
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientField[] => {
|
||||
const newClientFields: ClientField[] = []
|
||||
const clientFields: ClientField[] = []
|
||||
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i]
|
||||
|
||||
const newField = createClientField({
|
||||
clientField: clientFields[i],
|
||||
const clientField = createClientField({
|
||||
defaultIDType,
|
||||
field,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
if (newField) {
|
||||
newClientFields.push(newField)
|
||||
if (clientField) {
|
||||
clientFields.push(clientField)
|
||||
}
|
||||
}
|
||||
|
||||
const hasID = flattenTopLevelFields(fields).some((f) => fieldAffectsData(f) && f.name === 'id')
|
||||
|
||||
if (!disableAddingID && !hasID) {
|
||||
newClientFields.push({
|
||||
clientFields.push({
|
||||
name: 'id',
|
||||
type: defaultIDType,
|
||||
admin: {
|
||||
@@ -330,5 +342,5 @@ export const createClientFields = ({
|
||||
})
|
||||
}
|
||||
|
||||
return newClientFields
|
||||
return clientFields
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { ClientField, Field, TabAsField } from './config/types.js'
|
||||
|
||||
import { fieldAffectsData } from './config/types.js'
|
||||
import type { ClientField, Field, TabAsField, TabAsFieldClient } from './config/types.js'
|
||||
|
||||
type Args = {
|
||||
field: ClientField | Field | TabAsField
|
||||
field: ClientField | Field | TabAsField | TabAsFieldClient
|
||||
index: number
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
|
||||
@@ -10,14 +10,16 @@ import type { Payload } from '../../types/index.js'
|
||||
import type { SanitizedGlobalConfig } from './types.js'
|
||||
|
||||
import { type ClientField, createClientFields } from '../../fields/config/client.js'
|
||||
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
|
||||
|
||||
export type ServerOnlyGlobalProperties = keyof Pick<
|
||||
SanitizedGlobalConfig,
|
||||
'access' | 'admin' | 'custom' | 'endpoints' | 'fields' | 'flattenedFields' | 'hooks'
|
||||
>
|
||||
|
||||
export type ServerOnlyGlobalAdminProperties = keyof Pick<SanitizedGlobalConfig['admin'], 'hidden'>
|
||||
export type ServerOnlyGlobalAdminProperties = keyof Pick<
|
||||
SanitizedGlobalConfig['admin'],
|
||||
'components' | 'hidden'
|
||||
>
|
||||
|
||||
export type ClientGlobalConfig = {
|
||||
admin: {
|
||||
@@ -40,7 +42,10 @@ const serverOnlyProperties: Partial<ServerOnlyGlobalProperties>[] = [
|
||||
// `admin` is handled separately
|
||||
]
|
||||
|
||||
const serverOnlyGlobalAdminProperties: Partial<ServerOnlyGlobalAdminProperties>[] = ['hidden']
|
||||
const serverOnlyGlobalAdminProperties: Partial<ServerOnlyGlobalAdminProperties>[] = [
|
||||
'hidden',
|
||||
'components',
|
||||
]
|
||||
|
||||
export const createClientGlobalConfig = ({
|
||||
defaultIDType,
|
||||
@@ -53,44 +58,55 @@ export const createClientGlobalConfig = ({
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientGlobalConfig => {
|
||||
const clientGlobal = deepCopyObjectSimple(global, true) as unknown as ClientGlobalConfig
|
||||
const clientGlobal = {} as ClientGlobalConfig
|
||||
|
||||
for (const key in global) {
|
||||
if (serverOnlyProperties.includes(key as any)) {
|
||||
continue
|
||||
}
|
||||
switch (key) {
|
||||
case 'admin':
|
||||
if (!global.admin) {
|
||||
break
|
||||
}
|
||||
clientGlobal.admin = {} as ClientGlobalConfig['admin']
|
||||
for (const adminKey in global.admin) {
|
||||
if (serverOnlyGlobalAdminProperties.includes(adminKey as any)) {
|
||||
continue
|
||||
}
|
||||
switch (adminKey) {
|
||||
case 'livePreview':
|
||||
if (!global.admin.livePreview) {
|
||||
break
|
||||
}
|
||||
clientGlobal.admin.livePreview = {}
|
||||
if (global.admin.livePreview.breakpoints) {
|
||||
clientGlobal.admin.livePreview.breakpoints = global.admin.livePreview.breakpoints
|
||||
}
|
||||
break
|
||||
case 'preview':
|
||||
if (global.admin.preview) {
|
||||
clientGlobal.admin.preview = true
|
||||
}
|
||||
break
|
||||
default:
|
||||
clientGlobal.admin[adminKey] = global.admin[adminKey]
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'fields':
|
||||
clientGlobal.fields = createClientFields({
|
||||
clientFields: clientGlobal?.fields || [],
|
||||
defaultIDType,
|
||||
fields: global.fields,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
serverOnlyProperties.forEach((key) => {
|
||||
if (key in clientGlobal) {
|
||||
delete clientGlobal[key]
|
||||
break
|
||||
default: {
|
||||
clientGlobal[key] = global[key]
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
if (!clientGlobal.admin) {
|
||||
clientGlobal.admin = {} as ClientGlobalConfig['admin']
|
||||
}
|
||||
|
||||
serverOnlyGlobalAdminProperties.forEach((key) => {
|
||||
if (key in clientGlobal.admin) {
|
||||
delete clientGlobal.admin[key]
|
||||
}
|
||||
})
|
||||
|
||||
if (global.admin.preview) {
|
||||
clientGlobal.admin.preview = true
|
||||
}
|
||||
|
||||
clientGlobal.admin.components = null
|
||||
|
||||
if (
|
||||
'livePreview' in clientGlobal.admin &&
|
||||
clientGlobal.admin.livePreview &&
|
||||
'url' in clientGlobal.admin.livePreview
|
||||
) {
|
||||
delete clientGlobal.admin.livePreview.url
|
||||
}
|
||||
|
||||
return clientGlobal
|
||||
|
||||
@@ -90,6 +90,10 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
|
||||
// Clone the result - it may have come back memoized
|
||||
let result: any = deepCopyObjectSimple(results[0])
|
||||
|
||||
if (!result.version) {
|
||||
result.version = {}
|
||||
}
|
||||
|
||||
// Patch globalType onto version doc
|
||||
result.version.globalType = globalConfig.slug
|
||||
|
||||
|
||||
@@ -90,6 +90,10 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
|
||||
...paginatedDocs,
|
||||
docs: await Promise.all(
|
||||
paginatedDocs.docs.map(async (data) => {
|
||||
if (!data.version) {
|
||||
// Fallback if not selected
|
||||
;(data as any).version = {}
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
version: await afterRead<T>({
|
||||
|
||||
@@ -555,7 +555,7 @@ export class BasePayload {
|
||||
!checkedDependencies
|
||||
) {
|
||||
checkedDependencies = true
|
||||
await checkPayloadDependencies()
|
||||
void checkPayloadDependencies()
|
||||
}
|
||||
|
||||
this.importMap = options.importMap
|
||||
@@ -782,6 +782,12 @@ export const reload = async (
|
||||
if (payload.db.connect) {
|
||||
await payload.db.connect({ hotReload: true })
|
||||
}
|
||||
global._payload_clientConfig = null
|
||||
global._payload_schemaMap = null
|
||||
global._payload_clientSchemaMap = null
|
||||
global._payload_doNotCacheClientConfig = true // This will help refreshing the client config cache more reliably. If you remove this, please test HMR + client config refreshing (do new fields appear in the document?)
|
||||
global._payload_doNotCacheSchemaMap = true
|
||||
global._payload_doNotCacheClientSchemaMap = true
|
||||
}
|
||||
|
||||
export const getPayload = async (
|
||||
|
||||
@@ -212,10 +212,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(nodeKey)
|
||||
if (node && $isBlockNode(node)) {
|
||||
const newData = {
|
||||
...newFormStateData,
|
||||
blockType: formData.blockType,
|
||||
}
|
||||
const newData = newFormStateData
|
||||
newData.blockType = formData.blockType
|
||||
node.setFields(newData)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,7 +14,6 @@ import type { JSX } from 'react'
|
||||
|
||||
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
|
||||
import ObjectID from 'bson-objectid'
|
||||
import { deepCopyObjectSimple } from 'payload/shared'
|
||||
|
||||
type BaseBlockFields<TBlockFields extends JsonObject = JsonObject> = {
|
||||
/** Block form data */
|
||||
@@ -121,10 +120,8 @@ export class ServerBlockNode extends DecoratorBlockNode {
|
||||
}
|
||||
|
||||
setFields(fields: BlockFields): void {
|
||||
const fieldsCopy = deepCopyObjectSimple(fields)
|
||||
|
||||
const writable = this.getWritable()
|
||||
writable.__fields = fieldsCopy
|
||||
writable.__fields = fields
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { JSX } from 'react'
|
||||
|
||||
import ObjectID from 'bson-objectid'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import { deepCopyObjectSimple } from 'payload/shared'
|
||||
|
||||
export type InlineBlockFields = {
|
||||
/** Block form data */
|
||||
@@ -108,10 +107,8 @@ export class ServerInlineBlockNode extends DecoratorNode<null | React.ReactEleme
|
||||
}
|
||||
|
||||
setFields(fields: InlineBlockFields): void {
|
||||
const fieldsCopy = deepCopyObjectSimple(fields)
|
||||
|
||||
const writable = this.getWritable()
|
||||
writable.__fields = fieldsCopy
|
||||
writable.__fields = fields
|
||||
}
|
||||
|
||||
updateDOM(): boolean {
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
|
||||
import escapeHTML from 'escape-html'
|
||||
import { sanitizeFields } from 'payload'
|
||||
import { deepCopyObject } from 'payload/shared'
|
||||
|
||||
import type { ClientProps } from '../client/index.js'
|
||||
|
||||
@@ -78,7 +77,7 @@ export const LinkFeature = createServerFeature<
|
||||
const validRelationships = _config.collections.map((c) => c.slug) || []
|
||||
|
||||
const _transformedFields = transformExtraFields(
|
||||
props.fields ? deepCopyObject(props.fields) : null,
|
||||
props.fields ? props.fields : null,
|
||||
_config,
|
||||
props.enabledCollections,
|
||||
props.disabledCollections,
|
||||
@@ -97,7 +96,7 @@ export const LinkFeature = createServerFeature<
|
||||
// the text field is not included in the node data.
|
||||
// Thus, for tasks like validation, we do not want to pass it a text field in the schema which will never have data.
|
||||
// Otherwise, it will cause a validation error (field is required).
|
||||
const sanitizedFieldsWithoutText = deepCopyObject(sanitizedFields).filter(
|
||||
const sanitizedFieldsWithoutText = sanitizedFields.filter(
|
||||
(field) => !('name' in field) || field.name !== 'text',
|
||||
)
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ export const RscEntryLexicalField: React.FC<
|
||||
const field: RichTextFieldType = args.field as RichTextFieldType
|
||||
const path = args.path ?? (args.clientField as RichTextFieldClient).name
|
||||
const schemaPath = args.schemaPath ?? path
|
||||
|
||||
const { clientFeatures, featureClientSchemaMap } = initLexicalFeatures({
|
||||
clientFieldSchemaMap: args.clientFieldSchemaMap,
|
||||
fieldSchemaMap: args.fieldSchemaMap,
|
||||
i18n: args.i18n,
|
||||
path,
|
||||
@@ -43,6 +45,7 @@ export const RscEntryLexicalField: React.FC<
|
||||
initialLexicalFormState = await buildInitialState({
|
||||
context: {
|
||||
id: args.id,
|
||||
clientFieldSchemaMap: args.clientFieldSchemaMap,
|
||||
collectionSlug: args.collectionSlug,
|
||||
field,
|
||||
fieldSchemaMap: args.fieldSchemaMap,
|
||||
@@ -60,7 +63,7 @@ export const RscEntryLexicalField: React.FC<
|
||||
const props: LexicalRichTextFieldProps = {
|
||||
admin: args.admin,
|
||||
clientFeatures,
|
||||
featureClientSchemaMap,
|
||||
featureClientSchemaMap, // TODO: Does client need this? Why cant this just live in the server
|
||||
field: args.clientField as RichTextFieldClient,
|
||||
forceRender: args.forceRender,
|
||||
initialLexicalFormState,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
import { type SanitizedServerEditorConfig } from './index.js'
|
||||
import { defaultEditorConfig } from './lexical/config/server/default.js'
|
||||
import { sanitizeServerEditorConfig } from './lexical/config/server/sanitize.js'
|
||||
|
||||
let cachedDefaultSanitizedServerEditorConfig:
|
||||
| null
|
||||
| Promise<SanitizedServerEditorConfig>
|
||||
| SanitizedServerEditorConfig = (global as any)
|
||||
._payload_lexical_defaultSanitizedServerEditorConfig
|
||||
|
||||
if (!cachedDefaultSanitizedServerEditorConfig) {
|
||||
cachedDefaultSanitizedServerEditorConfig = (
|
||||
global as any
|
||||
)._payload_lexical_defaultSanitizedServerEditorConfig = null
|
||||
}
|
||||
|
||||
export const getDefaultSanitizedEditorConfig = cache(
|
||||
async (args: {
|
||||
config: SanitizedConfig
|
||||
parentIsLocalized: boolean
|
||||
}): Promise<SanitizedServerEditorConfig> => {
|
||||
const { config, parentIsLocalized } = args
|
||||
|
||||
if (cachedDefaultSanitizedServerEditorConfig) {
|
||||
return await cachedDefaultSanitizedServerEditorConfig
|
||||
}
|
||||
|
||||
cachedDefaultSanitizedServerEditorConfig = sanitizeServerEditorConfig(
|
||||
defaultEditorConfig,
|
||||
config,
|
||||
parentIsLocalized,
|
||||
)
|
||||
;(global as any).payload_lexical_defaultSanitizedServerEditorConfig =
|
||||
cachedDefaultSanitizedServerEditorConfig
|
||||
|
||||
cachedDefaultSanitizedServerEditorConfig = await cachedDefaultSanitizedServerEditorConfig
|
||||
;(global as any).payload_lexical_defaultSanitizedServerEditorConfig =
|
||||
cachedDefaultSanitizedServerEditorConfig
|
||||
|
||||
return cachedDefaultSanitizedServerEditorConfig
|
||||
},
|
||||
)
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
beforeChangeTraverseFields,
|
||||
beforeValidateTraverseFields,
|
||||
checkDependencies,
|
||||
deepCopyObject,
|
||||
deepCopyObjectSimple,
|
||||
withNullableJSONSchemaType,
|
||||
} from 'payload'
|
||||
|
||||
@@ -21,31 +19,27 @@ import type {
|
||||
LexicalRichTextAdapterProvider,
|
||||
} from './types.js'
|
||||
|
||||
import { getDefaultSanitizedEditorConfig } from './getDefaultSanitizedEditorConfig.js'
|
||||
import { i18n } from './i18n.js'
|
||||
import { defaultEditorConfig, defaultEditorFeatures } from './lexical/config/server/default.js'
|
||||
import { loadFeatures } from './lexical/config/server/loader.js'
|
||||
import {
|
||||
sanitizeServerEditorConfig,
|
||||
sanitizeServerFeatures,
|
||||
} from './lexical/config/server/sanitize.js'
|
||||
import { sanitizeServerFeatures } from './lexical/config/server/sanitize.js'
|
||||
import { populateLexicalPopulationPromises } from './populateGraphQL/populateLexicalPopulationPromises.js'
|
||||
import { getGenerateImportMap } from './utilities/generateImportMap.js'
|
||||
import { getGenerateSchemaMap } from './utilities/generateSchemaMap.js'
|
||||
import { recurseNodeTree } from './utilities/recurseNodeTree.js'
|
||||
import { richTextValidateHOC } from './validate/index.js'
|
||||
|
||||
let defaultSanitizedServerEditorConfig: null | SanitizedServerEditorConfig = null
|
||||
let checkedDependencies = false
|
||||
|
||||
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
|
||||
return async ({ config, isRoot, parentIsLocalized }) => {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' &&
|
||||
!checkedDependencies
|
||||
) {
|
||||
checkedDependencies = true
|
||||
await checkDependencies({
|
||||
void checkDependencies({
|
||||
dependencyGroups: [
|
||||
{
|
||||
name: 'lexical',
|
||||
@@ -65,45 +59,40 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return async ({ config, isRoot, parentIsLocalized }) => {
|
||||
let features: FeatureProviderServer<unknown, unknown, unknown>[] = []
|
||||
let resolvedFeatureMap: ResolvedServerFeatureMap
|
||||
|
||||
let finalSanitizedEditorConfig: SanitizedServerEditorConfig // For server only
|
||||
if (!props || (!props.features && !props.lexical)) {
|
||||
if (!defaultSanitizedServerEditorConfig) {
|
||||
defaultSanitizedServerEditorConfig = await sanitizeServerEditorConfig(
|
||||
defaultEditorConfig,
|
||||
finalSanitizedEditorConfig = await getDefaultSanitizedEditorConfig({
|
||||
config,
|
||||
parentIsLocalized,
|
||||
)
|
||||
features = deepCopyObject(defaultEditorFeatures)
|
||||
}
|
||||
})
|
||||
|
||||
finalSanitizedEditorConfig = deepCopyObject(defaultSanitizedServerEditorConfig)
|
||||
|
||||
delete finalSanitizedEditorConfig.lexical // We don't want to send the default lexical editor config to the client
|
||||
features = defaultEditorFeatures
|
||||
|
||||
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
|
||||
} else {
|
||||
if (props.features && typeof props.features === 'function') {
|
||||
const rootEditor = config.editor
|
||||
let rootEditorFeatures: FeatureProviderServer<unknown, unknown, unknown>[] = []
|
||||
if (typeof rootEditor === 'object' && 'features' in rootEditor) {
|
||||
rootEditorFeatures = (rootEditor as LexicalRichTextAdapter).features
|
||||
}
|
||||
|
||||
features =
|
||||
props.features && typeof props.features === 'function'
|
||||
? props.features({
|
||||
defaultFeatures: deepCopyObject(defaultEditorFeatures),
|
||||
features = props.features({
|
||||
defaultFeatures: defaultEditorFeatures,
|
||||
rootFeatures: rootEditorFeatures,
|
||||
})
|
||||
: (props.features as FeatureProviderServer<unknown, unknown, unknown>[])
|
||||
if (!features) {
|
||||
features = deepCopyObject(defaultEditorFeatures)
|
||||
} else {
|
||||
features = props.features as FeatureProviderServer<unknown, unknown, unknown>[]
|
||||
}
|
||||
|
||||
const lexical = props.lexical ?? deepCopyObjectSimple(defaultEditorConfig.lexical)!
|
||||
if (!features) {
|
||||
features = defaultEditorFeatures
|
||||
}
|
||||
|
||||
const lexical = props.lexical ?? defaultEditorConfig.lexical
|
||||
|
||||
resolvedFeatureMap = await loadFeatures({
|
||||
config,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import type {
|
||||
FeatureProviderServer,
|
||||
ResolvedServerFeature,
|
||||
ResolvedServerFeatureMap,
|
||||
ServerFeatureProviderMap,
|
||||
} from '../../../features/typesServer.js'
|
||||
@@ -186,14 +187,21 @@ export async function loadFeatures({
|
||||
unSanitizedEditorConfig,
|
||||
})
|
||||
: featureProvider.feature
|
||||
resolvedFeatures.set(featureProvider.key, {
|
||||
...feature,
|
||||
dependencies: featureProvider.dependencies!,
|
||||
dependenciesPriority: featureProvider.dependenciesPriority!,
|
||||
dependenciesSoft: featureProvider.dependenciesSoft!,
|
||||
key: featureProvider.key,
|
||||
order: loaded,
|
||||
})
|
||||
|
||||
const resolvedFeature: ResolvedServerFeature<any, any> = feature as ResolvedServerFeature<
|
||||
any,
|
||||
any
|
||||
>
|
||||
|
||||
// All these new properties would be added to the feature, as it's mutated. However, this does not cause any damage and allows
|
||||
// us to prevent an unnecessary spread operation.
|
||||
resolvedFeature.key = featureProvider.key
|
||||
resolvedFeature.order = loaded
|
||||
resolvedFeature.dependencies = featureProvider.dependencies!
|
||||
resolvedFeature.dependenciesPriority = featureProvider.dependenciesPriority!
|
||||
resolvedFeature.dependenciesSoft = featureProvider.dependenciesSoft!
|
||||
|
||||
resolvedFeatures.set(featureProvider.key, resolvedFeature)
|
||||
|
||||
loaded++
|
||||
}
|
||||
|
||||
@@ -80,10 +80,11 @@ export type LexicalRichTextAdapterProvider =
|
||||
parentIsLocalized: boolean
|
||||
}) => Promise<LexicalRichTextAdapter>
|
||||
|
||||
export type FeatureClientSchemaMap = {
|
||||
[featureKey: string]: {
|
||||
export type SingleFeatureClientSchemaMap = {
|
||||
[key: string]: ClientField[]
|
||||
}
|
||||
export type FeatureClientSchemaMap = {
|
||||
[featureKey: string]: SingleFeatureClientSchemaMap
|
||||
}
|
||||
|
||||
export type LexicalRichTextFieldProps = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
import type {
|
||||
ClientFieldSchemaMap,
|
||||
DocumentPreferences,
|
||||
FieldSchemaMap,
|
||||
FormState,
|
||||
@@ -22,6 +23,7 @@ export type InitialLexicalFormState = {
|
||||
|
||||
type Props = {
|
||||
context: {
|
||||
clientFieldSchemaMap: ClientFieldSchemaMap
|
||||
collectionSlug: string
|
||||
field: RichTextField
|
||||
fieldSchemaMap: FieldSchemaMap
|
||||
@@ -68,6 +70,7 @@ export async function buildInitialState({
|
||||
|
||||
const formStateResult = await fieldSchemasToFormState({
|
||||
id: context.id,
|
||||
clientFieldSchemaMap: context.clientFieldSchemaMap,
|
||||
collectionSlug: context.collectionSlug,
|
||||
data: blockNode.fields,
|
||||
fields: (context.fieldSchemaMap.get(schemaFieldsPath) as any)?.fields,
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
|
||||
import {
|
||||
type ClientField,
|
||||
createClientFields,
|
||||
deepCopyObjectSimple,
|
||||
type FieldSchemaMap,
|
||||
type Payload,
|
||||
} from 'payload'
|
||||
import { type ClientFieldSchemaMap, type FieldSchemaMap, type Payload } from 'payload'
|
||||
import { getFromImportMap } from 'payload/shared'
|
||||
|
||||
import type { FeatureProviderProviderClient } from '../features/typesClient.js'
|
||||
import type { SanitizedServerEditorConfig } from '../lexical/config/types.js'
|
||||
import type { FeatureClientSchemaMap, LexicalRichTextFieldProps } from '../types.js'
|
||||
type Args = {
|
||||
clientFieldSchemaMap: ClientFieldSchemaMap
|
||||
fieldSchemaMap: FieldSchemaMap
|
||||
i18n: I18nClient
|
||||
path: string
|
||||
@@ -27,9 +22,6 @@ export function initLexicalFeatures(args: Args): {
|
||||
} {
|
||||
const clientFeatures: LexicalRichTextFieldProps['clientFeatures'] = {}
|
||||
|
||||
const fieldSchemaMap = Object.fromEntries(new Map(args.fieldSchemaMap))
|
||||
//&const value = deepCopyObjectSimple(args.fieldState.value)
|
||||
|
||||
// turn args.resolvedFeatureMap into an array of [key, value] pairs, ordered by value.order, lowest order first:
|
||||
const resolvedFeatureMapArray = Array.from(
|
||||
args.sanitizedEditorConfig.resolvedFeatureMap.entries(),
|
||||
@@ -84,66 +76,16 @@ export function initLexicalFeatures(args: Args): {
|
||||
featureKey,
|
||||
].join('.')
|
||||
|
||||
const featurePath = [...args.path.split('.'), 'lexical_internal_feature', featureKey].join(
|
||||
'.',
|
||||
)
|
||||
|
||||
// Like args.fieldSchemaMap, we only want to include the sub-fields of the current feature
|
||||
const featureSchemaMap: typeof fieldSchemaMap = {}
|
||||
for (const key in fieldSchemaMap) {
|
||||
const state = fieldSchemaMap[key]
|
||||
|
||||
if (key.startsWith(featureSchemaPath)) {
|
||||
featureSchemaMap[key] = state
|
||||
}
|
||||
}
|
||||
|
||||
featureClientSchemaMap[featureKey] = {}
|
||||
|
||||
for (const key in featureSchemaMap) {
|
||||
const state = featureSchemaMap[key]
|
||||
|
||||
const clientFields = createClientFields({
|
||||
clientFields: ('fields' in state
|
||||
? deepCopyObjectSimple(state.fields)
|
||||
: [deepCopyObjectSimple(state)]) as ClientField[],
|
||||
defaultIDType: args.payload.config.db.defaultIDType,
|
||||
disableAddingID: true,
|
||||
fields: 'fields' in state ? state.fields : [state],
|
||||
i18n: args.i18n,
|
||||
importMap: args.payload.importMap,
|
||||
})
|
||||
featureClientSchemaMap[featureKey][key] = clientFields
|
||||
}
|
||||
|
||||
/*
|
||||
This is for providing an initial form state. Right now we only want to provide the clientfields though
|
||||
const schemaMap: {
|
||||
[key: string]: FieldState
|
||||
} = {}
|
||||
|
||||
const lexicalDeepIterate = (editorState) => {
|
||||
console.log('STATE', editorState)
|
||||
|
||||
if (
|
||||
editorState &&
|
||||
typeof editorState === 'object' &&
|
||||
'children' in editorState &&
|
||||
Array.isArray(editorState.children)
|
||||
) {
|
||||
for (const childKey in editorState.children) {
|
||||
const childState = editorState.children[childKey]
|
||||
|
||||
if (childState && typeof childState === 'object') {
|
||||
lexicalDeepIterate(childState)
|
||||
// Like args.fieldSchemaMap, we only want to include the sub-fields of the current feature
|
||||
for (const [key, entry] of args.clientFieldSchemaMap.entries()) {
|
||||
if (key.startsWith(featureSchemaPath)) {
|
||||
featureClientSchemaMap[featureKey][key] = 'fields' in entry ? entry.fields : [entry]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lexicalDeepIterate(value.root)*/
|
||||
}
|
||||
}
|
||||
return {
|
||||
clientFeatures,
|
||||
featureClientSchemaMap,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { createClientFields, deepCopyObjectSimple } from 'payload'
|
||||
import { createClientFields } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import type { AdapterArguments, RichTextCustomElement, RichTextCustomLeaf } from '../types.js'
|
||||
@@ -134,11 +134,7 @@ export const RscEntrySlateField: React.FC<
|
||||
|
||||
switch (element.name) {
|
||||
case 'link': {
|
||||
let clientFields = deepCopyObjectSimple(
|
||||
args.admin?.link?.fields,
|
||||
) as unknown as ClientField[]
|
||||
clientFields = createClientFields({
|
||||
clientFields,
|
||||
const clientFields = createClientFields({
|
||||
defaultIDType: payload.config.db.defaultIDType,
|
||||
fields: args.admin?.link?.fields as Field[],
|
||||
i18n,
|
||||
@@ -166,11 +162,7 @@ export const RscEntrySlateField: React.FC<
|
||||
|
||||
uploadEnabledCollections.forEach((collection) => {
|
||||
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
|
||||
let clientFields = deepCopyObjectSimple(
|
||||
args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
) as unknown as ClientField[]
|
||||
clientFields = createClientFields({
|
||||
clientFields,
|
||||
const clientFields = createClientFields({
|
||||
defaultIDType: payload.config.db.defaultIDType,
|
||||
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
i18n,
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
"types": "./src/utilities/buildTableState.ts",
|
||||
"default": "./src/utilities/buildTableState.ts"
|
||||
},
|
||||
"./utilities/getClientConfig": {
|
||||
"import": "./src/utilities/getClientConfig.ts",
|
||||
"types": "./src/utilities/getClientConfig.ts",
|
||||
"default": "./src/utilities/getClientConfig.ts"
|
||||
},
|
||||
"./utilities/buildFieldSchemaMap/traverseFields": {
|
||||
"import": "./src/utilities/buildFieldSchemaMap/traverseFields.ts",
|
||||
"types": "./src/utilities/buildFieldSchemaMap/traverseFields.ts",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ClientFieldSchemaMap,
|
||||
Data,
|
||||
DocumentPreferences,
|
||||
Field,
|
||||
@@ -35,6 +36,7 @@ export type AddFieldStatePromiseArgs = {
|
||||
* if all parents are localized, then the field is localized
|
||||
*/
|
||||
anyParentLocalized?: boolean
|
||||
clientFieldSchemaMap?: ClientFieldSchemaMap
|
||||
collectionSlug?: string
|
||||
data: Data
|
||||
field: Field
|
||||
@@ -93,6 +95,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized = false,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data,
|
||||
field,
|
||||
@@ -119,6 +122,12 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
state,
|
||||
} = args
|
||||
|
||||
if (!args.clientFieldSchemaMap && args.renderFieldFn) {
|
||||
console.warn(
|
||||
'clientFieldSchemaMap is not passed to addFieldStatePromise - this will reduce performance',
|
||||
)
|
||||
}
|
||||
|
||||
const { indexPath, path, schemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
@@ -237,6 +246,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
id,
|
||||
addErrorPathToParent,
|
||||
anyParentLocalized: field.localized || anyParentLocalized,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data: row,
|
||||
fields: field.fields,
|
||||
@@ -370,6 +380,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
id,
|
||||
addErrorPathToParent,
|
||||
anyParentLocalized: field.localized || anyParentLocalized,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data: row,
|
||||
fields: block.fields,
|
||||
@@ -460,6 +471,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
id,
|
||||
addErrorPathToParent,
|
||||
anyParentLocalized: field.localized || anyParentLocalized,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data: data?.[field.name] || {},
|
||||
fields: field.fields,
|
||||
@@ -605,6 +617,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
// passthrough parent functionality
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized: field.localized || anyParentLocalized,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data,
|
||||
fields: field.fields,
|
||||
@@ -668,6 +681,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized: tab.localized || anyParentLocalized,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data: isNamedTab ? data?.[tab.name] || {} : data,
|
||||
fields: tab.fields,
|
||||
@@ -733,6 +747,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
|
||||
renderFieldFn({
|
||||
id,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data: fullData,
|
||||
fieldConfig: fieldConfig as Field,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ClientFieldSchemaMap,
|
||||
Data,
|
||||
DocumentPreferences,
|
||||
Field,
|
||||
@@ -15,6 +16,12 @@ import { calculateDefaultValues } from './calculateDefaultValues/index.js'
|
||||
import { iterateFields } from './iterateFields.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* The client field schema map is required for field rendering.
|
||||
* If fields should not be rendered (=> `renderFieldFn` is not provided),
|
||||
* then the client field schema map is not required.
|
||||
*/
|
||||
clientFieldSchemaMap?: ClientFieldSchemaMap
|
||||
collectionSlug?: string
|
||||
data?: Data
|
||||
fields: Field[] | undefined
|
||||
@@ -40,12 +47,19 @@ type Args = {
|
||||
renderAllFields: boolean
|
||||
renderFieldFn?: RenderFieldMethod
|
||||
req: PayloadRequest
|
||||
|
||||
schemaPath: string
|
||||
}
|
||||
|
||||
export const fieldSchemasToFormState = async (args: Args): Promise<FormState> => {
|
||||
if (!args.clientFieldSchemaMap && args.renderFieldFn) {
|
||||
console.warn(
|
||||
'clientFieldSchemaMap is not passed to fieldSchemasToFormState - this will reduce performance',
|
||||
)
|
||||
}
|
||||
const {
|
||||
id,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data = {},
|
||||
fields,
|
||||
@@ -77,6 +91,7 @@ export const fieldSchemasToFormState = async (args: Args): Promise<FormState> =>
|
||||
await iterateFields({
|
||||
id,
|
||||
addErrorPathToParent: null,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data: dataWithDefaultValues,
|
||||
fields,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ClientFieldSchemaMap,
|
||||
Data,
|
||||
DocumentPreferences,
|
||||
Field as FieldSchema,
|
||||
@@ -20,6 +21,7 @@ type Args = {
|
||||
* if any parents is localized, then the field is localized. @default false
|
||||
*/
|
||||
anyParentLocalized?: boolean
|
||||
clientFieldSchemaMap?: ClientFieldSchemaMap
|
||||
collectionSlug?: string
|
||||
data: Data
|
||||
fields: FieldSchema[]
|
||||
@@ -71,6 +73,7 @@ export const iterateFields = async ({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized = false,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data,
|
||||
fields,
|
||||
@@ -112,6 +115,7 @@ export const iterateFields = async ({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data,
|
||||
field,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ClientComponentProps, ClientField, FieldPaths, ServerComponentProps } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { createClientField, deepCopyObjectSimple, MissingEditorProp } from 'payload'
|
||||
import { createClientField, MissingEditorProp } from 'payload'
|
||||
|
||||
import type { RenderFieldMethod } from './types.js'
|
||||
|
||||
@@ -18,6 +18,7 @@ const defaultUIFieldComponentKeys: Array<'Cell' | 'Description' | 'Field' | 'Fil
|
||||
|
||||
export const renderField: RenderFieldMethod = ({
|
||||
id,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data,
|
||||
fieldConfig,
|
||||
@@ -35,8 +36,9 @@ export const renderField: RenderFieldMethod = ({
|
||||
schemaPath,
|
||||
siblingData,
|
||||
}) => {
|
||||
const clientField = createClientField({
|
||||
clientField: deepCopyObjectSimple(fieldConfig) as ClientField,
|
||||
const clientField = clientFieldSchemaMap
|
||||
? (clientFieldSchemaMap.get(schemaPath) as ClientField)
|
||||
: createClientField({
|
||||
defaultIDType: req.payload.config.db.defaultIDType,
|
||||
field: fieldConfig,
|
||||
i18n: req.i18n,
|
||||
@@ -61,6 +63,7 @@ export const renderField: RenderFieldMethod = ({
|
||||
const serverProps: ServerComponentProps = {
|
||||
id,
|
||||
clientField,
|
||||
clientFieldSchemaMap,
|
||||
data,
|
||||
field: fieldConfig,
|
||||
fieldSchemaMap,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ClientFieldSchemaMap,
|
||||
Data,
|
||||
DocumentPreferences,
|
||||
Field,
|
||||
@@ -11,6 +12,7 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
export type RenderFieldArgs = {
|
||||
clientFieldSchemaMap?: ClientFieldSchemaMap
|
||||
collectionSlug: string
|
||||
data: Data
|
||||
fieldConfig: Field
|
||||
|
||||
@@ -14,7 +14,7 @@ const LocaleContext = createContext({} as Locale)
|
||||
|
||||
export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const {
|
||||
config: { localization },
|
||||
config: { localization = false },
|
||||
} = useConfig()
|
||||
|
||||
const { user } = useAuth()
|
||||
|
||||
94
packages/ui/src/utilities/buildClientFieldSchemaMap/index.ts
Normal file
94
packages/ui/src/utilities/buildClientFieldSchemaMap/index.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type {
|
||||
ClientConfig,
|
||||
ClientField,
|
||||
ClientFieldSchemaMap,
|
||||
FieldSchemaMap,
|
||||
Payload,
|
||||
TextFieldClient,
|
||||
} from 'payload'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
const baseAuthFields: ClientField[] = [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'confirm-password',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Flattens the config fields into a map of field schemas
|
||||
*/
|
||||
export const buildClientFieldSchemaMap = (args: {
|
||||
collectionSlug?: string
|
||||
config: ClientConfig
|
||||
globalSlug?: string
|
||||
i18n: I18n
|
||||
payload: Payload
|
||||
schemaMap: FieldSchemaMap
|
||||
}): { clientFieldSchemaMap: ClientFieldSchemaMap } => {
|
||||
const { collectionSlug, config, globalSlug, i18n, payload, schemaMap } = args
|
||||
|
||||
const clientSchemaMap: ClientFieldSchemaMap = new Map()
|
||||
|
||||
if (collectionSlug) {
|
||||
const matchedCollection = config.collections.find(
|
||||
(collection) => collection.slug === collectionSlug,
|
||||
)
|
||||
|
||||
if (matchedCollection) {
|
||||
if (matchedCollection.auth && !matchedCollection.auth.disableLocalStrategy) {
|
||||
// register schema with auth schemaPath
|
||||
;(baseAuthFields[0] as TextFieldClient).label = i18n.t('general:password')
|
||||
;(baseAuthFields[1] as TextFieldClient).label = i18n.t('authentication:confirmPassword')
|
||||
|
||||
clientSchemaMap.set(`_${matchedCollection.slug}.auth`, {
|
||||
fields: [...baseAuthFields, ...matchedCollection.fields],
|
||||
})
|
||||
}
|
||||
|
||||
clientSchemaMap.set(collectionSlug, {
|
||||
fields: matchedCollection.fields,
|
||||
})
|
||||
|
||||
traverseFields({
|
||||
clientSchemaMap,
|
||||
config,
|
||||
fields: matchedCollection.fields,
|
||||
i18n,
|
||||
parentIndexPath: '',
|
||||
parentSchemaPath: collectionSlug,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
}
|
||||
} else if (globalSlug) {
|
||||
const matchedGlobal = config.globals.find((global) => global.slug === globalSlug)
|
||||
|
||||
if (matchedGlobal) {
|
||||
clientSchemaMap.set(globalSlug, {
|
||||
fields: matchedGlobal.fields,
|
||||
})
|
||||
|
||||
traverseFields({
|
||||
clientSchemaMap,
|
||||
config,
|
||||
fields: matchedGlobal.fields,
|
||||
i18n,
|
||||
parentIndexPath: '',
|
||||
parentSchemaPath: globalSlug,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { clientFieldSchemaMap: clientSchemaMap }
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
|
||||
import {
|
||||
type ClientConfig,
|
||||
type ClientField,
|
||||
type ClientFieldSchemaMap,
|
||||
createClientFields,
|
||||
type FieldSchemaMap,
|
||||
type Payload,
|
||||
} from 'payload'
|
||||
import { getFieldPaths, tabHasName } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
clientSchemaMap: ClientFieldSchemaMap
|
||||
config: ClientConfig
|
||||
fields: ClientField[]
|
||||
i18n: I18n<any, any>
|
||||
parentIndexPath: string
|
||||
parentSchemaPath: string
|
||||
payload: Payload
|
||||
schemaMap: FieldSchemaMap
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
clientSchemaMap,
|
||||
config,
|
||||
fields,
|
||||
i18n,
|
||||
parentIndexPath,
|
||||
parentSchemaPath,
|
||||
payload,
|
||||
schemaMap,
|
||||
}: Args) => {
|
||||
for (const [index, field] of fields.entries()) {
|
||||
const { indexPath, schemaPath } = getFieldPaths({
|
||||
field,
|
||||
index,
|
||||
parentIndexPath: 'name' in field ? '' : parentIndexPath,
|
||||
parentPath: '',
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
clientSchemaMap.set(schemaPath, field)
|
||||
|
||||
switch (field.type) {
|
||||
case 'array':
|
||||
case 'group':
|
||||
traverseFields({
|
||||
clientSchemaMap,
|
||||
config,
|
||||
fields: field.fields,
|
||||
i18n,
|
||||
parentIndexPath: '',
|
||||
parentSchemaPath: schemaPath,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
field.blocks.map((block) => {
|
||||
const blockSchemaPath = `${schemaPath}.${block.slug}`
|
||||
|
||||
clientSchemaMap.set(blockSchemaPath, block)
|
||||
traverseFields({
|
||||
clientSchemaMap,
|
||||
config,
|
||||
fields: block.fields,
|
||||
i18n,
|
||||
parentIndexPath: '',
|
||||
parentSchemaPath: blockSchemaPath,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
})
|
||||
|
||||
break
|
||||
case 'collapsible':
|
||||
case 'row':
|
||||
traverseFields({
|
||||
clientSchemaMap,
|
||||
config,
|
||||
fields: field.fields,
|
||||
i18n,
|
||||
parentIndexPath: indexPath,
|
||||
parentSchemaPath,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
break
|
||||
|
||||
case 'richText': {
|
||||
// richText sub-fields are not part of the ClientConfig or the Config.
|
||||
// They only exist in the field schema map.
|
||||
// Thus, we need to
|
||||
// 1. get them from the field schema map
|
||||
// 2. convert them to client fields
|
||||
// 3. add them to the client schema map
|
||||
|
||||
// So these would basically be all fields that are not part of the client config already
|
||||
const richTextFieldSchemaMap: FieldSchemaMap = new Map()
|
||||
for (const [path, subField] of schemaMap.entries()) {
|
||||
if (path.startsWith(`${schemaPath}.`)) {
|
||||
richTextFieldSchemaMap.set(path, subField)
|
||||
}
|
||||
}
|
||||
|
||||
// Now loop through them, convert each entry to a client field and add it to the client schema map
|
||||
for (const [path, subField] of richTextFieldSchemaMap.entries()) {
|
||||
const clientFields = createClientFields({
|
||||
defaultIDType: payload.config.db.defaultIDType,
|
||||
disableAddingID: true,
|
||||
fields: 'fields' in subField ? subField.fields : [subField],
|
||||
i18n,
|
||||
importMap: payload.importMap,
|
||||
})
|
||||
clientSchemaMap.set(path, {
|
||||
fields: clientFields,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'tabs':
|
||||
field.tabs.map((tab, tabIndex) => {
|
||||
const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({
|
||||
field: {
|
||||
...tab,
|
||||
type: 'tab',
|
||||
},
|
||||
index: tabIndex,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath: '',
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
clientSchemaMap.set(tabSchemaPath, tab)
|
||||
|
||||
traverseFields({
|
||||
clientSchemaMap,
|
||||
config,
|
||||
fields: tab.fields,
|
||||
i18n,
|
||||
parentIndexPath: tabHasName(tab) ? '' : tabIndexPath,
|
||||
parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,25 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { Field, FieldSchemaMap, SanitizedConfig } from 'payload'
|
||||
import type { Field, FieldSchemaMap, SanitizedConfig, TextField } from 'payload'
|
||||
|
||||
import { confirmPassword, password } from 'payload/shared'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
const baseAuthFields: Field[] = [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
required: true,
|
||||
validate: password,
|
||||
},
|
||||
{
|
||||
name: 'confirm-password',
|
||||
type: 'text',
|
||||
required: true,
|
||||
validate: confirmPassword,
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Flattens the config fields into a map of field schemas
|
||||
*/
|
||||
@@ -25,22 +41,8 @@ export const buildFieldSchemaMap = (args: {
|
||||
if (matchedCollection) {
|
||||
if (matchedCollection.auth && !matchedCollection.auth.disableLocalStrategy) {
|
||||
// register schema with auth schemaPath
|
||||
const baseAuthFields: Field[] = [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
label: i18n.t('general:password'),
|
||||
required: true,
|
||||
validate: password,
|
||||
},
|
||||
{
|
||||
name: 'confirm-password',
|
||||
type: 'text',
|
||||
label: i18n.t('authentication:confirmPassword'),
|
||||
required: true,
|
||||
validate: confirmPassword,
|
||||
},
|
||||
]
|
||||
;(baseAuthFields[0] as TextField).label = i18n.t('general:password')
|
||||
;(baseAuthFields[1] as TextField).label = i18n.t('authentication:confirmPassword')
|
||||
|
||||
schemaMap.set(`_${matchedCollection.slug}.auth`, {
|
||||
fields: [...baseAuthFields, ...matchedCollection.fields],
|
||||
|
||||
@@ -63,7 +63,6 @@ export const traverseFields = ({
|
||||
|
||||
break
|
||||
case 'collapsible':
|
||||
|
||||
case 'row':
|
||||
traverseFields({
|
||||
config,
|
||||
|
||||
@@ -1,65 +1,15 @@
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
BuildFormStateArgs,
|
||||
ClientConfig,
|
||||
ClientUser,
|
||||
ErrorResult,
|
||||
FieldSchemaMap,
|
||||
FormState,
|
||||
SanitizedConfig,
|
||||
} from 'payload'
|
||||
import type { BuildFormStateArgs, ClientConfig, ClientUser, ErrorResult, FormState } from 'payload'
|
||||
|
||||
import { formatErrors } from 'payload'
|
||||
import { reduceFieldsToValues } from 'payload/shared'
|
||||
|
||||
import { fieldSchemasToFormState } from '../forms/fieldSchemasToFormState/index.js'
|
||||
import { renderField } from '../forms/fieldSchemasToFormState/renderField.js'
|
||||
import { buildFieldSchemaMap } from './buildFieldSchemaMap/index.js'
|
||||
import { getClientConfig } from './getClientConfig.js'
|
||||
import { getClientSchemaMap } from './getClientSchemaMap.js'
|
||||
import { getSchemaMap } from './getSchemaMap.js'
|
||||
import { handleFormStateLocking } from './handleFormStateLocking.js'
|
||||
|
||||
let cachedFieldMap = global._payload_fieldMap
|
||||
let cachedClientConfig = global._payload_clientConfig
|
||||
|
||||
if (!cachedFieldMap) {
|
||||
cachedFieldMap = global._payload_fieldMap = null
|
||||
}
|
||||
|
||||
if (!cachedClientConfig) {
|
||||
cachedClientConfig = global._payload_clientConfig = null
|
||||
}
|
||||
|
||||
export const getFieldSchemaMap = (args: {
|
||||
collectionSlug?: string
|
||||
config: SanitizedConfig
|
||||
globalSlug?: string
|
||||
i18n: I18nClient
|
||||
}): FieldSchemaMap => {
|
||||
const { collectionSlug, config, globalSlug, i18n } = args
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
if (!cachedFieldMap) {
|
||||
cachedFieldMap = new Map()
|
||||
}
|
||||
const cachedEntityFieldMap = cachedFieldMap.get(collectionSlug || globalSlug)
|
||||
if (cachedEntityFieldMap) {
|
||||
return cachedEntityFieldMap
|
||||
}
|
||||
}
|
||||
|
||||
const { fieldSchemaMap: entityFieldMap } = buildFieldSchemaMap({
|
||||
collectionSlug,
|
||||
config,
|
||||
globalSlug,
|
||||
i18n: i18n as I18n,
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
cachedFieldMap.set(collectionSlug || globalSlug, entityFieldMap)
|
||||
}
|
||||
|
||||
return entityFieldMap
|
||||
}
|
||||
|
||||
type BuildFormStateSuccessResult = {
|
||||
clientConfig?: ClientConfig
|
||||
errors?: never
|
||||
@@ -167,15 +117,24 @@ export const buildFormState = async (
|
||||
throw new Error('Either collectionSlug or globalSlug must be provided')
|
||||
}
|
||||
|
||||
const fieldSchemaMap = getFieldSchemaMap({
|
||||
const schemaMap = getSchemaMap({
|
||||
collectionSlug,
|
||||
config,
|
||||
globalSlug,
|
||||
i18n,
|
||||
})
|
||||
|
||||
const clientSchemaMap = getClientSchemaMap({
|
||||
collectionSlug,
|
||||
config: getClientConfig({ config, i18n, importMap: req.payload.importMap }),
|
||||
globalSlug,
|
||||
i18n,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
|
||||
const id = collectionSlug ? idFromArgs : undefined
|
||||
const fieldOrEntityConfig = fieldSchemaMap.get(schemaPath)
|
||||
const fieldOrEntityConfig = schemaMap.get(schemaPath)
|
||||
|
||||
if (!fieldOrEntityConfig) {
|
||||
throw new Error(`Could not find "${schemaPath}" in the fieldSchemaMap`)
|
||||
@@ -216,10 +175,11 @@ export const buildFormState = async (
|
||||
|
||||
const formStateResult = await fieldSchemasToFormState({
|
||||
id,
|
||||
clientFieldSchemaMap: clientSchemaMap,
|
||||
collectionSlug,
|
||||
data,
|
||||
fields,
|
||||
fieldSchemaMap,
|
||||
fieldSchemaMap: schemaMap,
|
||||
operation,
|
||||
permissions: docPermissions?.fields || {},
|
||||
preferences: docPreferences || { fields: {} },
|
||||
|
||||
@@ -1,49 +1,21 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
BuildTableStateArgs,
|
||||
ClientCollectionConfig,
|
||||
ClientConfig,
|
||||
ErrorResult,
|
||||
ImportMap,
|
||||
PaginatedDocs,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { dequal } from 'dequal/lite'
|
||||
import { createClientConfig, formatErrors } from 'payload'
|
||||
import { formatErrors } from 'payload'
|
||||
|
||||
import type { Column } from '../elements/Table/index.js'
|
||||
import type { ListPreferences } from '../elements/TableColumns/index.js'
|
||||
|
||||
import { getClientConfig } from './getClientConfig.js'
|
||||
import { renderFilters, renderTable } from './renderTable.js'
|
||||
|
||||
let cachedClientConfig = global._payload_clientConfig
|
||||
|
||||
if (!cachedClientConfig) {
|
||||
cachedClientConfig = global._payload_clientConfig = null
|
||||
}
|
||||
|
||||
export const getClientConfig = (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
importMap: ImportMap
|
||||
}): ClientConfig => {
|
||||
const { config, i18n, importMap } = args
|
||||
|
||||
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
cachedClientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
type BuildTableStateSuccessResult = {
|
||||
clientConfig?: ClientConfig
|
||||
data: PaginatedDocs
|
||||
|
||||
31
packages/ui/src/utilities/getClientConfig.ts
Normal file
31
packages/ui/src/utilities/getClientConfig.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientConfig, ImportMap, SanitizedConfig } from 'payload'
|
||||
|
||||
import { createClientConfig } from 'payload'
|
||||
import { cache } from 'react'
|
||||
|
||||
let cachedClientConfig = global._payload_clientConfig
|
||||
|
||||
if (!cachedClientConfig) {
|
||||
cachedClientConfig = global._payload_clientConfig = null
|
||||
}
|
||||
|
||||
export const getClientConfig = cache(
|
||||
(args: { config: SanitizedConfig; i18n: I18nClient; importMap: ImportMap }): ClientConfig => {
|
||||
if (cachedClientConfig && !global._payload_doNotCacheClientConfig) {
|
||||
return cachedClientConfig
|
||||
}
|
||||
const { config, i18n, importMap } = args
|
||||
|
||||
cachedClientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
importMap,
|
||||
})
|
||||
global._payload_clientConfig = cachedClientConfig
|
||||
|
||||
global._payload_doNotCacheClientConfig = false
|
||||
|
||||
return cachedClientConfig
|
||||
},
|
||||
)
|
||||
54
packages/ui/src/utilities/getClientSchemaMap.ts
Normal file
54
packages/ui/src/utilities/getClientSchemaMap.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientConfig, ClientFieldSchemaMap, FieldSchemaMap, Payload } from 'payload'
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
import { buildClientFieldSchemaMap } from './buildClientFieldSchemaMap/index.js'
|
||||
|
||||
let cachedClientSchemaMap = global._payload_clientSchemaMap
|
||||
|
||||
if (!cachedClientSchemaMap) {
|
||||
cachedClientSchemaMap = global._payload_clientSchemaMap = null
|
||||
}
|
||||
|
||||
export const getClientSchemaMap = cache(
|
||||
(args: {
|
||||
collectionSlug?: string
|
||||
config: ClientConfig
|
||||
globalSlug?: string
|
||||
i18n: I18nClient
|
||||
payload: Payload
|
||||
schemaMap: FieldSchemaMap
|
||||
}): ClientFieldSchemaMap => {
|
||||
const { collectionSlug, config, globalSlug, i18n, payload, schemaMap } = args
|
||||
|
||||
if (!cachedClientSchemaMap || global._payload_doNotCacheClientSchemaMap) {
|
||||
cachedClientSchemaMap = new Map()
|
||||
}
|
||||
|
||||
let cachedEntityClientFieldMap = cachedClientSchemaMap.get(collectionSlug || globalSlug)
|
||||
|
||||
if (cachedEntityClientFieldMap) {
|
||||
return cachedEntityClientFieldMap
|
||||
}
|
||||
|
||||
cachedEntityClientFieldMap = new Map()
|
||||
|
||||
const { clientFieldSchemaMap: entityClientFieldMap } = buildClientFieldSchemaMap({
|
||||
collectionSlug,
|
||||
config,
|
||||
globalSlug,
|
||||
i18n: i18n as I18n,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
|
||||
cachedClientSchemaMap.set(collectionSlug || globalSlug, entityClientFieldMap)
|
||||
|
||||
global._payload_clientSchemaMap = cachedClientSchemaMap
|
||||
|
||||
global._payload_doNotCacheClientSchemaMap = false
|
||||
|
||||
return entityClientFieldMap
|
||||
},
|
||||
)
|
||||
50
packages/ui/src/utilities/getSchemaMap.ts
Normal file
50
packages/ui/src/utilities/getSchemaMap.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { FieldSchemaMap, SanitizedConfig } from 'payload'
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
import { buildFieldSchemaMap } from './buildFieldSchemaMap/index.js'
|
||||
|
||||
let cachedSchemaMap = global._payload_schemaMap
|
||||
|
||||
if (!cachedSchemaMap) {
|
||||
cachedSchemaMap = global._payload_schemaMap = null
|
||||
}
|
||||
|
||||
export const getSchemaMap = cache(
|
||||
(args: {
|
||||
collectionSlug?: string
|
||||
config: SanitizedConfig
|
||||
globalSlug?: string
|
||||
i18n: I18nClient
|
||||
}): FieldSchemaMap => {
|
||||
const { collectionSlug, config, globalSlug, i18n } = args
|
||||
|
||||
if (!cachedSchemaMap || global._payload_doNotCacheSchemaMap) {
|
||||
cachedSchemaMap = new Map()
|
||||
}
|
||||
|
||||
let cachedEntityFieldMap = cachedSchemaMap.get(collectionSlug || globalSlug)
|
||||
|
||||
if (cachedEntityFieldMap) {
|
||||
return cachedEntityFieldMap
|
||||
}
|
||||
|
||||
cachedEntityFieldMap = new Map()
|
||||
|
||||
const { fieldSchemaMap: entityFieldMap } = buildFieldSchemaMap({
|
||||
collectionSlug,
|
||||
config,
|
||||
globalSlug,
|
||||
i18n: i18n as I18n,
|
||||
})
|
||||
|
||||
cachedSchemaMap.set(collectionSlug || globalSlug, entityFieldMap)
|
||||
|
||||
global._payload_schemaMap = cachedSchemaMap
|
||||
|
||||
global._payload_doNotCacheSchemaMap = false
|
||||
|
||||
return entityFieldMap
|
||||
},
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SanitizedCollectionConfig, VerifyConfig } from 'payload'
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export type Props = {
|
||||
className?: string
|
||||
@@ -13,5 +13,5 @@ export type Props = {
|
||||
setValidateBeforeSubmit: (validate: boolean) => void
|
||||
useAPIKey?: boolean
|
||||
username: string
|
||||
verify?: boolean | VerifyConfig
|
||||
verify?: boolean
|
||||
}
|
||||
|
||||
19
test/auth-basic/config.ts
Normal file
19
test/auth-basic/config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
autoLogin: false,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
152
test/auth-basic/e2e.spec.ts
Normal file
152
test/auth-basic/e2e.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { devUser } from 'credentials.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||
import type { Config } from './payload-types.js'
|
||||
|
||||
import { ensureCompilationIsDone, getRoutes, initPageConsoleErrorCatch } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../helpers/reInitializeDB.js'
|
||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
let payload: PayloadTestSDK<Config>
|
||||
|
||||
const { beforeAll, beforeEach, describe } = test
|
||||
|
||||
const createFirstUser = async ({
|
||||
page,
|
||||
serverURL,
|
||||
}: {
|
||||
customAdminRoutes?: SanitizedConfig['admin']['routes']
|
||||
customRoutes?: SanitizedConfig['routes']
|
||||
page: Page
|
||||
serverURL: string
|
||||
}) => {
|
||||
const {
|
||||
admin: {
|
||||
routes: { createFirstUser: createFirstUserRoute },
|
||||
},
|
||||
routes: { admin: adminRoute },
|
||||
} = getRoutes({})
|
||||
|
||||
// wait for create first user route
|
||||
await page.goto(serverURL + `${adminRoute}${createFirstUserRoute}`)
|
||||
|
||||
// forget to fill out confirm password
|
||||
await page.locator('#field-email').fill(devUser.email)
|
||||
await page.locator('#field-password').fill(devUser.password)
|
||||
await page.locator('.form-submit > button').click()
|
||||
await expect(page.locator('.field-type.confirm-password .field-error')).toHaveText(
|
||||
'This field is required.',
|
||||
)
|
||||
|
||||
// make them match, but does not pass password validation
|
||||
await page.locator('#field-email').fill(devUser.email)
|
||||
await page.locator('#field-password').fill('12')
|
||||
await page.locator('#field-confirm-password').fill('12')
|
||||
await page.locator('.form-submit > button').click()
|
||||
await expect(page.locator('.field-type.password .field-error')).toHaveText(
|
||||
'This value must be longer than the minimum length of 3 characters.',
|
||||
)
|
||||
|
||||
await page.locator('#field-email').fill(devUser.email)
|
||||
await page.locator('#field-password').fill(devUser.password)
|
||||
await page.locator('#field-confirm-password').fill(devUser.password)
|
||||
await page.locator('.form-submit > button').click()
|
||||
|
||||
await expect
|
||||
.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT })
|
||||
.not.toContain('create-first-user')
|
||||
}
|
||||
|
||||
describe('auth-basic', () => {
|
||||
let page: Page
|
||||
let url: AdminUrlUtil
|
||||
let serverURL: string
|
||||
let apiURL: string
|
||||
|
||||
beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
|
||||
apiURL = `${serverURL}/api`
|
||||
url = new AdminUrlUtil(serverURL, 'users')
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
|
||||
await ensureCompilationIsDone({
|
||||
page,
|
||||
serverURL,
|
||||
readyURL: `${serverURL}/admin/**`,
|
||||
noAutoLogin: true,
|
||||
})
|
||||
|
||||
// Undo onInit seeding, as we need to test this without having a user created, or testing create-first-user
|
||||
await reInitializeDB({
|
||||
serverURL,
|
||||
snapshotKey: 'auth-basic',
|
||||
deleteOnly: true,
|
||||
})
|
||||
|
||||
await payload.delete({
|
||||
collection: 'users',
|
||||
where: {
|
||||
id: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await ensureCompilationIsDone({
|
||||
page,
|
||||
serverURL,
|
||||
readyURL: `${serverURL}/admin/create-first-user`,
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await payload.delete({
|
||||
collection: 'users',
|
||||
where: {
|
||||
id: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('unauthenticated users', () => {
|
||||
test('ensure create first user page only has 3 fields', async () => {
|
||||
await page.goto(url.admin + '/create-first-user')
|
||||
|
||||
// Ensure there are only 2 elements with class field-type
|
||||
await expect(page.locator('.field-type')).toHaveCount(3) // Email, Password, Confirm Password
|
||||
})
|
||||
|
||||
test('ensure first user can be created', async () => {
|
||||
await createFirstUser({ page, serverURL })
|
||||
|
||||
// use the api key in a fetch to assert that it is disabled
|
||||
await expect(async () => {
|
||||
const users = await payload.find({
|
||||
collection: 'users',
|
||||
})
|
||||
|
||||
expect(users.totalDocs).toBe(1)
|
||||
expect(users.docs[0].email).toBe(devUser.email)
|
||||
}).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
186
test/auth-basic/payload-types.ts
Normal file
186
test/auth-basic/payload-types.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null;
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
13
test/auth-basic/tsconfig.eslint.json
Normal file
13
test/auth-basic/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
3
test/auth-basic/tsconfig.json
Normal file
3
test/auth-basic/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
||||
@@ -64,11 +64,13 @@ export async function ensureCompilationIsDone({
|
||||
page,
|
||||
serverURL,
|
||||
noAutoLogin,
|
||||
readyURL,
|
||||
}: {
|
||||
customAdminRoutes?: Config['admin']['routes']
|
||||
customRoutes?: Config['routes']
|
||||
noAutoLogin?: boolean
|
||||
page: Page
|
||||
readyURL?: string
|
||||
serverURL: string
|
||||
}): Promise<void> {
|
||||
const {
|
||||
@@ -82,11 +84,16 @@ export async function ensureCompilationIsDone({
|
||||
|
||||
while (attempt <= maxAttempts) {
|
||||
try {
|
||||
console.log(`Checking if compilation is done (attempt ${attempt}/${maxAttempts})...`)
|
||||
console.log(
|
||||
`Checking if compilation is done (attempt ${attempt}/${maxAttempts})...`,
|
||||
readyURL ??
|
||||
(noAutoLogin ? `${adminURL + (adminURL.endsWith('/') ? '' : '/')}login` : adminURL),
|
||||
)
|
||||
|
||||
await page.goto(adminURL)
|
||||
await page.waitForURL(
|
||||
noAutoLogin ? `${adminURL + (adminURL.endsWith('/') ? '' : '/')}login` : adminURL,
|
||||
readyURL ??
|
||||
(noAutoLogin ? `${adminURL + (adminURL.endsWith('/') ? '' : '/')}login` : adminURL),
|
||||
)
|
||||
|
||||
console.log('Successfully compiled')
|
||||
|
||||
Reference in New Issue
Block a user