diff --git a/app/(payload)/layout.tsx b/app/(payload)/layout.tsx index 396a5cc7569..497fd2e0291 100644 --- a/app/(payload)/layout.tsx +++ b/app/(payload)/layout.tsx @@ -1,8 +1,10 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import configPromise from '@payload-config' -import { RootLayout } from '@payloadcms/next/layouts' // import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src` +import type { ServerFunctionClient } from 'payload' + +import config from '@payload-config' +import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' import React from 'react' import { importMap } from './admin/importMap.js' @@ -12,8 +14,17 @@ type Args = { children: React.ReactNode } +const serverFunction: ServerFunctionClient = async function (args) { + 'use server' + return handleServerFunctions({ + ...args, + config, + importMap, + }) +} + const Layout = ({ children }: Args) => ( - + {children} ) diff --git a/docs/admin/fields.mdx b/docs/admin/fields.mdx index f13eb6c81aa..73ac795fccc 100644 --- a/docs/admin/fields.mdx +++ b/docs/admin/fields.mdx @@ -228,7 +228,6 @@ The following additional properties are also provided to the `field` prop: | Property | Description | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`_isPresentational`** | A boolean indicating that the field is purely visual and does not directly affect data or change data shape, i.e. the [UI Field](../fields/ui). | | **`_path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray[0].myField`. | | **`_schemaPath`** | A string representing the direct, static path to the [Field Config](../fields/overview), i.e. `myGroup.myArray.myField` | diff --git a/docs/lexical/converters.mdx b/docs/lexical/converters.mdx index 8f4e31178b1..ca98ba1695c 100644 --- a/docs/lexical/converters.mdx +++ b/docs/lexical/converters.mdx @@ -370,7 +370,12 @@ const yourEditorState: SerializedEditorState // <= your current editor state her // Import editor state into your headless editor try { - headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately + headlessEditor.update( + () => { + headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) + }, + { discrete: true }, // This should commit the editor state immediately + ) } catch (e) { logger.error({ err: e }, 'ERROR parsing editor state') } @@ -382,8 +387,6 @@ headlessEditor.getEditorState().read(() => { }) ``` -The `.setEditorState()` function immediately updates your editor state. Thus, there's no need for the `discrete: true` flag when reading the state afterward. - ## Lexical => Plain Text Export content from the Lexical editor into plain text using these steps: @@ -401,8 +404,13 @@ const yourEditorState: SerializedEditorState // <= your current editor state her // Import editor state into your headless editor try { - headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately -} catch (e) { + headlessEditor.update( + () => { + headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) + }, + { discrete: true }, // This should commit the editor state immediately + ) + } catch (e) { logger.error({ err: e }, 'ERROR parsing editor state') } diff --git a/examples/custom-components/src/app/(payload)/layout.tsx b/examples/custom-components/src/app/(payload)/layout.tsx index d2cf5ab1c79..af755c33b2f 100644 --- a/examples/custom-components/src/app/(payload)/layout.tsx +++ b/examples/custom-components/src/app/(payload)/layout.tsx @@ -1,6 +1,6 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import configPromise from '@payload-config' +import config from '@payload-config' import '@payloadcms/next/css' import { RootLayout } from '@payloadcms/next/layouts' import React from 'react' @@ -13,7 +13,7 @@ type Args = { } const Layout = ({ children }: Args) => ( - + {children} ) diff --git a/examples/hierarchy/src/app/(payload)/layout.tsx b/examples/hierarchy/src/app/(payload)/layout.tsx index 7997f272f10..cfcc0bcb0c3 100644 --- a/examples/hierarchy/src/app/(payload)/layout.tsx +++ b/examples/hierarchy/src/app/(payload)/layout.tsx @@ -1,8 +1,8 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import configPromise from '@payload-config' import '@payloadcms/next/css' import { RootLayout } from '@payloadcms/next/layouts' -/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import React from 'react' import './custom.scss' diff --git a/examples/multi-tenant/src/app/(payload)/layout.tsx b/examples/multi-tenant/src/app/(payload)/layout.tsx index d2cf5ab1c79..af755c33b2f 100644 --- a/examples/multi-tenant/src/app/(payload)/layout.tsx +++ b/examples/multi-tenant/src/app/(payload)/layout.tsx @@ -1,6 +1,6 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import configPromise from '@payload-config' +import config from '@payload-config' import '@payloadcms/next/css' import { RootLayout } from '@payloadcms/next/layouts' import React from 'react' @@ -13,7 +13,7 @@ type Args = { } const Layout = ({ children }: Args) => ( - + {children} ) diff --git a/next.config.mjs b/next.config.mjs index 3d865ff7f58..418a13b7d26 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -19,6 +19,11 @@ const config = withBundleAnalyzer( typescript: { ignoreBuildErrors: true, }, + experimental: { + serverActions: { + bodySizeLimit: '5mb', + }, + }, env: { PAYLOAD_CORE_DEV: 'true', ROOT_DIR: path.resolve(dirname), diff --git a/package.json b/package.json index 1a8a22b0d4d..37990302b55 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts", "dev:generate-types": "pnpm runts ./test/generateTypes.ts", "dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts", + "dev:prod": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod", "dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts", "devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev", "docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start", @@ -65,12 +66,12 @@ "docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down", "force:build": "pnpm run build:core:force", "lint": "turbo run lint --concurrency 1 --continue", - "lint-staged": "lint-staged", + "lint-staged": "node ./scripts/run-lint-staged.js", "lint:fix": "turbo run lint:fix --concurrency 1 --continue", "obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +", "prepare": "husky", - "prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..", - "prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..", + "prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..", + "prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..", "reinstall": "pnpm clean:all && pnpm install", "release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha", "release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta", diff --git a/packages/db-mongodb/src/countGlobalVersions.ts b/packages/db-mongodb/src/countGlobalVersions.ts new file mode 100644 index 00000000000..ea95c60f2ff --- /dev/null +++ b/packages/db-mongodb/src/countGlobalVersions.ts @@ -0,0 +1,48 @@ +import type { QueryOptions } from 'mongoose' +import type { CountGlobalVersions, PayloadRequest } from 'payload' + +import { flattenWhereToOperators } from 'payload' + +import type { MongooseAdapter } from './index.js' + +import { withSession } from './withSession.js' + +export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions( + this: MongooseAdapter, + { global, locale, req = {} as PayloadRequest, where }, +) { + const Model = this.versions[global] + const options: QueryOptions = await withSession(this, req) + + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } + + const query = await Model.buildQuery({ + locale, + payload: this.payload, + where, + }) + + // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. + const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 + + if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) { + // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding + // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, + // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses + // the correct indexed field + options.hint = { + _id: 1, + } + } + + const result = await Model.countDocuments(query, options) + + return { + totalDocs: result, + } +} diff --git a/packages/db-mongodb/src/countVersions.ts b/packages/db-mongodb/src/countVersions.ts new file mode 100644 index 00000000000..9b9fb6a6a5c --- /dev/null +++ b/packages/db-mongodb/src/countVersions.ts @@ -0,0 +1,48 @@ +import type { QueryOptions } from 'mongoose' +import type { CountVersions, PayloadRequest } from 'payload' + +import { flattenWhereToOperators } from 'payload' + +import type { MongooseAdapter } from './index.js' + +import { withSession } from './withSession.js' + +export const countVersions: CountVersions = async function countVersions( + this: MongooseAdapter, + { collection, locale, req = {} as PayloadRequest, where }, +) { + const Model = this.versions[collection] + const options: QueryOptions = await withSession(this, req) + + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } + + const query = await Model.buildQuery({ + locale, + payload: this.payload, + where, + }) + + // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. + const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 + + if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) { + // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding + // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, + // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses + // the correct indexed field + options.hint = { + _id: 1, + } + } + + const result = await Model.countDocuments(query, options) + + return { + totalDocs: result, + } +} diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index b49d90f655f..7aa0f75d7f8 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -12,6 +12,8 @@ import type { CollectionModel, GlobalModel, MigrateDownArgs, MigrateUpArgs } fro import { connect } from './connect.js' import { count } from './count.js' +import { countGlobalVersions } from './countGlobalVersions.js' +import { countVersions } from './countVersions.js' import { create } from './create.js' import { createGlobal } from './createGlobal.js' import { createGlobalVersion } from './createGlobalVersion.js' @@ -154,7 +156,6 @@ export function mongooseAdapter({ collections: {}, connection: undefined, connectOptions: connectOptions || {}, - count, disableIndexHints, globals: undefined, mongoMemoryServer, @@ -166,6 +167,9 @@ export function mongooseAdapter({ beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction, commitTransaction, connect, + count, + countGlobalVersions, + countVersions, create, createGlobal, createGlobalVersion, diff --git a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts index 5cec7a5291e..f7929b1cfb9 100644 --- a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts +++ b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts @@ -1,8 +1,6 @@ import type { PipelineStage } from 'mongoose' import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload' -import { combineQueries } from 'payload' - import type { MongooseAdapter } from '../index.js' import { buildSortParam } from '../queries/buildSortParam.js' @@ -60,11 +58,11 @@ export const buildJoinAggregation = async ({ for (const join of joinConfig[slug]) { const joinModel = adapter.collections[join.field.collection] - if (projection && !projection[join.schemaPath]) { + if (projection && !projection[join.joinPath]) { continue } - if (joins?.[join.schemaPath] === false) { + if (joins?.[join.joinPath] === false) { continue } @@ -72,7 +70,7 @@ export const buildJoinAggregation = async ({ limit: limitJoin = join.field.defaultLimit ?? 10, sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort, where: whereJoin, - } = joins?.[join.schemaPath] || {} + } = joins?.[join.joinPath] || {} const sort = buildSortParam({ config: adapter.payload.config, @@ -105,7 +103,7 @@ export const buildJoinAggregation = async ({ if (adapter.payload.config.localization && locale === 'all') { adapter.payload.config.localization.localeCodes.forEach((code) => { - const as = `${versions ? `version.${join.schemaPath}` : join.schemaPath}${code}` + const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}` aggregate.push( { @@ -146,7 +144,7 @@ export const buildJoinAggregation = async ({ } else { const localeSuffix = join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : '' - const as = `${versions ? `version.${join.schemaPath}` : join.schemaPath}${localeSuffix}` + const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${localeSuffix}` aggregate.push( { diff --git a/packages/db-mongodb/src/utilities/handleError.ts b/packages/db-mongodb/src/utilities/handleError.ts index d28d19af658..259f95f5d85 100644 --- a/packages/db-mongodb/src/utilities/handleError.ts +++ b/packages/db-mongodb/src/utilities/handleError.ts @@ -19,8 +19,8 @@ export const handleError = ({ collection, errors: [ { - field: Object.keys(error.keyValue)[0], message: req.t('error:valueMustBeUnique'), + path: Object.keys(error.keyValue)[0], }, ], global, diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index 473436021e3..9d1271d48b1 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -4,6 +4,8 @@ import { beginTransaction, commitTransaction, count, + countGlobalVersions, + countVersions, create, createGlobal, createGlobalVersion, @@ -126,6 +128,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj convertPathToJSONTraversal, count, countDistinct, + countGlobalVersions, + countVersions, create, createGlobal, createGlobalVersion, diff --git a/packages/db-sqlite/src/index.ts b/packages/db-sqlite/src/index.ts index 12b6420b0e3..320a2326e2a 100644 --- a/packages/db-sqlite/src/index.ts +++ b/packages/db-sqlite/src/index.ts @@ -5,6 +5,8 @@ import { beginTransaction, commitTransaction, count, + countGlobalVersions, + countVersions, create, createGlobal, createGlobalVersion, @@ -114,6 +116,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj { convertPathToJSONTraversal, count, countDistinct, + countGlobalVersions, + countVersions, create, createGlobal, createGlobalVersion, diff --git a/packages/db-vercel-postgres/src/index.ts b/packages/db-vercel-postgres/src/index.ts index e053dcccec4..757f74f6d7a 100644 --- a/packages/db-vercel-postgres/src/index.ts +++ b/packages/db-vercel-postgres/src/index.ts @@ -4,6 +4,8 @@ import { beginTransaction, commitTransaction, count, + countGlobalVersions, + countVersions, create, createGlobal, createGlobalVersion, @@ -127,6 +129,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj slug === global, + ) + + const tableName = this.tableNameMap.get( + `_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`, + ) + + const db = this.sessions[await req?.transactionID]?.db || this.drizzle + + const fields = buildVersionGlobalFields(this.payload.config, globalConfig) + + const { joins, where } = buildQuery({ + adapter: this, + fields, + locale, + tableName, + where: whereArg, + }) + + const countResult = await this.countDistinct({ + db, + joins, + tableName, + where, + }) + + return { totalDocs: countResult } +} diff --git a/packages/drizzle/src/countVersions.ts b/packages/drizzle/src/countVersions.ts new file mode 100644 index 00000000000..c4017acfaa6 --- /dev/null +++ b/packages/drizzle/src/countVersions.ts @@ -0,0 +1,40 @@ +import type { CountVersions, SanitizedCollectionConfig } from 'payload' + +import { buildVersionCollectionFields } from 'payload' +import toSnakeCase from 'to-snake-case' + +import type { DrizzleAdapter } from './types.js' + +import buildQuery from './queries/buildQuery.js' + +export const countVersions: CountVersions = async function countVersions( + this: DrizzleAdapter, + { collection, locale, req, where: whereArg }, +) { + const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config + + const tableName = this.tableNameMap.get( + `_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`, + ) + + const db = this.sessions[await req?.transactionID]?.db || this.drizzle + + const fields = buildVersionCollectionFields(this.payload.config, collectionConfig) + + const { joins, where } = buildQuery({ + adapter: this, + fields, + locale, + tableName, + where: whereArg, + }) + + const countResult = await this.countDistinct({ + db, + joins, + tableName, + where, + }) + + return { totalDocs: countResult } +} diff --git a/packages/drizzle/src/index.ts b/packages/drizzle/src/index.ts index d31b0226eee..6c72222f707 100644 --- a/packages/drizzle/src/index.ts +++ b/packages/drizzle/src/index.ts @@ -1,4 +1,6 @@ export { count } from './count.js' +export { countGlobalVersions } from './countGlobalVersions.js' +export { countVersions } from './countVersions.js' export { create } from './create.js' export { createGlobal } from './createGlobal.js' export { createGlobalVersion } from './createGlobalVersion.js' diff --git a/packages/drizzle/src/upsertRow/index.ts b/packages/drizzle/src/upsertRow/index.ts index 77c3448c001..fa09fecc2f8 100644 --- a/packages/drizzle/src/upsertRow/index.ts +++ b/packages/drizzle/src/upsertRow/index.ts @@ -391,8 +391,8 @@ export const upsertRow = async | TypeWithID>( id, errors: [ { - field: fieldName, message: req.t('error:valueMustBeUnique'), + path: fieldName, }, ], }, diff --git a/packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx index 5a73e6ee2e7..10a1c0fcb84 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx @@ -1,7 +1,8 @@ import type { DocumentTabConfig, DocumentTabProps } from 'payload' +import type React from 'react' -import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared' -import React, { Fragment } from 'react' +import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' +import { Fragment } from 'react' import './index.scss' import { DocumentTabLink } from './TabLink.js' @@ -59,17 +60,6 @@ export const DocumentTab: React.FC< }) : label - const createMappedComponent = getCreateMappedComponent({ - importMap: payload.importMap, - serverProps: { - i18n, - payload, - permissions, - }, - }) - - const mappedPin = createMappedComponent(Pill, undefined, Pill_Component, 'Pill') - return ( {labelToRender} - {mappedPin && ( + {Pill || Pill_Component ? (   - + - )} + ) : null} ) diff --git a/packages/next/src/elements/DocumentHeader/Tabs/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/index.tsx index 40896720ba3..987f193b8a9 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/index.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/index.tsx @@ -6,7 +6,7 @@ import type { SanitizedGlobalConfig, } from 'payload' -import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared' +import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import React from 'react' import { getCustomViews } from './getCustomViews.js' @@ -80,33 +80,21 @@ export const DocumentTabs: React.FC<{ const { path, tab } = CustomView if (tab.Component) { - const createMappedComponent = getCreateMappedComponent({ - importMap: payload.importMap, - serverProps: { - i18n, - payload, - permissions, - ...props, - key: `tab-custom-${index}`, - path, - }, - }) - - const mappedTab = createMappedComponent( - tab.Component, - undefined, - undefined, - 'tab.Component', - ) - return ( - ) } @@ -121,6 +109,7 @@ export const DocumentTabs: React.FC<{ /> ) } + return null })} diff --git a/packages/next/src/elements/DocumentHeader/Tabs/tabs/VersionsPill/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/tabs/VersionsPill/index.tsx index 72d61c26825..78a0e3d1465 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/tabs/VersionsPill/index.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/tabs/VersionsPill/index.tsx @@ -5,14 +5,11 @@ import React from 'react' import { baseClass } from '../../Tab/index.js' export const VersionsPill: React.FC = () => { - const { versions } = useDocumentInfo() + const { versionCount } = useDocumentInfo() - // don't count snapshots - const totalVersions = versions?.docs.filter((version) => !version.snapshot).length || 0 - - if (!versions?.totalDocs) { + if (!versionCount) { return null } - return {totalVersions} + return {versionCount} } diff --git a/packages/next/src/elements/DocumentHeader/index.tsx b/packages/next/src/elements/DocumentHeader/index.tsx index 6d9cfd34e2e..67e984a2f63 100644 --- a/packages/next/src/elements/DocumentHeader/index.tsx +++ b/packages/next/src/elements/DocumentHeader/index.tsx @@ -7,7 +7,7 @@ import type { } from 'payload' import { Gutter, RenderTitle } from '@payloadcms/ui' -import React, { Fragment } from 'react' +import React from 'react' import './index.scss' import { DocumentTabs } from './Tabs/index.js' @@ -16,32 +16,25 @@ const baseClass = `doc-header` export const DocumentHeader: React.FC<{ collectionConfig?: SanitizedCollectionConfig - customHeader?: React.ReactNode globalConfig?: SanitizedGlobalConfig hideTabs?: boolean i18n: I18n payload: Payload permissions: Permissions }> = (props) => { - const { collectionConfig, customHeader, globalConfig, hideTabs, i18n, payload, permissions } = - props + const { collectionConfig, globalConfig, hideTabs, i18n, payload, permissions } = props return ( - {customHeader && customHeader} - {!customHeader && ( - - - {!hideTabs && ( - - )} - + + {!hideTabs && ( + )} ) diff --git a/packages/next/src/elements/EmailAndUsername/index.tsx b/packages/next/src/elements/EmailAndUsername/index.tsx deleted file mode 100644 index a7727566d58..00000000000 --- a/packages/next/src/elements/EmailAndUsername/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'use client' - -import type { FieldPermissions, LoginWithUsernameOptions } from 'payload' - -import { EmailField, RenderFields, TextField, useTranslation } from '@payloadcms/ui' -import { email, username } from 'payload/shared' -import React from 'react' - -type Props = { - readonly loginWithUsername?: false | LoginWithUsernameOptions -} -function EmailFieldComponent(props: Props) { - const { loginWithUsername } = props - const { t } = useTranslation() - - const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail) - const showEmailField = - !loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin - - if (showEmailField) { - return ( - - ) - } - - return null -} - -function UsernameFieldComponent(props: Props) { - const { loginWithUsername } = props - const { t } = useTranslation() - - const requireUsername = loginWithUsername && loginWithUsername.requireUsername - const showUsernameField = Boolean(loginWithUsername) - - if (showUsernameField) { - return ( - - ) - } - - return null -} - -type RenderEmailAndUsernameFieldsProps = { - className?: string - loginWithUsername?: false | LoginWithUsernameOptions - operation?: 'create' | 'update' - permissions?: { - [fieldName: string]: FieldPermissions - } - readOnly: boolean -} -export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFieldsProps) { - const { className, loginWithUsername, operation, permissions, readOnly } = props - - return ( - , - }, - }, - }, - localized: false, - }, - { - name: 'username', - type: 'text', - admin: { - components: { - Field: { - type: 'client', - Component: null, - RenderedComponent: , - }, - }, - }, - localized: false, - }, - ]} - forceRender - operation={operation} - path="" - permissions={permissions} - readOnly={readOnly} - schemaPath="" - /> - ) -} diff --git a/packages/next/src/elements/Logo/index.tsx b/packages/next/src/elements/Logo/index.tsx index 99943074e27..5412b91d73e 100644 --- a/packages/next/src/elements/Logo/index.tsx +++ b/packages/next/src/elements/Logo/index.tsx @@ -1,6 +1,7 @@ import type { ServerProps } from 'payload' -import { getCreateMappedComponent, PayloadLogo, RenderComponent } from '@payloadcms/ui/shared' +import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' +import { PayloadLogo } from '@payloadcms/ui/shared' import React from 'react' export const Logo: React.FC = (props) => { @@ -16,20 +17,20 @@ export const Logo: React.FC = (props) => { } = {}, } = payload.config - const createMappedComponent = getCreateMappedComponent({ - importMap: payload.importMap, - serverProps: { - i18n, - locale, - params, - payload, - permissions, - searchParams, - user, - }, - }) - - const mappedCustomLogo = createMappedComponent(CustomLogo, undefined, PayloadLogo, 'CustomLogo') - - return + return ( + + ) } diff --git a/packages/next/src/elements/Nav/index.client.tsx b/packages/next/src/elements/Nav/index.client.tsx index 3a19a0cd31b..9187cf430b7 100644 --- a/packages/next/src/elements/Nav/index.client.tsx +++ b/packages/next/src/elements/Nav/index.client.tsx @@ -1,32 +1,23 @@ 'use client' -import type { EntityToGroup } from '@payloadcms/ui/shared' +import type { groupNavItems } from '@payloadcms/ui/shared' import { getTranslation } from '@payloadcms/translations' -import { - NavGroup, - useAuth, - useConfig, - useEntityVisibility, - useNav, - useTranslation, -} from '@payloadcms/ui' -import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared' +import { NavGroup, useConfig, useNav, useTranslation } from '@payloadcms/ui' +import { EntityType, formatAdminURL } from '@payloadcms/ui/shared' import LinkWithDefault from 'next/link.js' import { usePathname } from 'next/navigation.js' import React, { Fragment } from 'react' const baseClass = 'nav' -export const DefaultNavClient: React.FC = () => { - const { permissions } = useAuth() - const { isEntityVisible } = useEntityVisibility() +export const DefaultNavClient: React.FC<{ + groups: ReturnType +}> = ({ groups }) => { const pathname = usePathname() const { config: { - collections, - globals, routes: { admin: adminRoute }, }, } = useConfig() @@ -34,53 +25,23 @@ export const DefaultNavClient: React.FC = () => { const { i18n } = useTranslation() const { navOpen } = useNav() - const groups = groupNavItems( - [ - ...collections - .filter(({ slug }) => isEntityVisible({ collectionSlug: slug })) - .map((collection) => { - const entityToGroup: EntityToGroup = { - type: EntityType.collection, - entity: collection, - } - - return entityToGroup - }), - ...globals - .filter(({ slug }) => isEntityVisible({ globalSlug: slug })) - .map((global) => { - const entityToGroup: EntityToGroup = { - type: EntityType.global, - entity: global, - } - - return entityToGroup - }), - ], - permissions, - i18n, - ) - return ( {groups.map(({ entities, label }, key) => { return ( - {entities.map(({ type, entity }, i) => { - let entityLabel: string + {entities.map(({ slug, type, label }, i) => { let href: string let id: string if (type === EntityType.collection) { - href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` }) - entityLabel = getTranslation(entity.labels.plural, i18n) - id = `nav-${entity.slug}` + href = formatAdminURL({ adminRoute, path: `/collections/${slug}` }) + id = `nav-${slug}` } if (type === EntityType.global) { - href = formatAdminURL({ adminRoute, path: `/globals/${entity.slug}` }) - entityLabel = getTranslation(entity.label, i18n) - id = `nav-global-${entity.slug}` + href = formatAdminURL({ adminRoute, path: `/globals/${slug}` }) + id = `nav-global-${slug}` } const Link = (LinkWithDefault.default || @@ -102,7 +63,7 @@ export const DefaultNavClient: React.FC = () => { tabIndex={!navOpen ? -1 : undefined} > {activeCollection &&
} - {entityLabel} + {getTranslation(label, i18n)} ) })} diff --git a/packages/next/src/elements/Nav/index.tsx b/packages/next/src/elements/Nav/index.tsx index 11611ed62bb..b41eff55818 100644 --- a/packages/next/src/elements/Nav/index.tsx +++ b/packages/next/src/elements/Nav/index.tsx @@ -1,7 +1,9 @@ +import type { EntityToGroup } from '@payloadcms/ui/shared' import type { ServerProps } from 'payload' import { Logout } from '@payloadcms/ui' -import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared' +import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' +import { EntityType, groupNavItems } from '@payloadcms/ui/shared' import React from 'react' import './index.scss' @@ -15,7 +17,7 @@ import { DefaultNavClient } from './index.client.js' export type NavProps = ServerProps export const DefaultNav: React.FC = (props) => { - const { i18n, locale, params, payload, permissions, searchParams, user } = props + const { i18n, locale, params, payload, permissions, searchParams, user, visibleEntities } = props if (!payload?.config) { return null @@ -23,44 +25,82 @@ export const DefaultNav: React.FC = (props) => { const { admin: { - components: { afterNavLinks, beforeNavLinks }, + components: { afterNavLinks, beforeNavLinks, logout }, }, + collections, + globals, } = payload.config - const createMappedComponent = getCreateMappedComponent({ - importMap: payload.importMap, - serverProps: { - i18n, - locale, - params, - payload, - permissions, - searchParams, - user, - }, - }) - - const mappedBeforeNavLinks = createMappedComponent( - beforeNavLinks, - undefined, - undefined, - 'beforeNavLinks', - ) - const mappedAfterNavLinks = createMappedComponent( - afterNavLinks, - undefined, - undefined, - 'afterNavLinks', + const groups = groupNavItems( + [ + ...collections + .filter(({ slug }) => visibleEntities.collections.includes(slug)) + .map( + (collection) => + ({ + type: EntityType.collection, + entity: collection, + }) satisfies EntityToGroup, + ), + ...globals + .filter(({ slug }) => visibleEntities.globals.includes(slug)) + .map( + (global) => + ({ + type: EntityType.global, + entity: global, + }) satisfies EntityToGroup, + ), + ], + permissions, + i18n, ) return (
diff --git a/packages/next/src/exports/layouts.ts b/packages/next/src/exports/layouts.ts index d3a56a75036..a0d56aa30f0 100644 --- a/packages/next/src/exports/layouts.ts +++ b/packages/next/src/exports/layouts.ts @@ -1 +1,2 @@ export { metadata, RootLayout } from '../layouts/Root/index.js' +export { handleServerFunctions } from '../utilities/handleServerFunctions.js' diff --git a/packages/next/src/exports/utilities.ts b/packages/next/src/exports/utilities.ts index 041bc67e0d7..a51d6d5978b 100644 --- a/packages/next/src/exports/utilities.ts +++ b/packages/next/src/exports/utilities.ts @@ -1,3 +1,4 @@ +// NOTICE: Server-only utilities, do not import anything client-side here. export { addDataAndFileToRequest } from '../utilities/addDataAndFileToRequest.js' export { addLocalesToRequestFromData, sanitizeLocales } from '../utilities/addLocalesToRequest.js' export { createPayloadRequest } from '../utilities/createPayloadRequest.js' diff --git a/packages/next/src/exports/views.ts b/packages/next/src/exports/views.ts index 4d777b32e6b..fe3309ca4c5 100644 --- a/packages/next/src/exports/views.ts +++ b/packages/next/src/exports/views.ts @@ -1,4 +1,2 @@ -export { DefaultEditView as EditView } from '../views/Edit/Default/index.js' -export { DefaultListView as ListView } from '../views/List/Default/index.js' export { NotFoundPage } from '../views/NotFound/index.js' export { generatePageMetadata, type GenerateViewMetadata, RootPage } from '../views/Root/index.js' diff --git a/packages/next/src/fetchAPI-multipart/processMultipart.ts b/packages/next/src/fetchAPI-multipart/processMultipart.ts index 64f2dbba07e..733e10ff780 100644 --- a/packages/next/src/fetchAPI-multipart/processMultipart.ts +++ b/packages/next/src/fetchAPI-multipart/processMultipart.ts @@ -22,6 +22,7 @@ type ProcessMultipart = (args: { export const processMultipart: ProcessMultipart = async ({ options, request }) => { let parsingRequest = true + let shouldAbortProccessing = false let fileCount = 0 let filesCompleted = 0 let allFilesHaveResolved: (value?: unknown) => void @@ -42,14 +43,16 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) = headersObject[name] = value }) + const reader = request.body.getReader() + + const busboy = Busboy({ ...options, headers: headersObject }) + function abortAndDestroyFile(file: Readable, err: APIError) { file.destroy() - parsingRequest = false + shouldAbortProccessing = true failedResolvingFiles(err) } - const busboy = Busboy({ ...options, headers: headersObject }) - // Build multipart req.body fields busboy.on('field', (field, val) => { result.fields = buildFields(result.fields, field, val) @@ -136,7 +139,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) = mimetype: mime, size, tempFilePath: getFilePath(), - truncated: Boolean('truncated' in file && file.truncated), + truncated: Boolean('truncated' in file && file.truncated) || false, }, options, ), @@ -164,8 +167,6 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) = uploadTimer.set() }) - // TODO: Valid eslint error - this will likely be a floating promise. Evaluate if we need to handle this differently. - busboy.on('finish', async () => { debugLog(options, `Busboy finished parsing request.`) if (options.parseNested) { @@ -190,14 +191,10 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) = 'error', (err = new APIError('Busboy error parsing multipart request', httpStatus.BAD_REQUEST)) => { debugLog(options, `Busboy error`) - parsingRequest = false throw err }, ) - const reader = request.body.getReader() - - // Start parsing request while (parsingRequest) { const { done, value } = await reader.read() @@ -205,7 +202,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) = parsingRequest = false } - if (value) { + if (value && !shouldAbortProccessing) { busboy.write(value) } } diff --git a/packages/next/src/layouts/Root/NestProviders.tsx b/packages/next/src/layouts/Root/NestProviders.tsx new file mode 100644 index 00000000000..e92106d4338 --- /dev/null +++ b/packages/next/src/layouts/Root/NestProviders.tsx @@ -0,0 +1,30 @@ +import type { Config, ImportMap } from 'payload' + +import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' +import '@payloadcms/ui/scss/app.scss' +import React from 'react' + +type Args = { + readonly children: React.ReactNode + readonly importMap: ImportMap + readonly providers: Config['admin']['components']['providers'] +} + +export function NestProviders({ children, importMap, providers }: Args): React.ReactNode { + return ( + 1 ? ( + + {children} + + ) : ( + children + ), + }} + Component={providers[0]} + importMap={importMap} + /> + ) +} diff --git a/packages/next/src/layouts/Root/index.tsx b/packages/next/src/layouts/Root/index.tsx index e9a4f45be9a..dcb15503c7f 100644 --- a/packages/next/src/layouts/Root/index.tsx +++ b/packages/next/src/layouts/Root/index.tsx @@ -1,20 +1,19 @@ import type { AcceptedLanguages } from '@payloadcms/translations' -import type { CustomVersionParser, ImportMap, SanitizedConfig } from 'payload' +import type { CustomVersionParser, ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload' import { rtlLanguages } from '@payloadcms/translations' import { RootProvider } from '@payloadcms/ui' import '@payloadcms/ui/scss/app.scss' -import { createClientConfig } from '@payloadcms/ui/utilities/createClientConfig' import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js' import { checkDependencies, parseCookies } from 'payload' import React from 'react' +import { getClientConfig } from '../../utilities/getClientConfig.js' import { getPayloadHMR } from '../../utilities/getPayloadHMR.js' import { getRequestLanguage } from '../../utilities/getRequestLanguage.js' import { getRequestTheme } from '../../utilities/getRequestTheme.js' import { initReq } from '../../utilities/initReq.js' -import { DefaultEditView } from '../../views/Edit/Default/index.js' -import { DefaultListView } from '../../views/List/Default/index.js' +import { NestProviders } from './NestProviders.js' export const metadata = { description: 'Generated by Next.js', @@ -41,11 +40,12 @@ let checkedDependencies = false export const RootLayout = async ({ children, config: configPromise, - importMap, + serverFunction, }: { readonly children: React.ReactNode readonly config: Promise readonly importMap: ImportMap + readonly serverFunction: ServerFunctionClient }) => { if ( process.env.NODE_ENV !== 'production' && @@ -103,16 +103,6 @@ export const RootLayout = async ({ const { i18n, permissions, req, user } = await initReq(config) - const { clientConfig, render } = await createClientConfig({ - children, - config, - DefaultEditView, - DefaultListView, - i18n, - importMap, - payload, - }) - const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode) ? 'RTL' : 'LTR' @@ -174,23 +164,39 @@ export const RootLayout = async ({ const isNavOpen = navPreferences?.value?.open ?? true + const clientConfig = await getClientConfig({ + config, + i18n, + }) + return ( - {render} + {Array.isArray(config.admin?.components?.providers) && + config.admin?.components?.providers.length > 0 ? ( + + {children} + + ) : ( + children + )}
diff --git a/packages/next/src/routes/rest/buildFormState.ts b/packages/next/src/routes/rest/buildFormState.ts deleted file mode 100644 index e997a276cd5..00000000000 --- a/packages/next/src/routes/rest/buildFormState.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { PayloadRequest } from 'payload' - -import { buildFormState as buildFormStateFn } from '@payloadcms/ui/utilities/buildFormState' -import httpStatus from 'http-status' - -import { headersWithCors } from '../../utilities/headersWithCors.js' -import { routeError } from './routeError.js' - -export const buildFormState = async ({ req }: { req: PayloadRequest }) => { - const headers = headersWithCors({ - headers: new Headers(), - req, - }) - - try { - const result = await buildFormStateFn({ req }) - - return Response.json(result, { - headers, - status: httpStatus.OK, - }) - } catch (err) { - req.payload.logger.error({ err, msg: `There was an error building form state` }) - - if (err.message === 'Could not find field schema for given path') { - return Response.json( - { - message: err.message, - }, - { - headers, - status: httpStatus.BAD_REQUEST, - }, - ) - } - - if (err.message === 'Unauthorized') { - return Response.json(null, { - headers, - status: httpStatus.UNAUTHORIZED, - }) - } - - return routeError({ - config: req.payload.config, - err, - req, - }) - } -} diff --git a/packages/next/src/routes/rest/index.ts b/packages/next/src/routes/rest/index.ts index 58a7073854f..4858a9a52d8 100644 --- a/packages/next/src/routes/rest/index.ts +++ b/packages/next/src/routes/rest/index.ts @@ -26,7 +26,6 @@ import { registerFirstUser } from './auth/registerFirstUser.js' import { resetPassword } from './auth/resetPassword.js' import { unlock } from './auth/unlock.js' import { verifyEmail } from './auth/verifyEmail.js' -import { buildFormState } from './buildFormState.js' import { endpointsAreDisabled } from './checkEndpoints.js' import { count } from './collections/count.js' import { create } from './collections/create.js' @@ -110,9 +109,6 @@ const endpoints = { access, og: generateOGImage, }, - POST: { - 'form-state': buildFormState, - }, }, } @@ -575,10 +571,6 @@ export const POST = res = new Response('Route Not Found', { status: 404 }) } } - } else if (slug.length === 1 && slug1 in endpoints.root.POST) { - await addDataAndFileToRequest(req) - addLocalesToRequestFromData(req) - res = await endpoints.root.POST[slug1]({ req }) } if (res instanceof Response) { diff --git a/packages/next/src/routes/rest/og/image.tsx b/packages/next/src/routes/rest/og/image.tsx index 855f3aed2e4..4bc2b7a690f 100644 --- a/packages/next/src/routes/rest/og/image.tsx +++ b/packages/next/src/routes/rest/og/image.tsx @@ -1,15 +1,25 @@ -import type { MappedComponent } from 'payload' +import type { ImportMap, PayloadComponent } from 'payload' -import { RenderComponent } from '@payloadcms/ui/shared' +import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import React from 'react' export const OGImage: React.FC<{ description?: string + Fallback: React.ComponentType fontFamily?: string - Icon: MappedComponent + Icon: PayloadComponent + importMap: ImportMap leader?: string title?: string -}> = ({ description, fontFamily = 'Arial, sans-serif', Icon, leader, title }) => { +}> = ({ + description, + Fallback, + fontFamily = 'Arial, sans-serif', + Icon, + importMap, + leader, + title, +}) => { return (
-
diff --git a/packages/next/src/routes/rest/og/index.tsx b/packages/next/src/routes/rest/og/index.tsx index 7f406ee54dc..ab6bf7fb382 100644 --- a/packages/next/src/routes/rest/og/index.tsx +++ b/packages/next/src/routes/rest/og/index.tsx @@ -1,6 +1,6 @@ import type { PayloadRequest } from 'payload' -import { getCreateMappedComponent, PayloadIcon } from '@payloadcms/ui/shared' +import { PayloadIcon } from '@payloadcms/ui/shared' import fs from 'fs/promises' import { ImageResponse } from 'next/og.js' import { NextResponse } from 'next/server.js' @@ -33,18 +33,6 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => { const leader = hasLeader ? searchParams.get('leader')?.slice(0, 100).replace('-', ' ') : '' const description = searchParams.has('description') ? searchParams.get('description') : '' - const createMappedComponent = getCreateMappedComponent({ - importMap: req.payload.importMap, - serverProps: {}, - }) - - const mappedIcon = createMappedComponent( - config.admin?.components?.graphics?.Icon, - undefined, - PayloadIcon, - 'config.admin.components.graphics.Icon', - ) - let fontData try { @@ -62,8 +50,10 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => { ( diff --git a/packages/next/src/routes/rest/routeError.ts b/packages/next/src/routes/rest/routeError.ts index c9dd6c61d8e..ac5a5ad9b61 100644 --- a/packages/next/src/routes/rest/routeError.ts +++ b/packages/next/src/routes/rest/routeError.ts @@ -1,73 +1,12 @@ import type { Collection, ErrorResult, PayloadRequest, SanitizedConfig } from 'payload' import httpStatus from 'http-status' -import { APIError, APIErrorName, ValidationErrorName } from 'payload' +import { APIError, formatErrors } from 'payload' import { getPayloadHMR } from '../../utilities/getPayloadHMR.js' import { headersWithCors } from '../../utilities/headersWithCors.js' import { mergeHeaders } from '../../utilities/mergeHeaders.js' -const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResult => { - if (incoming) { - // Cannot use `instanceof` to check error type: https://github.com/microsoft/TypeScript/issues/13965 - // Instead, get the prototype of the incoming error and check its constructor name - const proto = Object.getPrototypeOf(incoming) - - // Payload 'ValidationError' and 'APIError' - if ( - (proto.constructor.name === ValidationErrorName || proto.constructor.name === APIErrorName) && - incoming.data - ) { - return { - errors: [ - { - name: incoming.name, - data: incoming.data, - message: incoming.message, - }, - ], - } - } - - // Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError - if (proto.constructor.name === ValidationErrorName && 'errors' in incoming && incoming.errors) { - return { - errors: Object.keys(incoming.errors).reduce((acc, key) => { - acc.push({ - field: incoming.errors[key].path, - message: incoming.errors[key].message, - }) - return acc - }, []), - } - } - - if (Array.isArray(incoming.message)) { - return { - errors: incoming.message, - } - } - - if (incoming.name) { - return { - errors: [ - { - message: incoming.message, - }, - ], - } - } - } - - return { - errors: [ - { - message: 'An unknown error occurred.', - }, - ], - } -} - export const routeError = async ({ collection, config: configArg, diff --git a/packages/next/src/templates/Default/index.tsx b/packages/next/src/templates/Default/index.tsx index 26936a7233f..ee974ab8b0a 100644 --- a/packages/next/src/templates/Default/index.tsx +++ b/packages/next/src/templates/Default/index.tsx @@ -1,7 +1,13 @@ -import type { MappedComponent, ServerProps, VisibleEntities } from 'payload' +import type { CustomComponent, ServerProps, VisibleEntities } from 'payload' -import { AppHeader, BulkUploadProvider, EntityVisibilityProvider, NavToggler } from '@payloadcms/ui' -import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared' +import { + ActionsProvider, + AppHeader, + BulkUploadProvider, + EntityVisibilityProvider, + NavToggler, +} from '@payloadcms/ui' +import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import React from 'react' import { DefaultNav } from '../../elements/Nav/index.js' @@ -14,6 +20,7 @@ const baseClass = 'template-default' export type DefaultTemplateProps = { children?: React.ReactNode className?: string + viewActions?: CustomComponent[] visibleEntities: VisibleEntities } & ServerProps @@ -27,10 +34,13 @@ export const DefaultTemplate: React.FC = ({ permissions, searchParams, user, + viewActions, visibleEntities, }) => { const { admin: { + avatar, + components, components: { header: CustomHeader, Nav: CustomNav } = { header: undefined, Nav: undefined, @@ -38,54 +48,98 @@ export const DefaultTemplate: React.FC = ({ } = {}, } = payload.config || {} - const createMappedComponent = getCreateMappedComponent({ - importMap: payload.importMap, - serverProps: { - i18n, - locale, - params, - payload, - permissions, - searchParams, - user, - }, - }) + const { Actions } = React.useMemo<{ + Actions: Record + }>(() => { + return { + Actions: viewActions + ? viewActions.reduce((acc, action) => { + if (action) { + if (typeof action === 'object') { + acc[action.path] = ( + + ) + } else { + acc[action] = ( + + ) + } + } - const MappedDefaultNav: MappedComponent = createMappedComponent( - CustomNav, - undefined, - DefaultNav, - 'CustomNav', - ) - - const MappedCustomHeader = createMappedComponent( - CustomHeader, - undefined, - undefined, - 'CustomHeader', - ) + return acc + }, {}) + : undefined, + } + }, [viewActions, payload]) return ( - -
-