feat!: on demand rsc (#8364)
Currently, Payload renders all custom components on initial compile of the admin panel. This is problematic for two key reasons: 1. Custom components do not receive contextual data, i.e. fields do not receive their field data, edit views do not receive their document data, etc. 2. Components are unnecessarily rendered before they are used This was initially required to support React Server Components within the Payload Admin Panel for two key reasons: 1. Fields can be dynamically rendered within arrays, blocks, etc. 2. Documents can be recursively rendered within a "drawer" UI, i.e. relationship fields 3. Payload supports server/client component composition In order to achieve this, components need to be rendered on the server and passed as "slots" to the client. Currently, the pattern for this is to render custom server components in the "client config". Then when a view or field is needed to be rendered, we first check the client config for a "pre-rendered" component, otherwise render our client-side fallback component. But for the reasons listed above, this pattern doesn't exactly make custom server components very useful within the Payload Admin Panel, which is where this PR comes in. Now, instead of pre-rendering all components on initial compile, we're able to render custom components _on demand_, only as they are needed. To achieve this, we've established [this pattern](https://github.com/payloadcms/payload/pull/8481) of React Server Functions in the Payload Admin Panel. With Server Functions, we can iterate the Payload Config and return JSX through React's `text/x-component` content-type. This means we're able to pass contextual props to custom components, such as data for fields and views. ## Breaking Changes 1. Add the following to your root layout file, typically located at `(app)/(payload)/layout.tsx`: ```diff /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ + import type { ServerFunctionClient } from 'payload' import config from '@payload-config' import { RootLayout } from '@payloadcms/next/layouts' import { handleServerFunctions } from '@payloadcms/next/utilities' import React from 'react' import { importMap } from './admin/importMap.js' import './custom.scss' type Args = { children: React.ReactNode } + const serverFunctions: ServerFunctionClient = async function (args) { + 'use server' + return handleServerFunctions({ + ...args, + config, + importMap, + }) + } const Layout = ({ children }: Args) => ( <RootLayout config={config} importMap={importMap} + serverFunctions={serverFunctions} > {children} </RootLayout> ) export default Layout ``` 2. If you were previously posting to the `/api/form-state` endpoint, it no longer exists. Instead, you'll need to invoke the `form-state` Server Function, which can be done through the _new_ `getFormState` utility: ```diff - import { getFormState } from '@payloadcms/ui' - const { state } = await getFormState({ - apiRoute: '', - body: { - // ... - }, - serverURL: '' - }) + const { getFormState } = useServerFunctions() + + const { state } = await getFormState({ + // ... + }) ``` ## Breaking Changes ```diff - useFieldProps() - useCellProps() ``` More details coming soon. --------- Co-authored-by: Alessio Gravili <alessio@gravili.de> Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com> Co-authored-by: James <james@trbl.design>
This commit is contained in:
@@ -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) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,11 @@ const config = withBundleAnalyzer(
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
|
||||
@@ -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",
|
||||
|
||||
48
packages/db-mongodb/src/countGlobalVersions.ts
Normal file
48
packages/db-mongodb/src/countGlobalVersions.ts
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
48
packages/db-mongodb/src/countVersions.ts
Normal file
48
packages/db-mongodb/src/countVersions.ts
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -126,6 +128,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -114,6 +116,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -127,6 +129,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
42
packages/drizzle/src/countGlobalVersions.ts
Normal file
42
packages/drizzle/src/countGlobalVersions.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { CountGlobalVersions, SanitizedGlobalConfig } from 'payload'
|
||||
|
||||
import { buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
this: DrizzleAdapter,
|
||||
{ global, locale, req, where: whereArg },
|
||||
) {
|
||||
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
|
||||
({ slug }) => 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 }
|
||||
}
|
||||
40
packages/drizzle/src/countVersions.ts
Normal file
40
packages/drizzle/src/countVersions.ts
Normal file
@@ -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 }
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -391,8 +391,8 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
id,
|
||||
errors: [
|
||||
{
|
||||
field: fieldName,
|
||||
message: req.t('error:valueMustBeUnique'),
|
||||
path: fieldName,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<DocumentTabLink
|
||||
adminRoute={routes.admin}
|
||||
@@ -82,12 +72,21 @@ export const DocumentTab: React.FC<
|
||||
>
|
||||
<span className={`${baseClass}__label`}>
|
||||
{labelToRender}
|
||||
{mappedPin && (
|
||||
{Pill || Pill_Component ? (
|
||||
<Fragment>
|
||||
|
||||
<RenderComponent mappedComponent={mappedPin} />
|
||||
<RenderServerComponent
|
||||
Component={Pill}
|
||||
Fallback={Pill_Component}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
) : null}
|
||||
</span>
|
||||
</DocumentTabLink>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<RenderComponent
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
key: `tab-custom-${index}`,
|
||||
path,
|
||||
}}
|
||||
Component={tab.Component}
|
||||
importMap={payload.importMap}
|
||||
key={`tab-custom-${index}`}
|
||||
mappedComponent={mappedTab}
|
||||
serverProps={{
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -121,6 +109,7 @@ export const DocumentTabs: React.FC<{
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</ul>
|
||||
|
||||
@@ -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 <span className={`${baseClass}__count`}>{totalVersions}</span>
|
||||
return <span className={`${baseClass}__count`}>{versionCount}</span>
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Gutter className={baseClass}>
|
||||
{customHeader && customHeader}
|
||||
{!customHeader && (
|
||||
<Fragment>
|
||||
<RenderTitle className={`${baseClass}__title`} />
|
||||
{!hideTabs && (
|
||||
<DocumentTabs
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
<RenderTitle className={`${baseClass}__title`} />
|
||||
{!hideTabs && (
|
||||
<DocumentTabs
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
</Gutter>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<EmailField
|
||||
autoComplete="off"
|
||||
field={{
|
||||
name: 'email',
|
||||
label: t('general:email'),
|
||||
required: requireEmail,
|
||||
}}
|
||||
validate={email}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function UsernameFieldComponent(props: Props) {
|
||||
const { loginWithUsername } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
|
||||
const showUsernameField = Boolean(loginWithUsername)
|
||||
|
||||
if (showUsernameField) {
|
||||
return (
|
||||
<TextField
|
||||
field={{
|
||||
name: 'username',
|
||||
label: t('authentication:username'),
|
||||
required: requireUsername,
|
||||
}}
|
||||
validate={username}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<RenderFields
|
||||
className={className}
|
||||
fields={[
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
admin: {
|
||||
autoComplete: 'off',
|
||||
components: {
|
||||
Field: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
|
||||
},
|
||||
},
|
||||
},
|
||||
localized: false,
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
|
||||
},
|
||||
},
|
||||
},
|
||||
localized: false,
|
||||
},
|
||||
]}
|
||||
forceRender
|
||||
operation={operation}
|
||||
path=""
|
||||
permissions={permissions}
|
||||
readOnly={readOnly}
|
||||
schemaPath=""
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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<ServerProps> = (props) => {
|
||||
@@ -16,20 +17,20 @@ export const Logo: React.FC<ServerProps> = (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 <RenderComponent mappedComponent={mappedCustomLogo} />
|
||||
return (
|
||||
<RenderServerComponent
|
||||
Component={CustomLogo}
|
||||
Fallback={PayloadLogo}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<typeof groupNavItems>
|
||||
}> = ({ 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 (
|
||||
<Fragment>
|
||||
{groups.map(({ entities, label }, key) => {
|
||||
return (
|
||||
<NavGroup key={key} label={label}>
|
||||
{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 && <div className={`${baseClass}__link-indicator`} />}
|
||||
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
|
||||
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
|
||||
</LinkElement>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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<NavProps> = (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<NavProps> = (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 (
|
||||
<NavWrapper baseClass={baseClass}>
|
||||
<nav className={`${baseClass}__wrap`}>
|
||||
<RenderComponent mappedComponent={mappedBeforeNavLinks} />
|
||||
<DefaultNavClient />
|
||||
<RenderComponent mappedComponent={mappedAfterNavLinks} />
|
||||
<RenderServerComponent
|
||||
Component={beforeNavLinks}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<DefaultNavClient groups={groups} />
|
||||
<RenderServerComponent
|
||||
Component={afterNavLinks}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Logout />
|
||||
<RenderServerComponent
|
||||
Component={logout?.Button}
|
||||
Fallback={Logout}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
<div className={`${baseClass}__header`}>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { metadata, RootLayout } from '../layouts/Root/index.js'
|
||||
export { handleServerFunctions } from '../utilities/handleServerFunctions.js'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
function abortAndDestroyFile(file: Readable, err: APIError) {
|
||||
file.destroy()
|
||||
parsingRequest = false
|
||||
failedResolvingFiles(err)
|
||||
}
|
||||
const reader = request.body.getReader()
|
||||
|
||||
const busboy = Busboy({ ...options, headers: headersObject })
|
||||
|
||||
function abortAndDestroyFile(file: Readable, err: APIError) {
|
||||
file.destroy()
|
||||
shouldAbortProccessing = true
|
||||
failedResolvingFiles(err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
30
packages/next/src/layouts/Root/NestProviders.tsx
Normal file
30
packages/next/src/layouts/Root/NestProviders.tsx
Normal file
@@ -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 (
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
children:
|
||||
providers.length > 1 ? (
|
||||
<NestProviders importMap={importMap} providers={providers.slice(1)}>
|
||||
{children}
|
||||
</NestProviders>
|
||||
) : (
|
||||
children
|
||||
),
|
||||
}}
|
||||
Component={providers[0]}
|
||||
importMap={importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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<SanitizedConfig>
|
||||
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 (
|
||||
<html data-theme={theme} dir={dir} lang={languageCode}>
|
||||
<body>
|
||||
<RootProvider
|
||||
config={clientConfig}
|
||||
dateFNSKey={i18n.dateFNSKey}
|
||||
fallbackLang={clientConfig.i18n.fallbackLanguage}
|
||||
fallbackLang={config.i18n.fallbackLanguage}
|
||||
isNavOpen={isNavOpen}
|
||||
languageCode={languageCode}
|
||||
languageOptions={languageOptions}
|
||||
permissions={permissions}
|
||||
serverFunction={serverFunction}
|
||||
switchLanguageServerAction={switchLanguageServerAction}
|
||||
theme={theme}
|
||||
translations={i18n.translations}
|
||||
user={user}
|
||||
>
|
||||
{render}
|
||||
{Array.isArray(config.admin?.components?.providers) &&
|
||||
config.admin?.components?.providers.length > 0 ? (
|
||||
<NestProviders
|
||||
importMap={payload.importMap}
|
||||
providers={config.admin?.components?.providers}
|
||||
>
|
||||
{children}
|
||||
</NestProviders>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</RootProvider>
|
||||
<div id="portal" />
|
||||
</body>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
@@ -85,11 +95,13 @@ export const OGImage: React.FC<{
|
||||
width: '38px',
|
||||
}}
|
||||
>
|
||||
<RenderComponent
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
fill: 'white',
|
||||
}}
|
||||
mappedComponent={Icon}
|
||||
Component={Icon}
|
||||
Fallback={Fallback}
|
||||
importMap={importMap}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 }) => {
|
||||
(
|
||||
<OGImage
|
||||
description={description}
|
||||
Fallback={PayloadIcon}
|
||||
fontFamily={fontFamily}
|
||||
Icon={mappedIcon}
|
||||
Icon={config.admin?.components?.graphics?.Icon}
|
||||
importMap={req.payload.importMap}
|
||||
leader={leader}
|
||||
title={title}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<DefaultTemplateProps> = ({
|
||||
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<DefaultTemplateProps> = ({
|
||||
} = {},
|
||||
} = payload.config || {}
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
const { Actions } = React.useMemo<{
|
||||
Actions: Record<string, React.ReactNode>
|
||||
}>(() => {
|
||||
return {
|
||||
Actions: viewActions
|
||||
? viewActions.reduce((acc, action) => {
|
||||
if (action) {
|
||||
if (typeof action === 'object') {
|
||||
acc[action.path] = (
|
||||
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||
)
|
||||
} else {
|
||||
acc[action] = (
|
||||
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const MappedDefaultNav: MappedComponent = createMappedComponent(
|
||||
CustomNav,
|
||||
undefined,
|
||||
DefaultNav,
|
||||
'CustomNav',
|
||||
)
|
||||
|
||||
const MappedCustomHeader = createMappedComponent(
|
||||
CustomHeader,
|
||||
undefined,
|
||||
undefined,
|
||||
'CustomHeader',
|
||||
)
|
||||
return acc
|
||||
}, {})
|
||||
: undefined,
|
||||
}
|
||||
}, [viewActions, payload])
|
||||
|
||||
return (
|
||||
<EntityVisibilityProvider visibleEntities={visibleEntities}>
|
||||
<BulkUploadProvider>
|
||||
<RenderComponent mappedComponent={MappedCustomHeader} />
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
||||
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
|
||||
<NavToggler className={`${baseClass}__nav-toggler`}>
|
||||
<NavHamburger />
|
||||
</NavToggler>
|
||||
<ActionsProvider Actions={Actions}>
|
||||
<RenderServerComponent
|
||||
clientProps={{ clientProps: { visibleEntities } }}
|
||||
Component={CustomHeader}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
||||
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
|
||||
<NavToggler className={`${baseClass}__nav-toggler`}>
|
||||
<NavHamburger />
|
||||
</NavToggler>
|
||||
</div>
|
||||
</div>
|
||||
<Wrapper baseClass={baseClass} className={className}>
|
||||
<RenderServerComponent
|
||||
clientProps={{ clientProps: { visibleEntities } }}
|
||||
Component={CustomNav}
|
||||
Fallback={DefaultNav}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<AppHeader
|
||||
CustomAvatar={
|
||||
avatar !== 'gravatar' && avatar !== 'default' ? (
|
||||
<RenderServerComponent
|
||||
Component={avatar.Component}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
CustomIcon={
|
||||
components?.graphics?.Icon ? (
|
||||
<RenderServerComponent
|
||||
Component={components.graphics.Icon}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
<Wrapper baseClass={baseClass} className={className}>
|
||||
<RenderComponent mappedComponent={MappedDefaultNav} />
|
||||
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<AppHeader />
|
||||
{children}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
</ActionsProvider>
|
||||
</BulkUploadProvider>
|
||||
</EntityVisibilityProvider>
|
||||
)
|
||||
|
||||
18
packages/next/src/utilities/getClientConfig.ts
Normal file
18
packages/next/src/utilities/getClientConfig.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientConfig, SanitizedConfig } from 'payload'
|
||||
|
||||
import { createClientConfig } from 'payload'
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getClientConfig = cache(
|
||||
async (args: { config: SanitizedConfig; i18n: I18nClient }): Promise<ClientConfig> => {
|
||||
const { config, i18n } = args
|
||||
|
||||
const clientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return Promise.resolve(clientConfig)
|
||||
},
|
||||
)
|
||||
37
packages/next/src/utilities/handleServerFunctions.ts
Normal file
37
packages/next/src/utilities/handleServerFunctions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { ServerFunction, ServerFunctionHandler } from 'payload'
|
||||
|
||||
import { buildFormStateHandler } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { buildTableStateHandler } from '@payloadcms/ui/utilities/buildTableState'
|
||||
|
||||
import { renderDocumentHandler } from '../views/Document/handleServerFunction.js'
|
||||
import { renderDocumentSlotsHandler } from '../views/Document/renderDocumentSlots.js'
|
||||
import { renderListHandler } from '../views/List/handleServerFunction.js'
|
||||
import { initReq } from './initReq.js'
|
||||
|
||||
export const handleServerFunctions: ServerFunctionHandler = async (args) => {
|
||||
const { name: fnKey, args: fnArgs, config: configPromise, importMap } = args
|
||||
|
||||
const { req } = await initReq(configPromise)
|
||||
|
||||
const augmentedArgs: Parameters<ServerFunction>[0] = {
|
||||
...fnArgs,
|
||||
importMap,
|
||||
req,
|
||||
}
|
||||
|
||||
const serverFunctions = {
|
||||
'form-state': buildFormStateHandler as any as ServerFunction,
|
||||
'render-document': renderDocumentHandler as any as ServerFunction,
|
||||
'render-document-slots': renderDocumentSlotsHandler as any as ServerFunction,
|
||||
'render-list': renderListHandler as any as ServerFunction,
|
||||
'table-state': buildTableStateHandler as any as ServerFunction,
|
||||
}
|
||||
|
||||
const fn = serverFunctions[fnKey]
|
||||
|
||||
if (!fn) {
|
||||
throw new Error(`Unknown Server Function: ${fnKey}`)
|
||||
}
|
||||
|
||||
return fn(augmentedArgs)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { InitPageResult, Locale, PayloadRequest, VisibleEntities } from 'payload'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { InitPageResult, Locale, VisibleEntities } from 'payload'
|
||||
|
||||
import { findLocaleFromCode } from '@payloadcms/ui/shared'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
@@ -47,13 +48,13 @@ export const initPage = async ({
|
||||
req: {
|
||||
headers,
|
||||
host: headers.get('host'),
|
||||
i18n,
|
||||
i18n: i18n as I18n,
|
||||
query: qs.parse(queryString, {
|
||||
depth: 10,
|
||||
ignoreQueryPrefix: true,
|
||||
}),
|
||||
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
|
||||
} as PayloadRequest,
|
||||
},
|
||||
},
|
||||
payload,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
@@ -16,7 +16,10 @@ type Result = {
|
||||
user: User
|
||||
}
|
||||
|
||||
export const initReq = cache(async function (config: SanitizedConfig): Promise<Result> {
|
||||
export const initReq = cache(async function (
|
||||
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
|
||||
): Promise<Result> {
|
||||
const config = await configPromise
|
||||
const payload = await getPayloadHMR({ config })
|
||||
|
||||
const headers = await getHeaders()
|
||||
@@ -40,9 +43,9 @@ export const initReq = cache(async function (config: SanitizedConfig): Promise<R
|
||||
req: {
|
||||
headers,
|
||||
host: headers.get('host'),
|
||||
i18n,
|
||||
i18n: i18n as I18n,
|
||||
url: `${payload.config.serverURL}`,
|
||||
} as PayloadRequest,
|
||||
},
|
||||
},
|
||||
payload,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ export const LocaleSelector: React.FC<{
|
||||
<SelectField
|
||||
field={{
|
||||
name: 'locale',
|
||||
_path: 'locale',
|
||||
label: t('general:locale'),
|
||||
options: localeOptions,
|
||||
}}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Gutter,
|
||||
MinimizeMaximizeIcon,
|
||||
NumberField,
|
||||
SetViewActions,
|
||||
SetDocumentStepNav,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useLocale,
|
||||
@@ -19,7 +19,6 @@ import { useSearchParams } from 'next/navigation.js'
|
||||
import * as React from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import './index.scss'
|
||||
import { LocaleSelector } from './LocaleSelector/index.js'
|
||||
import { RenderJSON } from './RenderJSON/index.js'
|
||||
@@ -42,8 +41,8 @@ export const APIViewClient: React.FC = () => {
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const collectionClientConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalClientConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
|
||||
const localeOptions =
|
||||
localization &&
|
||||
@@ -52,13 +51,13 @@ export const APIViewClient: React.FC = () => {
|
||||
let draftsEnabled: boolean = false
|
||||
let docEndpoint: string = ''
|
||||
|
||||
if (collectionClientConfig) {
|
||||
draftsEnabled = Boolean(collectionClientConfig.versions?.drafts)
|
||||
if (collectionConfig) {
|
||||
draftsEnabled = Boolean(collectionConfig.versions?.drafts)
|
||||
docEndpoint = `/${collectionSlug}/${id}`
|
||||
}
|
||||
|
||||
if (globalClientConfig) {
|
||||
draftsEnabled = Boolean(globalClientConfig.versions?.drafts)
|
||||
if (globalConfig) {
|
||||
draftsEnabled = Boolean(globalConfig.versions?.drafts)
|
||||
docEndpoint = `/globals/${globalSlug}`
|
||||
}
|
||||
|
||||
@@ -111,19 +110,13 @@ export const APIViewClient: React.FC = () => {
|
||||
>
|
||||
<SetDocumentStepNav
|
||||
collectionSlug={collectionSlug}
|
||||
globalLabel={globalClientConfig?.label}
|
||||
globalLabel={globalConfig?.label}
|
||||
globalSlug={globalSlug}
|
||||
id={id}
|
||||
pluralLabel={collectionClientConfig ? collectionClientConfig?.labels?.plural : undefined}
|
||||
useAsTitle={collectionClientConfig ? collectionClientConfig?.admin?.useAsTitle : undefined}
|
||||
pluralLabel={collectionConfig ? collectionConfig?.labels?.plural : undefined}
|
||||
useAsTitle={collectionConfig ? collectionConfig?.admin?.useAsTitle : undefined}
|
||||
view="API"
|
||||
/>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionClientConfig || globalClientConfig)?.admin?.components?.views?.edit?.api
|
||||
?.actions
|
||||
}
|
||||
/>
|
||||
<div className={`${baseClass}__configuration`}>
|
||||
<div className={`${baseClass}__api-url`}>
|
||||
<span className={`${baseClass}__label`}>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Settings: React.FC<{
|
||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
<h3>{i18n.t('general:payloadSettings')}</h3>
|
||||
<div className={`${baseClass}__language`}>
|
||||
<FieldLabel field={null} htmlFor="language-select" label={i18n.t('general:language')} />
|
||||
<FieldLabel htmlFor="language-select" label={i18n.t('general:language')} />
|
||||
<LanguageSelector languageOptions={languageOptions} />
|
||||
</div>
|
||||
{theme === 'all' && <ToggleTheme />}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import {
|
||||
DocumentInfoProvider,
|
||||
EditDepthProvider,
|
||||
HydrateAuthProvider,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui'
|
||||
import { getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
||||
import { getDocPreferences } from '../Document/getDocPreferences.js'
|
||||
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||
import { getIsLocked } from '../Document/getIsLocked.js'
|
||||
import { getVersions } from '../Document/getVersions.js'
|
||||
import { EditView } from '../Edit/index.js'
|
||||
import { AccountClient } from './index.client.js'
|
||||
import { Settings } from './Settings/index.js'
|
||||
@@ -50,57 +49,91 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
const collectionConfig = config.collections.find((collection) => collection.slug === userSlug)
|
||||
|
||||
if (collectionConfig && user?.id) {
|
||||
// Fetch the data required for the view
|
||||
const data = await getDocumentData({
|
||||
id: user.id,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
locale,
|
||||
payload,
|
||||
user,
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
// Get document preferences
|
||||
const docPreferences = await getDocPreferences({
|
||||
id: user.id,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
payload,
|
||||
user,
|
||||
})
|
||||
|
||||
// Get permissions
|
||||
const { docPermissions, hasPublishPermission, hasSavePermission } =
|
||||
await getDocumentPermissions({
|
||||
id: user.id,
|
||||
collectionConfig,
|
||||
data: user,
|
||||
data,
|
||||
req,
|
||||
})
|
||||
|
||||
const { data, formState } = await getDocumentData({
|
||||
// Build initial form state from data
|
||||
const { state: formState } = await buildFormState({
|
||||
id: user.id,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
locale: locale?.code,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionConfig.slug,
|
||||
})
|
||||
|
||||
// Fetch document lock state
|
||||
const { currentEditor, isLocked, lastUpdateTime } = await getIsLocked({
|
||||
id: user.id,
|
||||
collectionConfig,
|
||||
locale,
|
||||
req,
|
||||
isEditing: true,
|
||||
payload: req.payload,
|
||||
user,
|
||||
})
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
// Get all versions required for UI
|
||||
const { hasPublishedDoc, mostRecentVersionIsAutosaved, unpublishedVersionCount, versionCount } =
|
||||
await getVersions({
|
||||
id: user.id,
|
||||
collectionConfig,
|
||||
docPermissions,
|
||||
locale: locale?.code,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: [],
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedAccountComponent = createMappedComponent(
|
||||
CustomAccountComponent?.Component,
|
||||
undefined,
|
||||
EditView,
|
||||
'CustomAccountComponent.Component',
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} theme={theme} />}
|
||||
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
||||
collectionSlug={userSlug}
|
||||
currentEditor={currentEditor}
|
||||
docPermissions={docPermissions}
|
||||
hasPublishedDoc={hasPublishedDoc}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={user?.id}
|
||||
initialData={data}
|
||||
initialState={formState}
|
||||
isEditing
|
||||
isLocked={isLocked}
|
||||
lastUpdateTime={lastUpdateTime}
|
||||
mostRecentVersionIsAutosaved={mostRecentVersionIsAutosaved}
|
||||
unpublishedVersionCount={unpublishedVersionCount}
|
||||
versionCount={versionCount}
|
||||
>
|
||||
<EditDepthProvider depth={1}>
|
||||
<EditDepthProvider>
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
hideTabs
|
||||
@@ -109,7 +142,22 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
permissions={permissions}
|
||||
/>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<RenderComponent mappedComponent={mappedAccountComponent} />
|
||||
<RenderServerComponent
|
||||
Component={CustomAccountComponent}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: [],
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<EditView />
|
||||
<AccountClient />
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
'use client'
|
||||
import type { FormProps, UserWithToken } from '@payloadcms/ui'
|
||||
import type { ClientCollectionConfig, FormState, LoginWithUsernameOptions } from 'payload'
|
||||
import type {
|
||||
ClientCollectionConfig,
|
||||
DocumentPermissions,
|
||||
DocumentPreferences,
|
||||
FormState,
|
||||
LoginWithUsernameOptions,
|
||||
} from 'payload'
|
||||
|
||||
import {
|
||||
ConfirmPasswordField,
|
||||
EmailAndUsernameFields,
|
||||
Form,
|
||||
FormSubmit,
|
||||
PasswordField,
|
||||
RenderFields,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { RenderEmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
|
||||
|
||||
export const CreateFirstUserClient: React.FC<{
|
||||
docPermissions: DocumentPermissions
|
||||
docPreferences: DocumentPreferences
|
||||
initialState: FormState
|
||||
loginWithUsername?: false | LoginWithUsernameOptions
|
||||
userSlug: string
|
||||
}> = ({ initialState, loginWithUsername, userSlug }) => {
|
||||
}> = ({ docPermissions, docPreferences, initialState, loginWithUsername, userSlug }) => {
|
||||
const {
|
||||
config: {
|
||||
routes: { admin, api: apiRoute },
|
||||
@@ -30,6 +37,8 @@ export const CreateFirstUserClient: React.FC<{
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { setUser } = useAuth()
|
||||
|
||||
@@ -38,18 +47,17 @@ export const CreateFirstUserClient: React.FC<{
|
||||
const onChange: FormProps['onChange'][0] = React.useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
const { state } = await getFormState({
|
||||
apiRoute,
|
||||
body: {
|
||||
collectionSlug: userSlug,
|
||||
formState: prevFormState,
|
||||
operation: 'create',
|
||||
schemaPath: `_${userSlug}.auth`,
|
||||
},
|
||||
serverURL,
|
||||
collectionSlug: userSlug,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
formState: prevFormState,
|
||||
operation: 'create',
|
||||
schemaPath: `_${userSlug}.auth`,
|
||||
})
|
||||
|
||||
return state
|
||||
},
|
||||
[apiRoute, userSlug, serverURL],
|
||||
[userSlug, getFormState, docPermissions, docPreferences],
|
||||
)
|
||||
|
||||
const handleFirstRegister = (data: UserWithToken) => {
|
||||
@@ -66,14 +74,15 @@ export const CreateFirstUserClient: React.FC<{
|
||||
redirect={admin}
|
||||
validationOperation="create"
|
||||
>
|
||||
<RenderEmailAndUsernameFields
|
||||
<EmailAndUsernameFields
|
||||
className="emailAndUsername"
|
||||
loginWithUsername={loginWithUsername}
|
||||
operation="create"
|
||||
readOnly={false}
|
||||
t={t}
|
||||
/>
|
||||
<PasswordField
|
||||
autoComplete={'off'}
|
||||
autoComplete="off"
|
||||
field={{
|
||||
name: 'password',
|
||||
label: t('authentication:newPassword'),
|
||||
@@ -84,10 +93,11 @@ export const CreateFirstUserClient: React.FC<{
|
||||
<RenderFields
|
||||
fields={collectionConfig.fields}
|
||||
forceRender
|
||||
operation="create"
|
||||
path=""
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath={userSlug}
|
||||
permissions={null}
|
||||
readOnly={false}
|
||||
schemaPath={userSlug}
|
||||
/>
|
||||
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
||||
</Form>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import React from 'react'
|
||||
|
||||
import { getDocPreferences } from '../Document/getDocPreferences.js'
|
||||
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||
import { CreateFirstUserClient } from './index.client.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -26,11 +29,39 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
const { auth: authOptions } = collectionConfig
|
||||
const loginWithUsername = authOptions.loginWithUsername
|
||||
|
||||
const { formState } = await getDocumentData({
|
||||
collectionConfig,
|
||||
// Fetch the data required for the view
|
||||
const data = await getDocumentData({
|
||||
collectionSlug: collectionConfig.slug,
|
||||
locale,
|
||||
payload: req.payload,
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
// Get document preferences
|
||||
const docPreferences = await getDocPreferences({
|
||||
collectionSlug: collectionConfig.slug,
|
||||
payload: req.payload,
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
// Get permissions
|
||||
const { docPermissions } = await getDocumentPermissions({
|
||||
collectionConfig,
|
||||
data,
|
||||
req,
|
||||
schemaPath: `_${collectionConfig.slug}.auth`,
|
||||
})
|
||||
|
||||
// Build initial form state from data
|
||||
const { state: formState } = await buildFormState({
|
||||
collectionSlug: collectionConfig.slug,
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
locale: locale?.code,
|
||||
operation: 'create',
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionConfig.slug,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -38,6 +69,8 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
<h1>{req.t('general:welcome')}</h1>
|
||||
<p>{req.t('authentication:beginCreateFirstUser')}</p>
|
||||
<CreateFirstUserClient
|
||||
docPermissions={docPermissions}
|
||||
docPreferences={docPreferences}
|
||||
initialState={formState}
|
||||
loginWithUsername={loginWithUsername}
|
||||
userSlug={userSlug}
|
||||
|
||||
@@ -2,13 +2,9 @@ import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Button, Card, Gutter, Locked, SetStepNav, SetViewActions } from '@payloadcms/ui'
|
||||
import {
|
||||
EntityType,
|
||||
formatAdminURL,
|
||||
getCreateMappedComponent,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
@@ -50,41 +46,25 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
user,
|
||||
} = props
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedBeforeDashboards = createMappedComponent(
|
||||
beforeDashboard,
|
||||
undefined,
|
||||
undefined,
|
||||
'beforeDashboard',
|
||||
)
|
||||
|
||||
const mappedAfterDashboards = createMappedComponent(
|
||||
afterDashboard,
|
||||
undefined,
|
||||
undefined,
|
||||
'afterDashboard',
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SetStepNav nav={[]} />
|
||||
<SetViewActions actions={[]} />
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<RenderComponent mappedComponent={mappedBeforeDashboards} />
|
||||
{beforeDashboard && (
|
||||
<RenderServerComponent
|
||||
Component={beforeDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Fragment>
|
||||
<SetViewActions actions={[]} />
|
||||
{!navGroups || navGroups?.length === 0 ? (
|
||||
<p>no nav groups....</p>
|
||||
) : (
|
||||
@@ -93,7 +73,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
<div className={`${baseClass}__group`} key={groupIndex}>
|
||||
<h2 className={`${baseClass}__label`}>{label}</h2>
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{entities.map(({ type, entity }, entityIndex) => {
|
||||
{entities.map(({ slug, type, label }, entityIndex) => {
|
||||
let title: string
|
||||
let buttonAriaLabel: string
|
||||
let createHREF: string
|
||||
@@ -103,38 +83,34 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
let userEditing = null
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
title = getTranslation(entity.labels.plural, i18n)
|
||||
title = getTranslation(label, i18n)
|
||||
|
||||
buttonAriaLabel = t('general:showAllLabel', { label: title })
|
||||
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
|
||||
|
||||
createHREF = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${entity.slug}/create`,
|
||||
path: `/collections/${slug}/create`,
|
||||
})
|
||||
|
||||
hasCreatePermission =
|
||||
permissions?.collections?.[entity.slug]?.create?.permission
|
||||
hasCreatePermission = permissions?.collections?.[slug]?.create?.permission
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
title = getTranslation(entity.label, i18n)
|
||||
title = getTranslation(label, i18n)
|
||||
|
||||
buttonAriaLabel = t('general:editLabel', {
|
||||
label: getTranslation(entity.label, i18n),
|
||||
label: getTranslation(label, i18n),
|
||||
})
|
||||
|
||||
href = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${entity.slug}`,
|
||||
path: `/globals/${slug}`,
|
||||
})
|
||||
|
||||
// Find the lock status for the global
|
||||
const globalLockData = globalData.find(
|
||||
(global) => global.slug === entity.slug,
|
||||
)
|
||||
|
||||
const globalLockData = globalData.find((global) => global.slug === slug)
|
||||
if (globalLockData) {
|
||||
isLocked = globalLockData.data._isLocked
|
||||
userEditing = globalLockData.data._userEditing
|
||||
@@ -164,7 +140,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
) : hasCreatePermission && type === EntityType.collection ? (
|
||||
<Button
|
||||
aria-label={t('general:createNewLabel', {
|
||||
label: getTranslation(entity.labels.singular, i18n),
|
||||
label,
|
||||
})}
|
||||
buttonStyle="icon-label"
|
||||
el="link"
|
||||
@@ -178,9 +154,9 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
}
|
||||
buttonAriaLabel={buttonAriaLabel}
|
||||
href={href}
|
||||
id={`card-${entity.slug}`}
|
||||
id={`card-${slug}`}
|
||||
Link={Link}
|
||||
title={title}
|
||||
title={getTranslation(label, i18n)}
|
||||
titleAs="h3"
|
||||
/>
|
||||
</li>
|
||||
@@ -192,7 +168,21 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
})
|
||||
)}
|
||||
</Fragment>
|
||||
<RenderComponent mappedComponent={mappedAfterDashboards} />
|
||||
{afterDashboard && (
|
||||
<RenderServerComponent
|
||||
Component={afterDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import {
|
||||
EntityType,
|
||||
getCreateMappedComponent,
|
||||
groupNavItems,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { HydrateAuthProvider, SetStepNav } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -111,39 +107,31 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
|
||||
i18n,
|
||||
)
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
globalData,
|
||||
i18n,
|
||||
Link,
|
||||
locale,
|
||||
navGroups,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedDashboardComponent = createMappedComponent(
|
||||
CustomDashboardComponent?.Component,
|
||||
undefined,
|
||||
DefaultDashboard,
|
||||
'CustomDashboardComponent.Component',
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<RenderComponent
|
||||
<SetStepNav nav={[]} />
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
Link,
|
||||
locale,
|
||||
}}
|
||||
mappedComponent={mappedDashboardComponent}
|
||||
Component={CustomDashboardComponent}
|
||||
Fallback={DefaultDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
globalData,
|
||||
i18n,
|
||||
Link,
|
||||
locale,
|
||||
navGroups,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
60
packages/next/src/views/Document/getDocPreferences.ts
Normal file
60
packages/next/src/views/Document/getDocPreferences.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { DocumentPreferences, Payload, TypedUser } from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionSlug?: string
|
||||
globalSlug?: string
|
||||
id?: number | string
|
||||
payload: Payload
|
||||
user: TypedUser
|
||||
}
|
||||
|
||||
export const getDocPreferences = async ({
|
||||
id,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
payload,
|
||||
user,
|
||||
}: Args): Promise<DocumentPreferences> => {
|
||||
let preferencesKey
|
||||
|
||||
if (collectionSlug && id) {
|
||||
preferencesKey = `collection-${collectionSlug}-${id}`
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
preferencesKey = `global-${globalSlug}`
|
||||
}
|
||||
|
||||
if (preferencesKey) {
|
||||
const preferencesResult = (await payload.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: preferencesKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})) as unknown as { docs: { value: DocumentPreferences }[] }
|
||||
|
||||
if (preferencesResult?.docs?.[0]?.value) {
|
||||
return preferencesResult.docs[0].value
|
||||
}
|
||||
}
|
||||
|
||||
return { fields: {} }
|
||||
}
|
||||
52
packages/next/src/views/Document/getDocumentData.ts
Normal file
52
packages/next/src/views/Document/getDocumentData.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Locale, Payload, TypedUser, TypeWithID } from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionSlug?: string
|
||||
globalSlug?: string
|
||||
id?: number | string
|
||||
locale?: Locale
|
||||
payload: Payload
|
||||
user?: TypedUser
|
||||
}
|
||||
|
||||
export const getDocumentData = async ({
|
||||
id,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
user,
|
||||
}: Args): Promise<null | Record<string, unknown> | TypeWithID> => {
|
||||
let resolvedData: Record<string, unknown> | TypeWithID = null
|
||||
|
||||
try {
|
||||
if (collectionSlug && id) {
|
||||
resolvedData = await payload.findByID({
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
draft: true,
|
||||
fallbackLocale: null,
|
||||
locale: locale?.code,
|
||||
overrideAccess: false,
|
||||
user,
|
||||
})
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
resolvedData = await payload.findGlobal({
|
||||
slug: globalSlug,
|
||||
depth: 0,
|
||||
draft: true,
|
||||
fallbackLocale: null,
|
||||
locale: locale?.code,
|
||||
overrideAccess: false,
|
||||
user,
|
||||
})
|
||||
}
|
||||
} catch (_err) {
|
||||
payload.logger.error(_err)
|
||||
}
|
||||
|
||||
return resolvedData
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import type {
|
||||
Data,
|
||||
FormState,
|
||||
Locale,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { reduceFieldsToValues } from 'payload/shared'
|
||||
|
||||
export const getDocumentData = async (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
locale: Locale
|
||||
req: PayloadRequest
|
||||
schemaPath?: string
|
||||
}): Promise<{
|
||||
data: Data
|
||||
formState: FormState
|
||||
}> => {
|
||||
const { id, collectionConfig, globalConfig, locale, req, schemaPath: schemaPathFromProps } = args
|
||||
|
||||
const schemaPath = schemaPathFromProps || collectionConfig?.slug || globalConfig?.slug
|
||||
|
||||
try {
|
||||
const { state: formState } = await buildFormState({
|
||||
req: {
|
||||
...req,
|
||||
data: {
|
||||
id,
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
globalSlug: globalConfig?.slug,
|
||||
locale: locale?.code,
|
||||
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
|
||||
schemaPath,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const data = reduceFieldsToValues(formState, true)
|
||||
|
||||
return {
|
||||
data,
|
||||
formState,
|
||||
}
|
||||
} catch (error) {
|
||||
req.payload.logger.error({ err: error, msg: 'Error getting document data' })
|
||||
return {
|
||||
data: null,
|
||||
formState: {
|
||||
fields: {
|
||||
initialValue: undefined,
|
||||
valid: false,
|
||||
value: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
86
packages/next/src/views/Document/getIsLocked.ts
Normal file
86
packages/next/src/views/Document/getIsLocked.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type {
|
||||
Payload,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
TypedUser,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
isEditing: boolean
|
||||
payload: Payload
|
||||
user: TypedUser
|
||||
}
|
||||
|
||||
type Result = Promise<{
|
||||
currentEditor?: TypedUser
|
||||
isLocked: boolean
|
||||
lastUpdateTime?: number
|
||||
}>
|
||||
|
||||
export const getIsLocked = async ({
|
||||
id,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
isEditing,
|
||||
payload,
|
||||
user,
|
||||
}: Args): Result => {
|
||||
const entityConfig = collectionConfig || globalConfig
|
||||
|
||||
const entityHasLockingEnabled =
|
||||
entityConfig?.lockDocuments !== undefined ? entityConfig?.lockDocuments : true
|
||||
|
||||
if (!entityHasLockingEnabled || !isEditing) {
|
||||
return {
|
||||
isLocked: false,
|
||||
}
|
||||
}
|
||||
|
||||
const where: Where = {}
|
||||
|
||||
if (globalConfig) {
|
||||
where.globalSlug = {
|
||||
equals: globalConfig.slug,
|
||||
}
|
||||
} else {
|
||||
where.and = [
|
||||
{
|
||||
'document.value': {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'document.relationTo': {
|
||||
equals: collectionConfig.slug,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const { docs } = await payload.find({
|
||||
collection: 'payload-locked-documents',
|
||||
depth: 1,
|
||||
where,
|
||||
})
|
||||
|
||||
if (docs.length > 0) {
|
||||
const newEditor = docs[0].user?.value
|
||||
const lastUpdateTime = new Date(docs[0].updatedAt).getTime()
|
||||
|
||||
if (newEditor?.id !== user.id) {
|
||||
return {
|
||||
currentEditor: newEditor,
|
||||
isLocked: true,
|
||||
lastUpdateTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLocked: false,
|
||||
}
|
||||
}
|
||||
240
packages/next/src/views/Document/getVersions.ts
Normal file
240
packages/next/src/views/Document/getVersions.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import type {
|
||||
DocumentPermissions,
|
||||
Payload,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
TypedUser,
|
||||
} from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
docPermissions: DocumentPermissions
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
locale?: string
|
||||
payload: Payload
|
||||
user: TypedUser
|
||||
}
|
||||
|
||||
type Result = Promise<{
|
||||
hasPublishedDoc: boolean
|
||||
mostRecentVersionIsAutosaved: boolean
|
||||
unpublishedVersionCount: number
|
||||
versionCount: number
|
||||
}>
|
||||
|
||||
// TODO: in the future, we can parallelize some of these queries
|
||||
// this will speed up the API by ~30-100ms or so
|
||||
export const getVersions = async ({
|
||||
id,
|
||||
collectionConfig,
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
locale,
|
||||
payload,
|
||||
user,
|
||||
}: Args): Result => {
|
||||
let publishedQuery
|
||||
let hasPublishedDoc = false
|
||||
let mostRecentVersionIsAutosaved = false
|
||||
let unpublishedVersionCount = 0
|
||||
let versionCount = 0
|
||||
|
||||
const entityConfig = collectionConfig || globalConfig
|
||||
const versionsConfig = entityConfig?.versions
|
||||
|
||||
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions?.permission)
|
||||
|
||||
if (!shouldFetchVersions) {
|
||||
const hasPublishedDoc = Boolean((collectionConfig && id) || globalConfig)
|
||||
|
||||
return {
|
||||
hasPublishedDoc,
|
||||
mostRecentVersionIsAutosaved,
|
||||
unpublishedVersionCount,
|
||||
versionCount,
|
||||
}
|
||||
}
|
||||
|
||||
if (collectionConfig) {
|
||||
if (!id) {
|
||||
return {
|
||||
hasPublishedDoc,
|
||||
mostRecentVersionIsAutosaved,
|
||||
unpublishedVersionCount,
|
||||
versionCount,
|
||||
}
|
||||
}
|
||||
|
||||
if (versionsConfig?.drafts) {
|
||||
publishedQuery = await payload.find({
|
||||
collection: collectionConfig.slug,
|
||||
depth: 0,
|
||||
locale: locale || undefined,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
or: [
|
||||
{
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
{
|
||||
_status: {
|
||||
exists: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (publishedQuery.docs?.[0]) {
|
||||
hasPublishedDoc = true
|
||||
}
|
||||
|
||||
if (versionsConfig.drafts?.autosave) {
|
||||
const mostRecentVersion = await payload.findVersions({
|
||||
collection: collectionConfig.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (
|
||||
mostRecentVersion.docs[0] &&
|
||||
'autosave' in mostRecentVersion.docs[0] &&
|
||||
mostRecentVersion.docs[0].autosave
|
||||
) {
|
||||
mostRecentVersionIsAutosaved = true
|
||||
}
|
||||
}
|
||||
|
||||
if (publishedQuery.docs?.[0]?.updatedAt) {
|
||||
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
|
||||
collection: collectionConfig.slug,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'version._status': {
|
||||
equals: 'draft',
|
||||
},
|
||||
},
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: publishedQuery.docs[0].updatedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
;({ totalDocs: versionCount } = await payload.countVersions({
|
||||
collection: collectionConfig.slug,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
if (versionsConfig?.drafts) {
|
||||
publishedQuery = await payload.findGlobal({
|
||||
slug: globalConfig.slug,
|
||||
depth: 0,
|
||||
locale,
|
||||
user,
|
||||
})
|
||||
|
||||
if (publishedQuery?._status === 'published') {
|
||||
hasPublishedDoc = true
|
||||
}
|
||||
|
||||
if (versionsConfig.drafts?.autosave) {
|
||||
const mostRecentVersion = await payload.findGlobalVersions({
|
||||
slug: globalConfig.slug,
|
||||
limit: 1,
|
||||
select: {
|
||||
autosave: true,
|
||||
},
|
||||
user,
|
||||
})
|
||||
|
||||
if (
|
||||
mostRecentVersion.docs[0] &&
|
||||
'autosave' in mostRecentVersion.docs[0] &&
|
||||
mostRecentVersion.docs[0].autosave
|
||||
) {
|
||||
mostRecentVersionIsAutosaved = true
|
||||
}
|
||||
}
|
||||
|
||||
if (publishedQuery?.updatedAt) {
|
||||
;({ totalDocs: unpublishedVersionCount } = await payload.countGlobalVersions({
|
||||
depth: 0,
|
||||
global: globalConfig.slug,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'version._status': {
|
||||
equals: 'draft',
|
||||
},
|
||||
},
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: publishedQuery.updatedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
;({ totalDocs: versionCount } = await payload.countGlobalVersions({
|
||||
depth: 0,
|
||||
global: globalConfig.slug,
|
||||
user,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
hasPublishedDoc,
|
||||
mostRecentVersionIsAutosaved,
|
||||
unpublishedVersionCount,
|
||||
versionCount,
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import type {
|
||||
} from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { notFound } from 'next/navigation.js'
|
||||
|
||||
import { APIView as DefaultAPIView } from '../API/index.js'
|
||||
import { EditView as DefaultEditView } from '../Edit/index.js'
|
||||
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
|
||||
@@ -23,7 +21,7 @@ import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
|
||||
export type ViewFromConfig<TProps extends object> = {
|
||||
Component?: React.FC<TProps>
|
||||
payloadComponent?: PayloadComponent<TProps>
|
||||
ComponentConfig?: PayloadComponent<TProps>
|
||||
}
|
||||
|
||||
export const getViewsFromConfig = ({
|
||||
@@ -81,7 +79,7 @@ export const getViewsFromConfig = ({
|
||||
routeSegments
|
||||
|
||||
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
} else {
|
||||
// `../:id`, or `../create`
|
||||
switch (routeSegments.length) {
|
||||
@@ -94,7 +92,7 @@ export const getViewsFromConfig = ({
|
||||
docPermissions?.create?.permission
|
||||
) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
@@ -132,11 +130,11 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
} else {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
|
||||
DefaultView = {
|
||||
@@ -156,7 +154,7 @@ export const getViewsFromConfig = ({
|
||||
case 'api': {
|
||||
if (collectionConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'api'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'api'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultAPIView,
|
||||
@@ -171,7 +169,7 @@ export const getViewsFromConfig = ({
|
||||
Component: DefaultLivePreviewView,
|
||||
}
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'livePreview'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'livePreview'),
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -180,7 +178,7 @@ export const getViewsFromConfig = ({
|
||||
case 'versions': {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'versions'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionsView,
|
||||
@@ -218,7 +216,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +231,7 @@ export const getViewsFromConfig = ({
|
||||
if (segment4 === 'versions') {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'version'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionView,
|
||||
@@ -269,7 +267,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,12 +282,12 @@ export const getViewsFromConfig = ({
|
||||
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
|
||||
|
||||
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
} else {
|
||||
switch (routeSegments.length) {
|
||||
case 2: {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
@@ -303,7 +301,7 @@ export const getViewsFromConfig = ({
|
||||
case 'api': {
|
||||
if (globalConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'api'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'api'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultAPIView,
|
||||
@@ -318,7 +316,7 @@ export const getViewsFromConfig = ({
|
||||
Component: DefaultLivePreviewView,
|
||||
}
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'livePreview'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'livePreview'),
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -327,7 +325,7 @@ export const getViewsFromConfig = ({
|
||||
case 'versions': {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'versions'),
|
||||
}
|
||||
|
||||
DefaultView = {
|
||||
@@ -362,7 +360,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
} else {
|
||||
DefaultView = {
|
||||
@@ -385,7 +383,7 @@ export const getViewsFromConfig = ({
|
||||
if (segment3 === 'versions') {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'version'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionView,
|
||||
@@ -416,7 +414,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
195
packages/next/src/views/Document/handleServerFunction.tsx
Normal file
195
packages/next/src/views/Document/handleServerFunction.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
ClientConfig,
|
||||
Data,
|
||||
DocumentPreferences,
|
||||
FormState,
|
||||
PayloadRequest,
|
||||
SanitizedConfig,
|
||||
VisibleEntities,
|
||||
} from 'payload'
|
||||
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { createClientConfig, 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
|
||||
}): ClientConfig => {
|
||||
const { config, i18n } = args
|
||||
|
||||
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
cachedClientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
type RenderDocumentResult = {
|
||||
data: any
|
||||
Document: React.ReactNode
|
||||
preferences: DocumentPreferences
|
||||
}
|
||||
|
||||
export const renderDocumentHandler = async (args: {
|
||||
collectionSlug: string
|
||||
disableActions?: boolean
|
||||
docID: string
|
||||
drawerSlug?: string
|
||||
initialData?: Data
|
||||
initialState?: FormState
|
||||
redirectAfterDelete: boolean
|
||||
redirectAfterDuplicate: boolean
|
||||
req: PayloadRequest
|
||||
}): Promise<RenderDocumentResult> => {
|
||||
const {
|
||||
collectionSlug,
|
||||
disableActions,
|
||||
docID,
|
||||
drawerSlug,
|
||||
initialData,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
req,
|
||||
req: {
|
||||
i18n,
|
||||
payload,
|
||||
payload: { config },
|
||||
user,
|
||||
},
|
||||
} = args
|
||||
|
||||
const headers = await getHeaders()
|
||||
|
||||
const cookies = parseCookies(headers)
|
||||
|
||||
const incomingUserSlug = user?.collection
|
||||
|
||||
const adminUserSlug = config.admin.user
|
||||
|
||||
// If we have a user slug, test it against the functions
|
||||
if (incomingUserSlug) {
|
||||
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
|
||||
|
||||
// Run the admin access function from the config if it exists
|
||||
if (adminAccessFunction) {
|
||||
const canAccessAdmin = await adminAccessFunction({ req })
|
||||
|
||||
if (!canAccessAdmin) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
// Match the user collection to the global admin config
|
||||
} else if (adminUserSlug !== incomingUserSlug) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
} else {
|
||||
const hasUsers = await payload.find({
|
||||
collection: adminUserSlug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
})
|
||||
|
||||
// If there are users, we should not allow access because of /create-first-user
|
||||
if (hasUsers.docs.length) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
}
|
||||
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
let preferences: DocumentPreferences
|
||||
|
||||
if (docID) {
|
||||
const preferencesKey = `${collectionSlug}-edit-${docID}`
|
||||
|
||||
preferences = await payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: preferencesKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.then((res) => res.docs[0]?.value as DocumentPreferences)
|
||||
}
|
||||
|
||||
const visibleEntities: VisibleEntities = {
|
||||
collections: payload.config.collections
|
||||
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
|
||||
.filter(Boolean),
|
||||
globals: payload.config.globals
|
||||
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
|
||||
.filter(Boolean),
|
||||
}
|
||||
|
||||
const permissions = await getAccessResults({
|
||||
req,
|
||||
})
|
||||
|
||||
const { data, Document } = await renderDocument({
|
||||
clientConfig,
|
||||
disableActions,
|
||||
drawerSlug,
|
||||
importMap: payload.importMap,
|
||||
initialData,
|
||||
initPageResult: {
|
||||
collectionConfig: payload.config.collections.find(
|
||||
(collection) => collection.slug === collectionSlug,
|
||||
),
|
||||
cookies,
|
||||
docID,
|
||||
globalConfig: payload.config.globals.find((global) => global.slug === collectionSlug),
|
||||
languageOptions: undefined, // TODO
|
||||
permissions,
|
||||
req,
|
||||
translations: undefined, // TODO
|
||||
visibleEntities,
|
||||
},
|
||||
params: {
|
||||
segments: ['collections', collectionSlug, docID],
|
||||
},
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
searchParams: {},
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
Document,
|
||||
preferences,
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,52 @@
|
||||
import type {
|
||||
AdminViewProps,
|
||||
EditViewComponent,
|
||||
MappedComponent,
|
||||
Data,
|
||||
PayloadComponent,
|
||||
ServerProps,
|
||||
ServerSideEditViewProps,
|
||||
} from 'payload'
|
||||
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import {
|
||||
formatAdminURL,
|
||||
getCreateMappedComponent,
|
||||
isEditing as getIsEditing,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL, isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect.js'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
|
||||
import type { ViewFromConfig } from './getViewsFromConfig.js'
|
||||
|
||||
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
||||
import { NotFoundView } from '../NotFound/index.js'
|
||||
import { getDocPreferences } from './getDocPreferences.js'
|
||||
import { getDocumentData } from './getDocumentData.js'
|
||||
import { getDocumentPermissions } from './getDocumentPermissions.js'
|
||||
import { getIsLocked } from './getIsLocked.js'
|
||||
import { getMetaBySegment } from './getMetaBySegment.js'
|
||||
import { getVersions } from './getVersions.js'
|
||||
import { getViewsFromConfig } from './getViewsFromConfig.js'
|
||||
import { renderDocumentSlots } from './renderDocumentSlots.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
|
||||
|
||||
export const Document: React.FC<AdminViewProps> = async ({
|
||||
// This function will be responsible for rendering an Edit Document view
|
||||
// it will be called on the server for Edit page views as well as
|
||||
// called on-demand from document drawers
|
||||
export const renderDocument = async ({
|
||||
disableActions,
|
||||
drawerSlug,
|
||||
importMap,
|
||||
initialData,
|
||||
initPageResult,
|
||||
params,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
searchParams,
|
||||
}) => {
|
||||
}: AdminViewProps): Promise<{
|
||||
data: Data
|
||||
Document: React.ReactNode
|
||||
}> => {
|
||||
const {
|
||||
collectionConfig,
|
||||
docID: id,
|
||||
@@ -57,54 +72,108 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
const segments = Array.isArray(params?.segments) ? params.segments : []
|
||||
const collectionSlug = collectionConfig?.slug || undefined
|
||||
const globalSlug = globalConfig?.slug || undefined
|
||||
|
||||
const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
|
||||
|
||||
let RootViewOverride: MappedComponent<ServerSideEditViewProps>
|
||||
let CustomView: MappedComponent<ServerSideEditViewProps>
|
||||
let DefaultView: MappedComponent<ServerSideEditViewProps>
|
||||
let ErrorView: MappedComponent<AdminViewProps>
|
||||
let RootViewOverride: PayloadComponent
|
||||
let CustomView: ViewFromConfig<ServerSideEditViewProps>
|
||||
let DefaultView: ViewFromConfig<ServerSideEditViewProps>
|
||||
let ErrorView: ViewFromConfig<AdminViewProps>
|
||||
|
||||
let apiURL: string
|
||||
|
||||
const { data, formState } = await getDocumentData({
|
||||
id,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
locale,
|
||||
req,
|
||||
})
|
||||
// Fetch the doc required for the view
|
||||
const doc =
|
||||
initialData ||
|
||||
(await getDocumentData({
|
||||
id,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
user,
|
||||
}))
|
||||
|
||||
if (!data) {
|
||||
notFound()
|
||||
if (isEditing && !doc) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
|
||||
id,
|
||||
collectionConfig,
|
||||
data,
|
||||
globalConfig,
|
||||
req,
|
||||
})
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
const [
|
||||
docPreferences,
|
||||
{ docPermissions, hasPublishPermission, hasSavePermission },
|
||||
{ currentEditor, isLocked, lastUpdateTime },
|
||||
] = await Promise.all([
|
||||
// Get document preferences
|
||||
getDocPreferences({
|
||||
id,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
}),
|
||||
|
||||
// Get permissions
|
||||
getDocumentPermissions({
|
||||
id,
|
||||
collectionConfig,
|
||||
data: doc,
|
||||
globalConfig,
|
||||
req,
|
||||
}),
|
||||
|
||||
// Fetch document lock state
|
||||
getIsLocked({
|
||||
id,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
isEditing,
|
||||
payload: req.payload,
|
||||
user,
|
||||
}),
|
||||
])
|
||||
|
||||
const [
|
||||
{ hasPublishedDoc, mostRecentVersionIsAutosaved, unpublishedVersionCount, versionCount },
|
||||
{ state: formState },
|
||||
] = await Promise.all([
|
||||
getVersions({
|
||||
id,
|
||||
collectionConfig,
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
locale: locale?.code,
|
||||
payload,
|
||||
user,
|
||||
}),
|
||||
buildFormState({
|
||||
id,
|
||||
collectionSlug,
|
||||
data: doc,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
globalSlug,
|
||||
locale: locale?.code,
|
||||
operation: (collectionSlug && id) || globalSlug ? 'update' : 'create',
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionSlug || globalSlug,
|
||||
}),
|
||||
])
|
||||
|
||||
const serverProps: ServerProps = {
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
}
|
||||
|
||||
if (collectionConfig) {
|
||||
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
@@ -122,12 +191,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
RootViewOverride =
|
||||
collectionConfig?.admin?.components?.views?.edit?.root &&
|
||||
'Component' in collectionConfig.admin.components.views.edit.root
|
||||
? createMappedComponent(
|
||||
collectionConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
|
||||
undefined,
|
||||
undefined,
|
||||
'collectionConfig?.admin?.components?.views?.edit?.root',
|
||||
)
|
||||
? collectionConfig?.admin?.components?.views?.edit?.root?.Component
|
||||
: null
|
||||
|
||||
if (!RootViewOverride) {
|
||||
@@ -138,36 +202,21 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
routeSegments: segments,
|
||||
})
|
||||
|
||||
CustomView = createMappedComponent(
|
||||
collectionViews?.CustomView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.CustomView?.Component,
|
||||
'collectionViews?.CustomView.payloadComponent',
|
||||
)
|
||||
|
||||
DefaultView = createMappedComponent(
|
||||
collectionViews?.DefaultView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.DefaultView?.Component,
|
||||
'collectionViews?.DefaultView.payloadComponent',
|
||||
)
|
||||
|
||||
ErrorView = createMappedComponent(
|
||||
collectionViews?.ErrorView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.ErrorView?.Component,
|
||||
'collectionViews?.ErrorView.payloadComponent',
|
||||
)
|
||||
CustomView = collectionViews?.CustomView
|
||||
DefaultView = collectionViews?.DefaultView
|
||||
ErrorView = collectionViews?.ErrorView
|
||||
}
|
||||
|
||||
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
|
||||
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
|
||||
ErrorView = {
|
||||
Component: NotFoundView,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
@@ -189,12 +238,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
RootViewOverride =
|
||||
globalConfig?.admin?.components?.views?.edit?.root &&
|
||||
'Component' in globalConfig.admin.components.views.edit.root
|
||||
? createMappedComponent(
|
||||
globalConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
|
||||
undefined,
|
||||
undefined,
|
||||
'globalConfig?.admin?.components?.views?.edit?.root',
|
||||
)
|
||||
? globalConfig?.admin?.components?.views?.edit?.root?.Component
|
||||
: null
|
||||
|
||||
if (!RootViewOverride) {
|
||||
@@ -205,29 +249,14 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
routeSegments: segments,
|
||||
})
|
||||
|
||||
CustomView = createMappedComponent(
|
||||
globalViews?.CustomView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.CustomView?.Component,
|
||||
'globalViews?.CustomView.payloadComponent',
|
||||
)
|
||||
|
||||
DefaultView = createMappedComponent(
|
||||
globalViews?.DefaultView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.DefaultView?.Component,
|
||||
'globalViews?.DefaultView.payloadComponent',
|
||||
)
|
||||
|
||||
ErrorView = createMappedComponent(
|
||||
globalViews?.ErrorView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.ErrorView?.Component,
|
||||
'globalViews?.ErrorView.payloadComponent',
|
||||
)
|
||||
CustomView = globalViews?.CustomView
|
||||
DefaultView = globalViews?.DefaultView
|
||||
ErrorView = globalViews?.ErrorView
|
||||
|
||||
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
|
||||
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
|
||||
ErrorView = {
|
||||
Component: NotFoundView,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,13 +269,14 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
hasSavePermission &&
|
||||
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
|
||||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
|
||||
|
||||
const validateDraftData =
|
||||
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
|
||||
|
||||
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
|
||||
const doc = await payload.create({
|
||||
collection: collectionSlug,
|
||||
data: {},
|
||||
data: initialData || {},
|
||||
depth: 0,
|
||||
draft: true,
|
||||
fallbackLocale: null,
|
||||
@@ -263,57 +293,96 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
})
|
||||
redirect(redirectURL)
|
||||
} else {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
disableActions={false}
|
||||
docPermissions={docPermissions}
|
||||
globalSlug={globalConfig?.slug}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
initialData={data}
|
||||
initialState={formState}
|
||||
isEditing={isEditing}
|
||||
key={locale?.code}
|
||||
>
|
||||
{!RootViewOverride && (
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
{/**
|
||||
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
|
||||
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
|
||||
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error when loading up some version views - for example a versions
|
||||
* view in the draft-posts collection of the versions test suite. RenderCustomComponent is what renders the versions view.
|
||||
*
|
||||
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
|
||||
*/}
|
||||
<EditDepthProvider
|
||||
depth={1}
|
||||
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
||||
const documentSlots = renderDocumentSlots({
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
hasSavePermission,
|
||||
permissions: docPermissions,
|
||||
req,
|
||||
})
|
||||
|
||||
const clientProps = { formState, ...documentSlots }
|
||||
|
||||
return {
|
||||
data: doc,
|
||||
Document: (
|
||||
<DocumentInfoProvider
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
currentEditor={currentEditor}
|
||||
disableActions={disableActions ?? false}
|
||||
docPermissions={docPermissions}
|
||||
globalSlug={globalConfig?.slug}
|
||||
hasPublishedDoc={hasPublishedDoc}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
initialData={doc}
|
||||
initialState={formState}
|
||||
isEditing={isEditing}
|
||||
isLocked={isLocked}
|
||||
key={locale?.code}
|
||||
lastUpdateTime={lastUpdateTime}
|
||||
mostRecentVersionIsAutosaved={mostRecentVersionIsAutosaved}
|
||||
redirectAfterDelete={redirectAfterDelete}
|
||||
redirectAfterDuplicate={redirectAfterDuplicate}
|
||||
unpublishedVersionCount={unpublishedVersionCount}
|
||||
versionCount={versionCount}
|
||||
>
|
||||
{ErrorView ? (
|
||||
<RenderComponent mappedComponent={ErrorView} />
|
||||
) : (
|
||||
<RenderComponent
|
||||
mappedComponent={
|
||||
RootViewOverride ? RootViewOverride : CustomView ? CustomView : DefaultView
|
||||
}
|
||||
{!RootViewOverride && !drawerSlug && (
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
)
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<EditDepthProvider>
|
||||
{ErrorView ? (
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={ErrorView.ComponentConfig || ErrorView.Component}
|
||||
importMap={importMap}
|
||||
serverProps={serverProps}
|
||||
/>
|
||||
) : (
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={
|
||||
RootViewOverride
|
||||
? RootViewOverride
|
||||
: CustomView?.ComponentConfig || CustomView?.Component
|
||||
? CustomView?.ComponentConfig || CustomView?.Component
|
||||
: DefaultView?.ComponentConfig || DefaultView?.Component
|
||||
}
|
||||
importMap={importMap}
|
||||
serverProps={serverProps}
|
||||
/>
|
||||
)}
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export const Document: React.FC<AdminViewProps> = async (args) => {
|
||||
try {
|
||||
const { Document: RenderedDocument } = await renderDocument(args)
|
||||
return RenderedDocument
|
||||
} catch (error) {
|
||||
if (isRedirectError(error)) {
|
||||
throw error
|
||||
}
|
||||
args.initPageResult.req.payload.logger.error(error)
|
||||
|
||||
if (error.message === 'not-found') {
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
136
packages/next/src/views/Document/renderDocumentSlots.tsx
Normal file
136
packages/next/src/views/Document/renderDocumentSlots.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import type {
|
||||
DefaultServerFunctionArgs,
|
||||
DocumentPermissions,
|
||||
DocumentSlots,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
StaticDescription,
|
||||
} from 'payload'
|
||||
|
||||
import { ViewDescription } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import React from 'react'
|
||||
|
||||
import { getDocumentPermissions } from './getDocumentPermissions.js'
|
||||
|
||||
export const renderDocumentSlots: (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
hasSavePermission: boolean
|
||||
permissions: DocumentPermissions
|
||||
req: PayloadRequest
|
||||
}) => DocumentSlots = (args) => {
|
||||
const { collectionConfig, globalConfig, hasSavePermission, req } = args
|
||||
|
||||
const components: DocumentSlots = {} as DocumentSlots
|
||||
|
||||
const unsavedDraftWithValidations = undefined
|
||||
|
||||
const isPreviewEnabled = collectionConfig?.admin?.preview || globalConfig?.admin?.preview
|
||||
|
||||
const CustomPreviewButton =
|
||||
collectionConfig?.admin?.components?.edit?.PreviewButton ||
|
||||
globalConfig?.admin?.components?.elements?.PreviewButton
|
||||
|
||||
if (isPreviewEnabled && CustomPreviewButton) {
|
||||
components.PreviewButton = (
|
||||
<RenderServerComponent Component={CustomPreviewButton} importMap={req.payload.importMap} />
|
||||
)
|
||||
}
|
||||
|
||||
const descriptionFromConfig =
|
||||
collectionConfig?.admin?.description || globalConfig?.admin?.description
|
||||
|
||||
const staticDescription: StaticDescription =
|
||||
typeof descriptionFromConfig === 'function'
|
||||
? descriptionFromConfig({ t: req.i18n.t })
|
||||
: descriptionFromConfig
|
||||
|
||||
const CustomDescription =
|
||||
collectionConfig?.admin?.components?.Description ||
|
||||
globalConfig?.admin?.components?.elements?.Description
|
||||
|
||||
const hasDescription = CustomDescription || staticDescription
|
||||
|
||||
if (hasDescription) {
|
||||
components.Description = (
|
||||
<RenderServerComponent
|
||||
clientProps={{ description: staticDescription }}
|
||||
Component={CustomDescription}
|
||||
Fallback={ViewDescription}
|
||||
importMap={req.payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (hasSavePermission) {
|
||||
if (collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts) {
|
||||
const CustomPublishButton =
|
||||
collectionConfig?.admin?.components?.edit?.PublishButton ||
|
||||
globalConfig?.admin?.components?.elements?.PublishButton
|
||||
|
||||
if (CustomPublishButton) {
|
||||
components.PublishButton = (
|
||||
<RenderServerComponent
|
||||
Component={CustomPublishButton}
|
||||
importMap={req.payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const CustomSaveDraftButton =
|
||||
collectionConfig?.admin?.components?.edit?.SaveDraftButton ||
|
||||
globalConfig?.admin?.components?.elements?.SaveDraftButton
|
||||
|
||||
const draftsEnabled =
|
||||
(collectionConfig?.versions?.drafts && !collectionConfig?.versions?.drafts?.autosave) ||
|
||||
(globalConfig?.versions?.drafts && !globalConfig?.versions?.drafts?.autosave)
|
||||
|
||||
if ((draftsEnabled || unsavedDraftWithValidations) && CustomSaveDraftButton) {
|
||||
components.SaveDraftButton = (
|
||||
<RenderServerComponent
|
||||
Component={CustomSaveDraftButton}
|
||||
importMap={req.payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const CustomSaveButton =
|
||||
collectionConfig?.admin?.components?.edit?.SaveButton ||
|
||||
globalConfig?.admin?.components?.elements?.SaveButton
|
||||
|
||||
if (CustomSaveButton) {
|
||||
components.SaveButton = (
|
||||
<RenderServerComponent Component={CustomSaveButton} importMap={req.payload.importMap} />
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
export const renderDocumentSlotsHandler = async (
|
||||
args: { collectionSlug: string } & DefaultServerFunctionArgs,
|
||||
) => {
|
||||
const { collectionSlug, req } = args
|
||||
|
||||
const collectionConfig = req.payload.collections[collectionSlug]?.config
|
||||
|
||||
if (!collectionConfig) {
|
||||
throw new Error(req.t('error:incorrectCollection'))
|
||||
}
|
||||
|
||||
const { docPermissions, hasSavePermission } = await getDocumentPermissions({
|
||||
collectionConfig,
|
||||
data: {},
|
||||
req,
|
||||
})
|
||||
|
||||
return renderDocumentSlots({
|
||||
collectionConfig,
|
||||
hasSavePermission,
|
||||
permissions: docPermissions,
|
||||
req,
|
||||
})
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
|
||||
|
||||
import { RenderComponent, SetViewActions, useConfig, useDocumentInfo } from '@payloadcms/ui'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
export const EditViewClient: React.FC = () => {
|
||||
const { collectionSlug, globalSlug } = useDocumentInfo()
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
|
||||
const Edit = (collectionConfig || globalConfig)?.admin?.components?.views?.edit?.default
|
||||
?.Component
|
||||
|
||||
if (!Edit) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.default?.actions
|
||||
}
|
||||
/>
|
||||
<RenderComponent mappedComponent={Edit} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { EditViewComponent, PayloadServerReactComponent } from 'payload'
|
||||
'use client'
|
||||
|
||||
import type { ClientSideEditViewProps } from 'payload'
|
||||
|
||||
import { DefaultEditView } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { EditViewClient } from './index.client.js'
|
||||
|
||||
export const EditView: PayloadServerReactComponent<EditViewComponent> = () => {
|
||||
return <EditViewClient />
|
||||
export const EditView: React.FC<ClientSideEditViewProps> = (props) => {
|
||||
return <DefaultEditView {...props} />
|
||||
}
|
||||
|
||||
@@ -105,9 +105,11 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<EmailField
|
||||
autoComplete="email"
|
||||
field={{
|
||||
name: 'email',
|
||||
admin: {
|
||||
autoComplete: 'email',
|
||||
},
|
||||
label: t('general:email'),
|
||||
required: true,
|
||||
}}
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
Button,
|
||||
DeleteMany,
|
||||
EditMany,
|
||||
Gutter,
|
||||
ListControls,
|
||||
ListHeader,
|
||||
ListSelection,
|
||||
Pagination,
|
||||
PerPage,
|
||||
PublishMany,
|
||||
RelationshipProvider,
|
||||
RenderComponent,
|
||||
SelectionProvider,
|
||||
SetViewActions,
|
||||
StaggeredShimmers,
|
||||
Table,
|
||||
UnpublishMany,
|
||||
useAuth,
|
||||
useBulkUpload,
|
||||
useConfig,
|
||||
useEditDepth,
|
||||
useListInfo,
|
||||
useListQuery,
|
||||
useModal,
|
||||
useStepNav,
|
||||
useTranslation,
|
||||
useWindowInfo,
|
||||
ViewDescription,
|
||||
} from '@payloadcms/ui'
|
||||
import LinkImport from 'next/link.js'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import { formatFilesize, isNumber } from 'payload/shared'
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'collection-list'
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
|
||||
export const DefaultListView: React.FC = () => {
|
||||
const { user } = useAuth()
|
||||
const {
|
||||
beforeActions,
|
||||
collectionSlug,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
hasCreatePermission,
|
||||
Header,
|
||||
newDocumentURL,
|
||||
} = useListInfo()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const { data, defaultLimit, handlePageChange, handlePerPageChange, params } = useListQuery()
|
||||
const { openModal } = useModal()
|
||||
const { setCollectionSlug, setOnSuccess } = useBulkUpload()
|
||||
const { drawerSlug } = useBulkUpload()
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
|
||||
const {
|
||||
admin: {
|
||||
components: {
|
||||
afterList,
|
||||
afterListTable,
|
||||
beforeList,
|
||||
beforeListTable,
|
||||
Description,
|
||||
views: {
|
||||
list: { actions },
|
||||
},
|
||||
},
|
||||
description,
|
||||
},
|
||||
fields,
|
||||
labels,
|
||||
} = collectionConfig
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const drawerDepth = useEditDepth()
|
||||
|
||||
const { setStepNav } = useStepNav()
|
||||
|
||||
const {
|
||||
breakpoints: { s: smallBreak },
|
||||
} = useWindowInfo()
|
||||
|
||||
let docs = data.docs || []
|
||||
|
||||
const isUploadCollection = Boolean(collectionConfig.upload)
|
||||
|
||||
if (isUploadCollection) {
|
||||
docs = docs?.map((doc) => {
|
||||
return {
|
||||
...doc,
|
||||
filesize: formatFilesize(doc.filesize),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openBulkUpload = React.useCallback(() => {
|
||||
setCollectionSlug(collectionSlug)
|
||||
openModal(drawerSlug)
|
||||
setOnSuccess(() => router.refresh())
|
||||
}, [router, collectionSlug, drawerSlug, openModal, setCollectionSlug, setOnSuccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (drawerDepth <= 1) {
|
||||
setStepNav([
|
||||
{
|
||||
label: labels?.plural,
|
||||
},
|
||||
])
|
||||
}
|
||||
}, [setStepNav, labels, drawerDepth])
|
||||
|
||||
const isBulkUploadEnabled = isUploadCollection && collectionConfig.upload.bulkUpload
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} ${baseClass}--${collectionSlug}`}>
|
||||
<SetViewActions actions={actions} />
|
||||
<SelectionProvider docs={data.docs} totalDocs={data.totalDocs} user={user}>
|
||||
<RenderComponent mappedComponent={beforeList} />
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
{Header || (
|
||||
<ListHeader heading={getTranslation(labels?.plural, i18n)}>
|
||||
{hasCreatePermission && (
|
||||
<>
|
||||
<Button
|
||||
aria-label={i18n.t('general:createNewLabel', {
|
||||
label: getTranslation(labels?.singular, i18n),
|
||||
})}
|
||||
buttonStyle="pill"
|
||||
el={'link'}
|
||||
Link={Link}
|
||||
size="small"
|
||||
to={newDocumentURL}
|
||||
>
|
||||
{i18n.t('general:createNew')}
|
||||
</Button>
|
||||
|
||||
{isBulkUploadEnabled && (
|
||||
<Button
|
||||
aria-label={t('upload:bulkUpload')}
|
||||
buttonStyle="pill"
|
||||
onClick={openBulkUpload}
|
||||
size="small"
|
||||
>
|
||||
{t('upload:bulkUpload')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!smallBreak && (
|
||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||
)}
|
||||
{(description || Description) && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription Description={Description} description={description} />
|
||||
</div>
|
||||
)}
|
||||
</ListHeader>
|
||||
)}
|
||||
<ListControls collectionConfig={collectionConfig} fields={fields} />
|
||||
<RenderComponent mappedComponent={beforeListTable} />
|
||||
{!data.docs && (
|
||||
<StaggeredShimmers
|
||||
className={[`${baseClass}__shimmer`, `${baseClass}__shimmer--rows`].join(' ')}
|
||||
count={6}
|
||||
/>
|
||||
)}
|
||||
{data.docs && data.docs.length > 0 && (
|
||||
<RelationshipProvider>
|
||||
<Table
|
||||
customCellContext={{
|
||||
collectionSlug,
|
||||
uploadConfig: collectionConfig.upload,
|
||||
}}
|
||||
data={docs}
|
||||
fields={fields}
|
||||
/>
|
||||
</RelationshipProvider>
|
||||
)}
|
||||
{data.docs && data.docs.length === 0 && (
|
||||
<div className={`${baseClass}__no-results`}>
|
||||
<p>{i18n.t('general:noResults', { label: getTranslation(labels?.plural, i18n) })}</p>
|
||||
{hasCreatePermission && newDocumentURL && (
|
||||
<Button el="link" Link={Link} to={newDocumentURL}>
|
||||
{i18n.t('general:createNewLabel', {
|
||||
label: getTranslation(labels?.singular, i18n),
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<RenderComponent mappedComponent={afterListTable} />
|
||||
{data.docs && data.docs.length > 0 && (
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Pagination
|
||||
hasNextPage={data.hasNextPage}
|
||||
hasPrevPage={data.hasPrevPage}
|
||||
limit={data.limit}
|
||||
nextPage={data.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
onChange={(page) => void handlePageChange(page)}
|
||||
page={data.page}
|
||||
prevPage={data.prevPage}
|
||||
totalPages={data.totalPages}
|
||||
/>
|
||||
{data?.totalDocs > 0 && (
|
||||
<Fragment>
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{data.page * data.limit - (data.limit - 1)}-
|
||||
{data.totalPages > 1 && data.totalPages !== data.page
|
||||
? data.limit * data.page
|
||||
: data.totalDocs}{' '}
|
||||
{i18n.t('general:of')} {data.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
handleChange={(limit) => void handlePerPageChange(limit)}
|
||||
limit={isNumber(params?.limit) ? Number(params.limit) : defaultLimit}
|
||||
limits={collectionConfig?.admin?.pagination?.limits}
|
||||
resetPage={data.totalDocs <= data.pagingCounter}
|
||||
/>
|
||||
{smallBreak && (
|
||||
<div className={`${baseClass}__list-selection`}>
|
||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||
<div className={`${baseClass}__list-selection-actions`}>
|
||||
{beforeActions && beforeActions}
|
||||
{!disableBulkEdit && (
|
||||
<Fragment>
|
||||
<EditMany collection={collectionConfig} fields={fields} />
|
||||
<PublishMany collection={collectionConfig} />
|
||||
<UnpublishMany collection={collectionConfig} />
|
||||
</Fragment>
|
||||
)}
|
||||
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Gutter>
|
||||
<RenderComponent mappedComponent={afterList} />
|
||||
</SelectionProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
194
packages/next/src/views/List/handleServerFunction.tsx
Normal file
194
packages/next/src/views/List/handleServerFunction.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ListPreferences } from '@payloadcms/ui'
|
||||
import type {
|
||||
ClientConfig,
|
||||
ListQuery,
|
||||
PayloadRequest,
|
||||
SanitizedConfig,
|
||||
VisibleEntities,
|
||||
} from 'payload'
|
||||
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { createClientConfig, 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
|
||||
}): ClientConfig => {
|
||||
const { config, i18n } = args
|
||||
|
||||
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
cachedClientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
type RenderListResult = {
|
||||
List: React.ReactNode
|
||||
preferences: ListPreferences
|
||||
}
|
||||
|
||||
export const renderListHandler = async (args: {
|
||||
collectionSlug: string
|
||||
disableActions?: boolean
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
documentDrawerSlug: string
|
||||
drawerSlug?: string
|
||||
enableRowSelections: boolean
|
||||
query: ListQuery
|
||||
redirectAfterDelete: boolean
|
||||
redirectAfterDuplicate: boolean
|
||||
req: PayloadRequest
|
||||
}): Promise<RenderListResult> => {
|
||||
const {
|
||||
collectionSlug,
|
||||
disableActions,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
query,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
req,
|
||||
req: {
|
||||
i18n,
|
||||
payload,
|
||||
payload: { config },
|
||||
user,
|
||||
},
|
||||
} = args
|
||||
|
||||
const headers = await getHeaders()
|
||||
|
||||
const cookies = parseCookies(headers)
|
||||
|
||||
const incomingUserSlug = user?.collection
|
||||
|
||||
const adminUserSlug = config.admin.user
|
||||
|
||||
// If we have a user slug, test it against the functions
|
||||
if (incomingUserSlug) {
|
||||
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
|
||||
|
||||
// Run the admin access function from the config if it exists
|
||||
if (adminAccessFunction) {
|
||||
const canAccessAdmin = await adminAccessFunction({ req })
|
||||
|
||||
if (!canAccessAdmin) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
// Match the user collection to the global admin config
|
||||
} else if (adminUserSlug !== incomingUserSlug) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
} else {
|
||||
const hasUsers = await payload.find({
|
||||
collection: adminUserSlug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
})
|
||||
|
||||
// If there are users, we should not allow access because of /create-first-user
|
||||
if (hasUsers.docs.length) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
}
|
||||
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
const preferencesKey = `${collectionSlug}-list`
|
||||
|
||||
const preferences = await payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: preferencesKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.then((res) => res.docs[0]?.value as ListPreferences)
|
||||
|
||||
const visibleEntities: VisibleEntities = {
|
||||
collections: payload.config.collections
|
||||
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
|
||||
.filter(Boolean),
|
||||
globals: payload.config.globals
|
||||
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
|
||||
.filter(Boolean),
|
||||
}
|
||||
|
||||
const permissions = await getAccessResults({
|
||||
req,
|
||||
})
|
||||
|
||||
const { List } = await renderListView({
|
||||
clientConfig,
|
||||
disableActions,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
importMap: payload.importMap,
|
||||
initPageResult: {
|
||||
collectionConfig: payload.config.collections.find(
|
||||
(collection) => collection.slug === collectionSlug,
|
||||
),
|
||||
cookies,
|
||||
globalConfig: payload.config.globals.find((global) => global.slug === collectionSlug),
|
||||
languageOptions: undefined, // TODO
|
||||
permissions,
|
||||
req,
|
||||
translations: undefined, // TODO
|
||||
visibleEntities,
|
||||
},
|
||||
params: {
|
||||
segments: ['collections', collectionSlug],
|
||||
},
|
||||
query,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
searchParams: {},
|
||||
})
|
||||
|
||||
return {
|
||||
List,
|
||||
preferences,
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,52 @@
|
||||
import type { AdminViewProps, ClientCollectionConfig, Where } from 'payload'
|
||||
import type { ListPreferences, ListViewClientProps } from '@payloadcms/ui'
|
||||
import type { AdminViewProps, ListQuery, Where } from 'payload'
|
||||
|
||||
import {
|
||||
HydrateAuthProvider,
|
||||
ListInfoProvider,
|
||||
ListQueryProvider,
|
||||
TableColumnsProvider,
|
||||
} from '@payloadcms/ui'
|
||||
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { createClientCollectionConfig } from '@payloadcms/ui/utilities/createClientConfig'
|
||||
import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { renderFilters, renderTable } from '@payloadcms/ui/rsc'
|
||||
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import { deepCopyObjectSimple, mergeListSearchAndWhere } from 'payload'
|
||||
import { isNumber } from 'payload/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { ListPreferences } from './Default/types.js'
|
||||
|
||||
import { DefaultEditView } from '../Edit/Default/index.js'
|
||||
import { DefaultListView } from './Default/index.js'
|
||||
import { renderListViewSlots } from './renderListViewSlots.js'
|
||||
|
||||
export { generateListMetadata } from './meta.js'
|
||||
|
||||
export const ListView: React.FC<AdminViewProps> = async ({
|
||||
initPageResult,
|
||||
params,
|
||||
searchParams,
|
||||
}) => {
|
||||
type ListViewArgs = {
|
||||
customCellProps?: Record<string, any>
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
enableRowSelections: boolean
|
||||
query: ListQuery
|
||||
} & AdminViewProps
|
||||
|
||||
export const renderListView = async (
|
||||
args: ListViewArgs,
|
||||
): Promise<{
|
||||
List: React.ReactNode
|
||||
}> => {
|
||||
const {
|
||||
clientConfig,
|
||||
customCellProps,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
initPageResult,
|
||||
params,
|
||||
query: queryFromArgs,
|
||||
searchParams,
|
||||
} = args
|
||||
|
||||
const {
|
||||
collectionConfig,
|
||||
collectionConfig: {
|
||||
slug: collectionSlug,
|
||||
admin: { useAsTitle },
|
||||
defaultSort,
|
||||
fields,
|
||||
},
|
||||
locale: fullLocale,
|
||||
permissions,
|
||||
req,
|
||||
@@ -35,18 +55,18 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
locale,
|
||||
payload,
|
||||
payload: { config },
|
||||
query,
|
||||
query: queryFromReq,
|
||||
user,
|
||||
},
|
||||
visibleEntities,
|
||||
} = initPageResult
|
||||
|
||||
const collectionSlug = collectionConfig?.slug
|
||||
|
||||
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const query = queryFromArgs || queryFromReq
|
||||
|
||||
let listPreferences: ListPreferences
|
||||
const preferenceKey = `${collectionSlug}-list`
|
||||
|
||||
@@ -79,7 +99,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
},
|
||||
})
|
||||
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
} catch (_err) {} // eslint-disable-line no-empty
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
@@ -87,20 +107,21 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
|
||||
if (collectionConfig) {
|
||||
if (!visibleEntities.collections.includes(collectionSlug)) {
|
||||
return notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const page = isNumber(query?.page) ? Number(query.page) : 0
|
||||
|
||||
const whereQuery = mergeListSearchAndWhere({
|
||||
collectionConfig,
|
||||
query: {
|
||||
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||
where: (query?.where as Where) || undefined,
|
||||
},
|
||||
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||
where: (query?.where as Where) || undefined,
|
||||
})
|
||||
|
||||
const limit = isNumber(query?.limit)
|
||||
? Number(query.limit)
|
||||
: listPreferences?.limit || collectionConfig.admin.pagination.defaultLimit
|
||||
|
||||
const sort =
|
||||
query?.sort && typeof query.sort === 'string'
|
||||
? query.sort
|
||||
@@ -125,89 +146,104 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
where: whereQuery || {},
|
||||
})
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
data,
|
||||
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
|
||||
i18n,
|
||||
limit,
|
||||
listPreferences,
|
||||
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
||||
locale: fullLocale,
|
||||
newDocumentURL: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
}),
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)
|
||||
|
||||
const { columnState, Table } = renderTable({
|
||||
collectionConfig: clientCollectionConfig,
|
||||
columnPreferences: listPreferences?.columns,
|
||||
customCellProps,
|
||||
docs: data.docs,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
fields,
|
||||
i18n: req.i18n,
|
||||
payload,
|
||||
useAsTitle,
|
||||
})
|
||||
|
||||
const ListComponent = createMappedComponent(
|
||||
collectionConfig?.admin?.components?.views?.list?.Component,
|
||||
undefined,
|
||||
DefaultListView,
|
||||
'collectionConfig?.admin?.components?.views?.list?.Component',
|
||||
)
|
||||
const renderedFilters = renderFilters(fields, req.payload.importMap)
|
||||
|
||||
let clientCollectionConfig = deepCopyObjectSimple(
|
||||
const staticDescription =
|
||||
typeof collectionConfig.admin.description === 'function'
|
||||
? collectionConfig.admin.description({ t: i18n.t })
|
||||
: collectionConfig.admin.description
|
||||
|
||||
const listViewSlots = renderListViewSlots({
|
||||
collectionConfig,
|
||||
) as unknown as ClientCollectionConfig
|
||||
clientCollectionConfig = createClientCollectionConfig({
|
||||
clientCollection: clientCollectionConfig,
|
||||
collection: collectionConfig,
|
||||
createMappedComponent,
|
||||
DefaultEditView,
|
||||
DefaultListView,
|
||||
i18n,
|
||||
importMap: payload.importMap,
|
||||
description: staticDescription,
|
||||
payload,
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<ListInfoProvider
|
||||
collectionConfig={clientCollectionConfig}
|
||||
collectionSlug={collectionSlug}
|
||||
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
|
||||
newDocumentURL={formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
})}
|
||||
>
|
||||
const clientProps: ListViewClientProps = {
|
||||
...listViewSlots,
|
||||
collectionSlug,
|
||||
columnState,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
|
||||
listPreferences,
|
||||
newDocumentURL: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
}),
|
||||
renderedFilters,
|
||||
Table,
|
||||
}
|
||||
|
||||
const isInDrawer = Boolean(drawerSlug)
|
||||
|
||||
return {
|
||||
List: (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<ListQueryProvider
|
||||
collectionSlug={collectionSlug}
|
||||
data={data}
|
||||
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
|
||||
defaultLimit={limit}
|
||||
defaultSort={sort}
|
||||
modifySearchParams
|
||||
modifySearchParams={!isInDrawer}
|
||||
preferenceKey={preferenceKey}
|
||||
>
|
||||
<TableColumnsProvider
|
||||
collectionSlug={collectionSlug}
|
||||
enableRowSelections
|
||||
listPreferences={listPreferences}
|
||||
preferenceKey={preferenceKey}
|
||||
>
|
||||
<RenderComponent
|
||||
clientProps={{
|
||||
collectionSlug,
|
||||
listSearchableFields: collectionConfig?.admin?.listSearchableFields,
|
||||
}}
|
||||
mappedComponent={ListComponent}
|
||||
/>
|
||||
</TableColumnsProvider>
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={collectionConfig?.admin?.components?.views?.list?.Component}
|
||||
Fallback={DefaultListView}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
data,
|
||||
i18n,
|
||||
limit,
|
||||
listPreferences,
|
||||
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
||||
locale: fullLocale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
</ListQueryProvider>
|
||||
</ListInfoProvider>
|
||||
</Fragment>
|
||||
)
|
||||
</Fragment>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
export const ListView: React.FC<ListViewArgs> = async (args) => {
|
||||
try {
|
||||
const { List: RenderedList } = await renderListView({ ...args, enableRowSelections: true })
|
||||
return RenderedList
|
||||
} catch (error) {
|
||||
if (error.message === 'not-found') {
|
||||
notFound()
|
||||
} else {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
packages/next/src/views/List/renderListViewSlots.tsx
Normal file
66
packages/next/src/views/List/renderListViewSlots.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { ListViewSlots } from '@payloadcms/ui'
|
||||
import type { Payload, SanitizedCollectionConfig, StaticDescription } from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
|
||||
export const renderListViewSlots = ({
|
||||
collectionConfig,
|
||||
description,
|
||||
payload,
|
||||
}: {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
description?: StaticDescription
|
||||
payload: Payload
|
||||
}): ListViewSlots => {
|
||||
const result: ListViewSlots = {} as ListViewSlots
|
||||
|
||||
if (collectionConfig.admin.components?.afterList) {
|
||||
result.AfterList = (
|
||||
<RenderServerComponent
|
||||
Component={collectionConfig.admin.components.afterList}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.afterListTable) {
|
||||
result.AfterListTable = (
|
||||
<RenderServerComponent
|
||||
Component={collectionConfig.admin.components.afterListTable}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeList) {
|
||||
result.BeforeList = (
|
||||
<RenderServerComponent
|
||||
Component={collectionConfig.admin.components.beforeList}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeListTable) {
|
||||
result.BeforeListTable = (
|
||||
<RenderServerComponent
|
||||
Component={collectionConfig.admin.components.beforeListTable}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.Description) {
|
||||
result.Description = (
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
description,
|
||||
}}
|
||||
Component={collectionConfig.admin.components.Description}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -13,29 +13,25 @@ import type {
|
||||
import {
|
||||
DocumentControls,
|
||||
DocumentFields,
|
||||
DocumentLocked,
|
||||
DocumentTakeOver,
|
||||
Form,
|
||||
LeaveWithoutSaving,
|
||||
OperationProvider,
|
||||
SetViewActions,
|
||||
SetDocumentStepNav,
|
||||
SetDocumentTitle,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useDocumentDrawerContext,
|
||||
useDocumentEvents,
|
||||
useDocumentInfo,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import {
|
||||
getFormState,
|
||||
handleBackToDashboard,
|
||||
handleGoBack,
|
||||
handleTakeOver,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { handleBackToDashboard, handleGoBack, handleTakeOver } from '@payloadcms/ui/shared'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { DocumentLocked } from '../../elements/DocumentLocked/index.js'
|
||||
import { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
|
||||
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle/index.js'
|
||||
import { useLivePreviewContext } from './Context/context.js'
|
||||
import { LivePreviewProvider } from './Context/index.js'
|
||||
import './index.scss'
|
||||
@@ -55,13 +51,11 @@ type Props = {
|
||||
}
|
||||
|
||||
const PreviewView: React.FC<Props> = ({
|
||||
apiRoute,
|
||||
collectionConfig,
|
||||
config,
|
||||
fields,
|
||||
globalConfig,
|
||||
schemaPath,
|
||||
serverURL,
|
||||
}) => {
|
||||
const {
|
||||
id,
|
||||
@@ -69,7 +63,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
AfterDocument,
|
||||
AfterFields,
|
||||
apiURL,
|
||||
BeforeDocument,
|
||||
BeforeFields,
|
||||
collectionSlug,
|
||||
currentEditor,
|
||||
@@ -86,13 +79,16 @@ const PreviewView: React.FC<Props> = ({
|
||||
isEditing,
|
||||
isInitializing,
|
||||
lastUpdateTime,
|
||||
onSave: onSaveFromProps,
|
||||
setCurrentEditor,
|
||||
setDocumentIsLocked,
|
||||
unlockDocument,
|
||||
updateDocumentEditor,
|
||||
} = useDocumentInfo()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const { onSave: onSaveFromProps } = useDocumentDrawerContext()
|
||||
|
||||
const operation = id ? 'update' : 'create'
|
||||
|
||||
const {
|
||||
@@ -120,6 +116,8 @@ const PreviewView: React.FC<Props> = ({
|
||||
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
|
||||
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
||||
|
||||
const lockExpiryTime = lastUpdateTime + lockDurationInMilliseconds
|
||||
@@ -178,6 +176,17 @@ const PreviewView: React.FC<Props> = ({
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
|
||||
const currentTime = Date.now()
|
||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||
|
||||
@@ -190,19 +199,17 @@ const PreviewView: React.FC<Props> = ({
|
||||
const docPreferences = await getDocPreferences()
|
||||
|
||||
const { lockedState, state } = await getFormState({
|
||||
apiRoute,
|
||||
body: {
|
||||
id,
|
||||
collectionSlug,
|
||||
docPreferences,
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation,
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath,
|
||||
updateLastEdited,
|
||||
},
|
||||
serverURL,
|
||||
id,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation,
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath,
|
||||
signal: abortController.signal,
|
||||
updateLastEdited,
|
||||
})
|
||||
|
||||
setDocumentIsLocked(true)
|
||||
@@ -214,8 +221,13 @@ const PreviewView: React.FC<Props> = ({
|
||||
: documentLockStateRef.current?.user
|
||||
|
||||
if (lockedState) {
|
||||
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
|
||||
if (previousOwnerId === user.id && lockedState.user.id !== user.id) {
|
||||
const lockedUserID =
|
||||
typeof lockedState.user === 'string' || typeof lockedState.user === 'number'
|
||||
? lockedState.user
|
||||
: lockedState.user.id
|
||||
|
||||
if (!documentLockStateRef.current || lockedUserID !== previousOwnerId) {
|
||||
if (previousOwnerId === user.id && lockedUserID !== user.id) {
|
||||
setShowTakeOverModal(true)
|
||||
documentLockStateRef.current.hasShownLockedModal = true
|
||||
}
|
||||
@@ -223,9 +235,10 @@ const PreviewView: React.FC<Props> = ({
|
||||
documentLockStateRef.current = documentLockStateRef.current = {
|
||||
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
|
||||
isLocked: true,
|
||||
user: lockedState.user,
|
||||
user: lockedState.user as ClientUser,
|
||||
}
|
||||
setCurrentEditor(lockedState.user)
|
||||
|
||||
setCurrentEditor(lockedState.user as ClientUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,25 +246,33 @@ const PreviewView: React.FC<Props> = ({
|
||||
return state
|
||||
},
|
||||
[
|
||||
collectionSlug,
|
||||
editSessionStartTime,
|
||||
globalSlug,
|
||||
serverURL,
|
||||
apiRoute,
|
||||
id,
|
||||
isLockingEnabled,
|
||||
getDocPreferences,
|
||||
getFormState,
|
||||
id,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
globalSlug,
|
||||
operation,
|
||||
schemaPath,
|
||||
getDocPreferences,
|
||||
setCurrentEditor,
|
||||
setDocumentIsLocked,
|
||||
user,
|
||||
user.id,
|
||||
setCurrentEditor,
|
||||
],
|
||||
)
|
||||
|
||||
// Clean up when the component unmounts or when the document is unlocked
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLockingEnabled) {
|
||||
return
|
||||
}
|
||||
@@ -415,7 +436,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{BeforeDocument}
|
||||
<DocumentFields
|
||||
AfterFields={AfterFields}
|
||||
BeforeFields={BeforeFields}
|
||||
@@ -423,7 +443,7 @@ const PreviewView: React.FC<Props> = ({
|
||||
fields={fields}
|
||||
forceSidebarWrap
|
||||
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
|
||||
schemaPath={collectionSlug || globalSlug}
|
||||
schemaPathSegments={[collectionSlug || globalSlug]}
|
||||
/>
|
||||
{AfterDocument}
|
||||
</div>
|
||||
@@ -464,11 +484,6 @@ export const LivePreviewClient: React.FC<{
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.livePreview?.actions
|
||||
}
|
||||
/>
|
||||
<LivePreviewProvider
|
||||
breakpoints={breakpoints}
|
||||
fieldSchema={collectionConfig?.fields || globalConfig?.fields}
|
||||
|
||||
@@ -17,9 +17,11 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
|
||||
if (type === 'email') {
|
||||
return (
|
||||
<EmailField
|
||||
autoComplete="email"
|
||||
field={{
|
||||
name: 'email',
|
||||
admin: {
|
||||
autoComplete: 'email',
|
||||
},
|
||||
label: t('general:email'),
|
||||
required,
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -28,23 +28,6 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
routes: { admin },
|
||||
} = config
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedBeforeLogins = createMappedComponent(beforeLogin, undefined, undefined, 'beforeLogin')
|
||||
|
||||
const mappedAfterLogins = createMappedComponent(afterLogin, undefined, undefined, 'afterLogin')
|
||||
|
||||
if (user) {
|
||||
redirect((searchParams.redirect as string) || admin)
|
||||
}
|
||||
@@ -82,7 +65,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
<RenderComponent mappedComponent={mappedBeforeLogins} />
|
||||
<RenderServerComponent
|
||||
Component={beforeLogin}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
{!collectionConfig?.auth?.disableLocalStrategy && (
|
||||
<LoginForm
|
||||
prefillEmail={prefillEmail}
|
||||
@@ -91,7 +86,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
)}
|
||||
<RenderComponent mappedComponent={mappedAfterLogins} />
|
||||
<RenderServerComponent
|
||||
Component={afterLogin}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,13 +75,26 @@ export const ResetPasswordForm: React.FC<Args> = ({ token }) => {
|
||||
label: i18n.t('authentication:newPassword'),
|
||||
required: true,
|
||||
}}
|
||||
indexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath=""
|
||||
path="password"
|
||||
schemaPath={`${userSlug}.password`}
|
||||
/>
|
||||
<ConfirmPasswordField />
|
||||
<HiddenField
|
||||
field={{
|
||||
name: 'token',
|
||||
type: 'text',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
}}
|
||||
forceUsePathFromProps
|
||||
indexPath=""
|
||||
parentPath={userSlug}
|
||||
parentSchemaPath={userSlug}
|
||||
path="token"
|
||||
schemaPath={`${userSlug}.token`}
|
||||
value={token}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { AdminViewComponent, AdminViewProps, ImportMap, SanitizedConfig } from 'payload'
|
||||
import type {
|
||||
AdminViewComponent,
|
||||
AdminViewProps,
|
||||
CustomComponent,
|
||||
EditConfig,
|
||||
ImportMap,
|
||||
SanitizedConfig,
|
||||
} from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
@@ -46,6 +53,20 @@ const oneSegmentViews: OneSegmentViews = {
|
||||
unauthorized: UnauthorizedView,
|
||||
}
|
||||
|
||||
function getViewActions({
|
||||
editConfig,
|
||||
viewKey,
|
||||
}: {
|
||||
editConfig: EditConfig
|
||||
viewKey: keyof EditConfig
|
||||
}): CustomComponent[] | undefined {
|
||||
if (editConfig && viewKey in editConfig && 'actions' in editConfig[viewKey]) {
|
||||
return editConfig[viewKey].actions
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const getViewFromConfig = ({
|
||||
adminRoute,
|
||||
config,
|
||||
@@ -65,8 +86,10 @@ export const getViewFromConfig = ({
|
||||
}): {
|
||||
DefaultView: ViewFromConfig
|
||||
initPageOptions: Parameters<typeof initPage>[0]
|
||||
serverProps: Record<string, unknown>
|
||||
templateClassName: string
|
||||
templateType: 'default' | 'minimal'
|
||||
viewActions?: CustomComponent[]
|
||||
} => {
|
||||
let ViewToRender: ViewFromConfig = null
|
||||
let templateClassName: string
|
||||
@@ -79,10 +102,30 @@ export const getViewFromConfig = ({
|
||||
searchParams,
|
||||
}
|
||||
|
||||
const [segmentOne, segmentTwo] = segments
|
||||
let viewActions: CustomComponent[] = config?.admin?.components?.actions || []
|
||||
|
||||
const [segmentOne, segmentTwo, segmentThree, segmentFour, segmentFive] = segments
|
||||
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
const isCollection = segmentOne === 'collections'
|
||||
let matchedCollection: SanitizedConfig['collections'][number] = undefined
|
||||
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
|
||||
|
||||
let serverProps = {}
|
||||
|
||||
if (isCollection) {
|
||||
matchedCollection = config.collections.find(({ slug }) => slug === segmentTwo)
|
||||
serverProps = {
|
||||
collectionConfig: matchedCollection,
|
||||
}
|
||||
}
|
||||
|
||||
if (isGlobal) {
|
||||
matchedGlobal = config.globals.find(({ slug }) => slug === segmentTwo)
|
||||
serverProps = {
|
||||
globalConfig: matchedGlobal,
|
||||
}
|
||||
}
|
||||
|
||||
switch (segments.length) {
|
||||
case 0: {
|
||||
@@ -146,7 +189,7 @@ export const getViewFromConfig = ({
|
||||
templateType = 'minimal'
|
||||
}
|
||||
|
||||
if (isCollection) {
|
||||
if (isCollection && matchedCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
|
||||
ViewToRender = {
|
||||
@@ -155,7 +198,8 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `${segmentTwo}-list`
|
||||
templateType = 'default'
|
||||
} else if (isGlobal) {
|
||||
viewActions = viewActions.concat(matchedCollection.admin.components?.views?.list?.actions)
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
// --> /globals/:globalSlug
|
||||
|
||||
ViewToRender = {
|
||||
@@ -164,6 +208,14 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = 'global-edit'
|
||||
templateType = 'default'
|
||||
|
||||
// add default view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'default',
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -176,13 +228,13 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = 'verify'
|
||||
templateType = 'minimal'
|
||||
} else if (isCollection) {
|
||||
} else if (isCollection && matchedCollection) {
|
||||
// Custom Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionId
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
@@ -190,7 +242,65 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
} else if (isGlobal) {
|
||||
|
||||
// Adds view actions to the current collection view
|
||||
if (matchedCollection.admin?.components?.views?.edit) {
|
||||
if ('root' in matchedCollection.admin.components.views.edit) {
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views?.edit,
|
||||
viewKey: 'root',
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
if (segmentFive) {
|
||||
if (segmentFour === 'versions') {
|
||||
// add version view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views?.edit,
|
||||
viewKey: 'version',
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else if (segmentFour) {
|
||||
if (segmentFour === 'versions') {
|
||||
// add versions view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views.edit,
|
||||
viewKey: 'versions',
|
||||
}),
|
||||
)
|
||||
} else if (segmentFour === 'preview') {
|
||||
// add livePreview view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views.edit,
|
||||
viewKey: 'livePreview',
|
||||
}),
|
||||
)
|
||||
} else if (segmentFour === 'api') {
|
||||
// add api view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views.edit,
|
||||
viewKey: 'api',
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else if (segmentThree) {
|
||||
// add default view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views.edit,
|
||||
viewKey: 'default',
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
// Custom Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/preview
|
||||
@@ -203,6 +313,56 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `global-edit`
|
||||
templateType = 'default'
|
||||
|
||||
// Adds view actions to the current global view
|
||||
if (matchedGlobal.admin?.components?.views?.edit) {
|
||||
if ('root' in matchedGlobal.admin.components.views.edit) {
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin.components?.views?.edit,
|
||||
viewKey: 'root',
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
if (segmentFour) {
|
||||
if (segmentThree === 'versions') {
|
||||
// add version view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'version',
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else if (segmentThree) {
|
||||
if (segmentThree === 'versions') {
|
||||
// add versions view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'versions',
|
||||
}),
|
||||
)
|
||||
} else if (segmentThree === 'preview') {
|
||||
// add livePreview view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'livePreview',
|
||||
}),
|
||||
)
|
||||
} else if (segmentThree === 'api') {
|
||||
// add api view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'api',
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -214,7 +374,9 @@ export const getViewFromConfig = ({
|
||||
return {
|
||||
DefaultView: ViewToRender,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewActions: viewActions.reverse(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type { ImportMap, MappedComponent, SanitizedConfig } from 'payload'
|
||||
import type { ImportMap, SanitizedConfig } from 'payload'
|
||||
|
||||
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
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'
|
||||
|
||||
@@ -55,7 +57,14 @@ export const RootPage = async ({
|
||||
|
||||
const searchParams = await searchParamsPromise
|
||||
|
||||
const { DefaultView, initPageOptions, templateClassName, templateType } = getViewFromConfig({
|
||||
const {
|
||||
DefaultView,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewActions,
|
||||
} = getViewFromConfig({
|
||||
adminRoute,
|
||||
config,
|
||||
currentRoute,
|
||||
@@ -64,21 +73,22 @@ export const RootPage = async ({
|
||||
segments,
|
||||
})
|
||||
|
||||
let dbHasUser = false
|
||||
|
||||
const initPageResult = await initPage(initPageOptions)
|
||||
|
||||
dbHasUser = await initPageResult?.req.payload.db
|
||||
.findOne({
|
||||
collection: userSlug,
|
||||
req: initPageResult?.req,
|
||||
})
|
||||
?.then((doc) => !!doc)
|
||||
const dbHasUser =
|
||||
initPageResult.req.user ||
|
||||
(await initPageResult?.req.payload.db
|
||||
.findOne({
|
||||
collection: userSlug,
|
||||
req: initPageResult?.req,
|
||||
})
|
||||
?.then((doc) => !!doc))
|
||||
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||
if (initPageResult?.req?.user) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
if (dbHasUser) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
@@ -111,27 +121,30 @@ export const RootPage = async ({
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
const createMappedView = getCreateMappedComponent({
|
||||
importMap,
|
||||
serverProps: {
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
params,
|
||||
payload: initPageResult?.req.payload,
|
||||
searchParams,
|
||||
},
|
||||
const clientConfig = await getClientConfig({
|
||||
config,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
})
|
||||
|
||||
const MappedView: MappedComponent = createMappedView(
|
||||
DefaultView.payloadComponent,
|
||||
undefined,
|
||||
DefaultView.Component,
|
||||
'createMappedView',
|
||||
const RenderedView = (
|
||||
<RenderServerComponent
|
||||
clientProps={{ clientConfig }}
|
||||
Component={DefaultView.payloadComponent}
|
||||
Fallback={DefaultView.Component}
|
||||
importMap={importMap}
|
||||
serverProps={{
|
||||
...serverProps,
|
||||
clientConfig,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
params,
|
||||
payload: initPageResult?.req.payload,
|
||||
searchParams,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const RenderedView = <RenderComponent mappedComponent={MappedView} />
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{!templateType && <Fragment>{RenderedView}</Fragment>}
|
||||
@@ -147,6 +160,7 @@ export const RootPage = async ({
|
||||
permissions={initPageResult?.permissions}
|
||||
searchParams={searchParams}
|
||||
user={initPageResult?.req.user}
|
||||
viewActions={viewActions}
|
||||
visibleEntities={{
|
||||
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
|
||||
// which this caused as soon as initPageResult.visibleEntities is passed in
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig, ClientGlobalConfig, OptionObject } from 'payload'
|
||||
|
||||
import {
|
||||
Gutter,
|
||||
SetViewActions,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
usePayloadAPI,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { Gutter, useConfig, useDocumentInfo, usePayloadAPI, useTranslation } from '@payloadcms/ui'
|
||||
import { formatDate } from '@payloadcms/ui/shared'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
@@ -80,11 +73,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
|
||||
return (
|
||||
<main className={baseClass}>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.version?.actions
|
||||
}
|
||||
/>
|
||||
<SetStepNav
|
||||
collectionConfig={collectionConfig}
|
||||
collectionSlug={collectionSlug}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { SanitizedCollectionConfig, SanitizedConfig, SanitizedGlobalConfig } from 'payload'
|
||||
import type {
|
||||
PaginatedDocs,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
TypeWithVersion,
|
||||
} from 'payload'
|
||||
|
||||
import { type Column, SortColumn } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
@@ -11,6 +17,7 @@ import { IDCell } from './cells/ID/index.js'
|
||||
export const buildVersionColumns = ({
|
||||
collectionConfig,
|
||||
docID,
|
||||
docs,
|
||||
globalConfig,
|
||||
i18n: { t },
|
||||
latestDraftVersion,
|
||||
@@ -19,6 +26,7 @@ export const buildVersionColumns = ({
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
config: SanitizedConfig
|
||||
docID?: number | string
|
||||
docs: PaginatedDocs<TypeWithVersion<any>>['docs']
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
i18n: I18n
|
||||
latestDraftVersion?: string
|
||||
@@ -30,56 +38,37 @@ export const buildVersionColumns = ({
|
||||
{
|
||||
accessor: 'updatedAt',
|
||||
active: true,
|
||||
cellProps: {
|
||||
field: {
|
||||
name: '',
|
||||
type: 'date',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: (
|
||||
<CreatedAtCell
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
docID={docID}
|
||||
globalSlug={globalConfig?.slug}
|
||||
/>
|
||||
),
|
||||
},
|
||||
Label: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: {
|
||||
name: '',
|
||||
type: 'date',
|
||||
},
|
||||
Heading: <SortColumn Label={t('general:updatedAt')} name="updatedAt" />,
|
||||
renderedCells: docs.map((doc, i) => {
|
||||
return (
|
||||
<CreatedAtCell
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
docID={docID}
|
||||
globalSlug={globalConfig?.slug}
|
||||
key={i}
|
||||
rowData={{
|
||||
id: doc.id,
|
||||
updatedAt: doc.updatedAt,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}),
|
||||
},
|
||||
{
|
||||
accessor: 'id',
|
||||
active: true,
|
||||
cellProps: {
|
||||
field: {
|
||||
name: '',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: <IDCell />,
|
||||
},
|
||||
Label: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: {
|
||||
name: '',
|
||||
type: 'text',
|
||||
},
|
||||
Heading: <SortColumn disable Label={t('version:versionID')} name="id" />,
|
||||
renderedCells: docs.map((doc, i) => {
|
||||
return <IDCell id={doc.id} key={i} />
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -90,31 +79,21 @@ export const buildVersionColumns = ({
|
||||
columns.push({
|
||||
accessor: '_status',
|
||||
active: true,
|
||||
cellProps: {
|
||||
field: {
|
||||
name: '',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: (
|
||||
<AutosaveCell
|
||||
latestDraftVersion={latestDraftVersion}
|
||||
latestPublishedVersion={latestPublishedVersion}
|
||||
/>
|
||||
),
|
||||
},
|
||||
Label: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: {
|
||||
name: '',
|
||||
type: 'checkbox',
|
||||
},
|
||||
Heading: <SortColumn disable Label={t('version:status')} name="status" />,
|
||||
renderedCells: docs.map((doc, i) => {
|
||||
return (
|
||||
<AutosaveCell
|
||||
key={i}
|
||||
latestDraftVersion={latestDraftVersion}
|
||||
latestPublishedVersion={latestPublishedVersion}
|
||||
rowData={doc}
|
||||
/>
|
||||
)
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
'use client'
|
||||
import { Pill, useConfig, useTableCell, useTranslation } from '@payloadcms/ui'
|
||||
import { Pill, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
type AutosaveCellProps = {
|
||||
latestDraftVersion?: string
|
||||
latestPublishedVersion?: string
|
||||
rowData?: {
|
||||
autosave?: boolean
|
||||
publishedLocale?: string
|
||||
version: {
|
||||
_status?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const renderPill = (data, latestVersion, currentLabel, previousLabel, pillStyle) => {
|
||||
@@ -23,9 +30,10 @@ export const renderPill = (data, latestVersion, currentLabel, previousLabel, pil
|
||||
export const AutosaveCell: React.FC<AutosaveCellProps> = ({
|
||||
latestDraftVersion,
|
||||
latestPublishedVersion,
|
||||
rowData = { autosave: undefined, publishedLocale: undefined, version: undefined },
|
||||
}) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
const { rowData } = useTableCell()
|
||||
|
||||
const {
|
||||
config: { localization },
|
||||
} = useConfig()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { useConfig, useTableCell, useTranslation } from '@payloadcms/ui'
|
||||
import { useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { formatAdminURL, formatDate } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React from 'react'
|
||||
@@ -10,12 +10,17 @@ type CreatedAtCellProps = {
|
||||
collectionSlug?: string
|
||||
docID?: number | string
|
||||
globalSlug?: string
|
||||
rowData?: {
|
||||
id: number | string
|
||||
updatedAt: Date | number | string
|
||||
}
|
||||
}
|
||||
|
||||
export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||
collectionSlug,
|
||||
docID,
|
||||
globalSlug,
|
||||
rowData: { id, updatedAt } = {},
|
||||
}) => {
|
||||
const {
|
||||
config: {
|
||||
@@ -26,30 +31,25 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const { cellData, rowData } = useTableCell()
|
||||
|
||||
const versionID = rowData.id
|
||||
|
||||
let to: string
|
||||
|
||||
if (collectionSlug) {
|
||||
to = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${docID}/versions/${versionID}`,
|
||||
path: `/collections/${collectionSlug}/${docID}/versions/${id}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
to = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${globalSlug}/versions/${versionID}`,
|
||||
path: `/globals/${globalSlug}/versions/${id}`,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={to} prefetch={false}>
|
||||
{cellData &&
|
||||
formatDate({ date: cellData as Date | number | string, i18n, pattern: dateFormat })}
|
||||
{formatDate({ date: updatedAt, i18n, pattern: dateFormat })}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
'use client'
|
||||
import { useTableCell } from '@payloadcms/ui'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
export const IDCell: React.FC = () => {
|
||||
const { cellData } = useTableCell()
|
||||
return <Fragment>{cellData as number | string}</Fragment>
|
||||
export function IDCell({ id }: { id: number | string }) {
|
||||
return <Fragment>{id}</Fragment>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig, ClientGlobalConfig, SanitizedCollectionConfig } from 'payload'
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
type Column,
|
||||
LoadingOverlayToggle,
|
||||
Pagination,
|
||||
PerPage,
|
||||
SetViewActions,
|
||||
Table,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useListQuery,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
@@ -24,14 +21,8 @@ export const VersionsViewClient: React.FC<{
|
||||
}> = (props) => {
|
||||
const { baseClass, columns, paginationLimits } = props
|
||||
|
||||
const { collectionSlug, globalSlug } = useDocumentInfo()
|
||||
const { data, handlePageChange, handlePerPageChange } = useListQuery()
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const limit = searchParams.get('limit')
|
||||
|
||||
@@ -41,11 +32,6 @@ export const VersionsViewClient: React.FC<{
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.versions?.actions
|
||||
}
|
||||
/>
|
||||
<LoadingOverlayToggle name="versions" show={!data} />
|
||||
{versionCount === 0 && (
|
||||
<div className={`${baseClass}__no-versions`}>
|
||||
@@ -54,11 +40,7 @@ export const VersionsViewClient: React.FC<{
|
||||
)}
|
||||
{versionCount > 0 && (
|
||||
<React.Fragment>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data?.docs}
|
||||
fields={(collectionConfig || globalConfig)?.fields}
|
||||
/>
|
||||
<Table columns={columns} data={data?.docs} />
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Pagination
|
||||
hasNextPage={data.hasNextPage}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { EditViewComponent, PaginatedDocs, PayloadServerReactComponent } from 'payload'
|
||||
|
||||
import { Gutter, ListQueryProvider } from '@payloadcms/ui'
|
||||
import { Gutter, ListQueryProvider, SetDocumentStepNav } from '@payloadcms/ui'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import { isNumber } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import { buildVersionColumns } from './buildColumns.js'
|
||||
import { getLatestVersion } from './getLatestVersion.js'
|
||||
import { VersionsViewClient } from './index.client.js'
|
||||
@@ -165,6 +164,7 @@ export const VersionsView: PayloadServerReactComponent<EditViewComponent> = asyn
|
||||
collectionConfig,
|
||||
config,
|
||||
docID: id,
|
||||
docs: versionsData?.docs,
|
||||
globalConfig,
|
||||
i18n,
|
||||
latestDraftVersion: latestDraftVersion?.id,
|
||||
@@ -190,6 +190,7 @@ export const VersionsView: PayloadServerReactComponent<EditViewComponent> = asyn
|
||||
<main className={baseClass}>
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<ListQueryProvider
|
||||
collectionSlug={collectionSlug}
|
||||
data={versionsData}
|
||||
defaultLimit={limitToUse}
|
||||
defaultSort={sort as string}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/withPayload.js" /* Include the withPayload.js file in the build */
|
||||
],
|
||||
, "../ui/src/utilities/renderFields.tsx" ],
|
||||
"references": [
|
||||
{ "path": "../payload" },
|
||||
{ "path": "../ui" },
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { JSONSchema4 } from 'json-schema'
|
||||
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
|
||||
import type { ValidationFieldError } from '../errors/ValidationError.js'
|
||||
import type {
|
||||
Field,
|
||||
FieldAffectingData,
|
||||
RichTextField,
|
||||
RichTextFieldClient,
|
||||
@@ -15,7 +15,7 @@ import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { RequestContext } from '../index.js'
|
||||
import type { JsonObject, Payload, PayloadRequest, PopulateType } from '../types/index.js'
|
||||
import type { RichTextFieldClientProps } from './fields/RichText.js'
|
||||
import type { CreateMappedComponent } from './types.js'
|
||||
import type { FieldSchemaMap } from './types.js'
|
||||
|
||||
export type AfterReadRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
@@ -91,7 +91,7 @@ export type BeforeChangeRichTextHookArgs<
|
||||
|
||||
duplicate?: boolean
|
||||
|
||||
errors?: { field: string; message: string }[]
|
||||
errors?: ValidationFieldError[]
|
||||
/** Only available in `beforeChange` field hooks */
|
||||
mergeLocaleActions?: (() => Promise<void>)[]
|
||||
/** A string relating to which operation the field type is currently executing within. */
|
||||
@@ -184,32 +184,19 @@ export type RichTextHooks = {
|
||||
beforeChange?: BeforeChangeRichTextHook[]
|
||||
beforeValidate?: BeforeValidateRichTextHook[]
|
||||
}
|
||||
|
||||
export type RichTextGenerateComponentMap = (args: {
|
||||
clientField: RichTextFieldClient
|
||||
createMappedComponent: CreateMappedComponent
|
||||
field: RichTextField
|
||||
i18n: I18nClient
|
||||
|
||||
importMap: ImportMap
|
||||
payload: Payload
|
||||
schemaPath: string
|
||||
}) => Map<string, unknown>
|
||||
|
||||
type RichTextAdapterBase<
|
||||
Value extends object = object,
|
||||
AdapterProps = any,
|
||||
ExtraFieldProperties = {},
|
||||
> = {
|
||||
generateComponentMap: PayloadComponent<any, never>
|
||||
generateImportMap?: Config['admin']['importMap']['generators'][0]
|
||||
generateSchemaMap?: (args: {
|
||||
config: SanitizedConfig
|
||||
field: RichTextField
|
||||
i18n: I18n<any, any>
|
||||
schemaMap: Map<string, Field[]>
|
||||
schemaMap: FieldSchemaMap
|
||||
schemaPath: string
|
||||
}) => Map<string, Field[]>
|
||||
}) => FieldSchemaMap
|
||||
/**
|
||||
* Like an afterRead hook, but runs only for the GraphQL resolver. For populating data, this should be used, as afterRead hooks do not have a depth in graphQL.
|
||||
*
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { ClientCollectionConfig } from '../../collections/config/client.js'
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||
import type { ClientField } from '../../fields/config/client.js'
|
||||
|
||||
export type RowData = Record<string, any>
|
||||
|
||||
export type CellComponentProps<TField extends ClientField = ClientField> = {
|
||||
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
|
||||
readonly cellData: TCellData
|
||||
readonly className?: string
|
||||
readonly collectionConfig: ClientCollectionConfig
|
||||
readonly columnIndex?: number
|
||||
readonly customCellProps?: Record<string, any>
|
||||
readonly field: TField
|
||||
readonly link?: boolean
|
||||
readonly onClick?: (args: {
|
||||
@@ -12,13 +17,5 @@ export type CellComponentProps<TField extends ClientField = ClientField> = {
|
||||
collectionSlug: SanitizedCollectionConfig['slug']
|
||||
rowData: RowData
|
||||
}) => void
|
||||
}
|
||||
|
||||
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
|
||||
readonly cellData: TCellData
|
||||
readonly customCellContext?: {
|
||||
collectionSlug?: SanitizedCollectionConfig['slug']
|
||||
uploadConfig?: SanitizedCollectionConfig['upload']
|
||||
}
|
||||
readonly rowData: RowData
|
||||
} & CellComponentProps<TField>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { ArrayField, ArrayFieldClient } from '../../fields/config/types.js'
|
||||
import type { ArrayField, ArrayFieldClient, ClientField } from '../../fields/config/types.js'
|
||||
import type { ArrayFieldValidation } from '../../fields/validations.js'
|
||||
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
||||
import type {
|
||||
@@ -14,15 +14,14 @@ import type {
|
||||
FieldDescriptionServerComponent,
|
||||
FieldLabelClientComponent,
|
||||
FieldLabelServerComponent,
|
||||
MappedComponent,
|
||||
} from '../types.js'
|
||||
|
||||
type ArrayFieldClientWithoutType = MarkOptional<ArrayFieldClient, 'type'>
|
||||
|
||||
type ArrayFieldBaseClientProps = {
|
||||
readonly CustomRowLabel?: MappedComponent
|
||||
readonly path?: string
|
||||
readonly validate?: ArrayFieldValidation
|
||||
}
|
||||
} & Pick<ServerFieldBase, 'permissions'>
|
||||
|
||||
export type ArrayFieldClientProps = ArrayFieldBaseClientProps &
|
||||
ClientFieldBase<ArrayFieldClientWithoutType>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { BlocksField, BlocksFieldClient } from '../../fields/config/types.js'
|
||||
import type { BlocksField, BlocksFieldClient, ClientField } from '../../fields/config/types.js'
|
||||
import type { BlocksFieldValidation } from '../../fields/validations.js'
|
||||
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
||||
import type {
|
||||
@@ -19,8 +19,9 @@ import type {
|
||||
type BlocksFieldClientWithoutType = MarkOptional<BlocksFieldClient, 'type'>
|
||||
|
||||
type BlocksFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: BlocksFieldValidation
|
||||
}
|
||||
} & Pick<ServerFieldBase, 'permissions'>
|
||||
|
||||
export type BlocksFieldClientProps = BlocksFieldBaseClientProps &
|
||||
ClientFieldBase<BlocksFieldClientWithoutType>
|
||||
|
||||
@@ -24,6 +24,7 @@ type CheckboxFieldBaseClientProps = {
|
||||
readonly id?: string
|
||||
readonly onChange?: (value: boolean) => void
|
||||
readonly partialChecked?: boolean
|
||||
readonly path?: string
|
||||
readonly validate?: CheckboxFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ type CodeFieldClientWithoutType = MarkOptional<CodeFieldClient, 'type'>
|
||||
|
||||
type CodeFieldBaseClientProps = {
|
||||
readonly autoComplete?: string
|
||||
readonly valiCode?: CodeFieldValidation
|
||||
readonly path?: string
|
||||
readonly validate?: CodeFieldValidation
|
||||
}
|
||||
|
||||
export type CodeFieldClientProps = ClientFieldBase<CodeFieldClientWithoutType> &
|
||||
|
||||
@@ -15,9 +15,14 @@ import type {
|
||||
FieldLabelServerComponent,
|
||||
} from '../types.js'
|
||||
|
||||
type CollapsibleFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
} & Pick<ServerFieldBase, 'permissions'>
|
||||
|
||||
type CollapsibleFieldClientWithoutType = MarkOptional<CollapsibleFieldClient, 'type'>
|
||||
|
||||
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType>
|
||||
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType> &
|
||||
CollapsibleFieldBaseClientProps
|
||||
|
||||
export type CollapsibleFieldServerProps = ServerFieldBase<
|
||||
CollapsibleField,
|
||||
@@ -29,8 +34,10 @@ export type CollapsibleFieldServerComponent = FieldServerComponent<
|
||||
CollapsibleFieldClientWithoutType
|
||||
>
|
||||
|
||||
export type CollapsibleFieldClientComponent =
|
||||
FieldClientComponent<CollapsibleFieldClientWithoutType>
|
||||
export type CollapsibleFieldClientComponent = FieldClientComponent<
|
||||
CollapsibleFieldClientWithoutType,
|
||||
CollapsibleFieldBaseClientProps
|
||||
>
|
||||
|
||||
export type CollapsibleFieldLabelServerComponent = FieldLabelServerComponent<
|
||||
CollapsibleField,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type DateFieldClientWithoutType = MarkOptional<DateFieldClient, 'type'>
|
||||
|
||||
type DateFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: DateFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
type EmailFieldClientWithoutType = MarkOptional<EmailFieldClient, 'type'>
|
||||
|
||||
type EmailFieldBaseClientProps = {
|
||||
readonly autoComplete?: string
|
||||
readonly path?: string
|
||||
readonly validate?: EmailFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,12 @@ import type {
|
||||
|
||||
type GroupFieldClientWithoutType = MarkOptional<GroupFieldClient, 'type'>
|
||||
|
||||
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType>
|
||||
export type GroupFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
} & Pick<ServerFieldBase, 'permissions'>
|
||||
|
||||
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType> &
|
||||
GroupFieldBaseClientProps
|
||||
|
||||
export type GroupFieldServerProps = ServerFieldBase<GroupField, GroupFieldClientWithoutType>
|
||||
|
||||
@@ -26,7 +31,10 @@ export type GroupFieldServerComponent = FieldServerComponent<
|
||||
GroupFieldClientWithoutType
|
||||
>
|
||||
|
||||
export type GroupFieldClientComponent = FieldClientComponent<GroupFieldClientWithoutType>
|
||||
export type GroupFieldClientComponent = FieldClientComponent<
|
||||
GroupFieldClientWithoutType,
|
||||
GroupFieldBaseClientProps
|
||||
>
|
||||
|
||||
export type GroupFieldLabelServerComponent = FieldLabelServerComponent<
|
||||
GroupField,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ClientField } from '../../fields/config/client.js'
|
||||
import type { FormFieldBase } from '../types.js'
|
||||
import type { ClientField } from '../../fields/config/types.js'
|
||||
import type { ClientFieldBase } from '../types.js'
|
||||
|
||||
export type HiddenFieldProps = {
|
||||
type HiddenFieldBaseClientProps = {
|
||||
readonly disableModifyingForm?: false
|
||||
readonly field?: {
|
||||
readonly name?: string
|
||||
} & Pick<ClientField, '_path'>
|
||||
readonly forceUsePathFromProps?: boolean
|
||||
} & ClientField
|
||||
readonly value?: unknown
|
||||
} & FormFieldBase
|
||||
}
|
||||
|
||||
export type HiddenFieldProps = ClientFieldBase & HiddenFieldBaseClientProps
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type JSONFieldClientWithoutType = MarkOptional<JSONFieldClient, 'type'>
|
||||
|
||||
type JSONFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: JSONFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ import type {
|
||||
|
||||
type JoinFieldClientWithoutType = MarkOptional<JoinFieldClient, 'type'>
|
||||
|
||||
export type JoinFieldClientProps = ClientFieldBase<JoinFieldClientWithoutType>
|
||||
export type JoinFieldClientProps = {
|
||||
path?: string
|
||||
} & ClientFieldBase<JoinFieldClientWithoutType>
|
||||
|
||||
export type JoinFieldServerProps = ServerFieldBase<JoinField>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ type NumberFieldClientWithoutType = MarkOptional<NumberFieldClient, 'type'>
|
||||
|
||||
type NumberFieldBaseClientProps = {
|
||||
readonly onChange?: (e: number) => void
|
||||
readonly path?: string
|
||||
readonly validate?: NumberFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type PointFieldClientWithoutType = MarkOptional<PointFieldClient, 'type'>
|
||||
|
||||
type PointFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: PointFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ type RadioFieldBaseClientProps = {
|
||||
*/
|
||||
readonly disableModifyingForm?: boolean
|
||||
readonly onChange?: OnChange
|
||||
readonly path?: string
|
||||
readonly validate?: RadioFieldValidation
|
||||
readonly value?: string
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type RelationshipFieldClientWithoutType = MarkOptional<RelationshipFieldClient, 'type'>
|
||||
|
||||
type RelationshipFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: RelationshipFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,18 @@ import type {
|
||||
FieldLabelServerComponent,
|
||||
} from '../types.js'
|
||||
|
||||
type RichTextFieldClientWithoutType = MarkOptional<RichTextFieldClient, 'type'>
|
||||
type RichTextFieldClientWithoutType<
|
||||
TValue extends object = any,
|
||||
TAdapterProps = any,
|
||||
TExtraProperties = object,
|
||||
> = MarkOptional<RichTextFieldClient<TValue, TAdapterProps, TExtraProperties>, 'type'>
|
||||
|
||||
type RichTextFieldBaseClientProps<
|
||||
TValue extends object = any,
|
||||
TAdapterProps = any,
|
||||
TExtraProperties = object,
|
||||
> = {
|
||||
readonly path?: string
|
||||
readonly validate?: RichTextFieldValidation
|
||||
}
|
||||
|
||||
@@ -30,7 +35,7 @@ export type RichTextFieldClientProps<
|
||||
TValue extends object = any,
|
||||
TAdapterProps = any,
|
||||
TExtraProperties = object,
|
||||
> = ClientFieldBase<RichTextFieldClientWithoutType> &
|
||||
> = ClientFieldBase<RichTextFieldClientWithoutType<TValue, TAdapterProps, TExtraProperties>> &
|
||||
RichTextFieldBaseClientProps<TValue, TAdapterProps, TExtraProperties>
|
||||
|
||||
export type RichTextFieldServerProps = ServerFieldBase<
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user